Merge "Update image creation error msg to mention the correct image type"
diff --git a/.coveragerc b/.coveragerc
index c9b6467..449e62c 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -1,4 +1,4 @@
[run]
branch = True
source = tempest
-omit = tempest/tests/*,tempest/openstack/*
+omit = tempest/tests/*,tempest/scenario/test_*.py,tempest/api/*
diff --git a/.gitignore b/.gitignore
index efba45e..7cb052f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,8 @@
AUTHORS
ChangeLog
*.pyc
+__pycache__/
+etc/accounts.yaml
etc/tempest.conf
etc/tempest.conf.sample
etc/logging.conf
@@ -16,7 +18,15 @@
dist
build
.testrepository
+.stestr
+.idea
+.project
+.pydevproject
.coverage*
!.coveragerc
cover/
doc/source/_static/tempest.conf.sample
+doc/source/plugin-registry.rst
+
+# Files created by releasenotes build
+releasenotes/build
diff --git a/.mailmap b/.mailmap
index 5c37a5e..3ea6ab0 100644
--- a/.mailmap
+++ b/.mailmap
@@ -1,9 +1,27 @@
-Ravikumar Venkatesan <ravikumar.venkatesan@hp.com> ravikumar-venkatesan <ravikumar.venkatesan@hp.com>
-Ravikumar Venkatesan <ravikumar.venkatesan@hp.com> ravikumar venkatesan <ravikumar.venkatesan@hp.com>
-Rohit Karajgi <rohit.karajgi@nttdata.com> Rohit Karajgi <rohit.karajgi@vertex.co.in>
-Jay Pipes <jaypipes@gmail.com> Jay Pipes <jpipes@librebox.gateway.2wire.net>
-Joe Gordon <joe.gordon0@gmail.com> <jogo@cloudscaling.com>
<brian.waldon@rackspace.com> <bcwaldon@gmail.com>
-Daryl Walleck <daryl.walleck@rackspace.com> dwalleck <daryl.walleck@rackspace.com>
<jeblair@hp.com> <corvus@inaugust.com>
<jeblair@hp.com> <james.blair@rackspace.com>
+Adam Gandelman <adamg@ubuntu.com> <adamg@canonical.com>
+Andrea Frittoli <andrea.frittoli@gmail.com> <andrea.frittoli@hp.com>
+Andrea Frittoli <andrea.frittoli@gmail.com> <andrea.frittoli@hpe.com>
+Daryl Walleck <daryl.walleck@rackspace.com> <daryl.walleck@rackspace.com>
+David Kranz <dkranz@redhat.com> David Kranz <david.kranz@qrclab.com>
+Ghanshyam <ghanshyam.mann@nectechnologies.in> <ghanshyam.mann@nectechnologies.in>
+Ghanshyam <ghanshyam.mann@nectechnologies.in> <ghanshyam.mann@nectechnologies.in>
+Jay Pipes <jaypipes@gmail.com> <jpipes@librebox.gateway.2wire.net>
+Joe Gordon <joe.gordon0@gmail.com> <jogo@cloudscaling.com>
+Ken'ichi Ohmichi <ken-oomichi@wx.jp.nec.com> <oomichi@mxs.nes.nec.co.jp>
+Ken'ichi Ohmichi <ken-oomichi@wx.jp.nec.com> <ken1ohmichi@gmail.com>
+Marc Koderer <marc@koderer.com> <m.koderer@telekom.de>
+Masayuki Igawa <masayuki@igawa.me> <igawa@mxs.nes.nec.co.jp>
+Masayuki Igawa <masayuki@igawa.me> <mas-igawa@ut.jp.nec.com>
+Masayuki Igawa <masayuki@igawa.me> <masayuki.igawa@gmail.com>
+Matthew Treinish <mtreinish@kortar.org> <treinish@linux.vnet.ibm.com>
+Nayna Patel <nayna.patel@hp.com> <nayna.patel@hp.com>
+ravikumar-venkatesan <ravikumar.venkatesan@hp.com> <ravikumar.venkatesan@hp.com>
+ravikumar-venkatesan <ravikumar.venkatesan@hp.com> <ravikumar.venkatesan@hp.com>
+Rohit Karajgi <rohit.karajgi@nttdata.com> <rohit.karajgi@vertex.co.in>
+Sean Dague <sean@dague.net> <sdague@linux.vnet.ibm.com>
+Sean Dague <sean@dague.net> <sean.dague@samsung.com>
+Yuiko Takada <takada-yuiko@mxn.nes.nec.co.jp> <takada-yuiko@mxn.nes.nec.co.jp>
+Zhi Kun Liu <zhikunli@cn.ibm.com> <zhikunli@cn.ibm.com>
diff --git a/.stestr.conf b/.stestr.conf
new file mode 100644
index 0000000..818c743
--- /dev/null
+++ b/.stestr.conf
@@ -0,0 +1,3 @@
+[DEFAULT]
+test_path=./tempest/test_discover
+group_regex=([^\.]*\.)*
diff --git a/.testr.conf b/.testr.conf
deleted file mode 100644
index 95a4fb4..0000000
--- a/.testr.conf
+++ /dev/null
@@ -1,9 +0,0 @@
-[DEFAULT]
-test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
- OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \
- OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-500} \
- OS_TEST_LOCK_PATH=${OS_TEST_LOCK_PATH:-${TMPDIR:-'/tmp'}} \
- ${PYTHON:-python} -m subunit.run discover -t ${OS_TOP_LEVEL:-./} ${OS_TEST_PATH:-./tempest/test_discover} $LISTOPT $IDOPTION
-test_id_option=--load-list $IDFILE
-test_list_option=--list
-group_regex=([^\.]*\.)*
diff --git a/.zuul.yaml b/.zuul.yaml
new file mode 100644
index 0000000..8ab3028
--- /dev/null
+++ b/.zuul.yaml
@@ -0,0 +1,389 @@
+- job:
+ name: devstack-tempest
+ parent: devstack
+ nodeset: openstack-single-node
+ description: |
+ Base Tempest job.
+
+ This Tempest job provides the base for both the single and multi-node
+ test setup. To run a multi-node test inherit from devstack-tempest and
+ set the nodeset to a multi-node one.
+ required-projects:
+ - openstack/tempest
+ timeout: 7200
+ roles:
+ - zuul: openstack-dev/devstack
+ vars:
+ devstack_services:
+ tempest: true
+ devstack_local_conf:
+ test-config:
+ $TEMPEST_CONFIG:
+ compute:
+ min_compute_nodes: "{{ groups['compute'] | default(['controller']) | length }}"
+ test_results_stage_name: 'test_results'
+ zuul_copy_output:
+ '{{ devstack_base_dir }}/tempest/etc/tempest.conf': 'logs'
+ '{{ devstack_base_dir }}/tempest/etc/accounts.yaml': 'logs'
+ '{{ devstack_base_dir }}/tempest/tempest.log': 'logs'
+ '{{ stage_dir }}/{{ test_results_stage_name }}.subunit': 'logs'
+ '{{ stage_dir }}/{{ test_results_stage_name }}.html': 'logs'
+ '{{ stage_dir }}/stackviz': 'logs'
+ extensions_to_txt:
+ conf: True
+ log: True
+ yaml: True
+ yml: True
+ run: playbooks/devstack-tempest.yaml
+ post-run: playbooks/post-tempest.yaml
+
+- job:
+ name: tempest-full
+ parent: devstack-tempest
+ # This currently works from stable/pike on.
+ branches:
+ - master
+ - stable/queens
+ - stable/pike
+ description: |
+ Base integration test with Neutron networking and py27.
+ Former names for this job where:
+ * legacy-tempest-dsvm-neutron-full
+ * gate-tempest-dsvm-neutron-full-ubuntu-xenial
+ vars:
+ tox_envlist: full
+ devstack_localrc:
+ 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:
+ - master
+ - stable/queens
+ description: |
+ Base integration test with Neutron networking and py3.
+ Former names for this job where:
+ * legacy-tempest-dsvm-py35
+ * gate-tempest-dsvm-py35
+ vars:
+ tox_envlist: full
+ devstack_localrc:
+ USE_PYTHON3: True
+ FORCE_CONFIG_DRIVE: True
+ devstack_services:
+ s-account: false
+ s-container: false
+ s-object: false
+ s-proxy: false
+ # without Swift, c-bak cannot run (in the Gate at least)
+ c-bak: false
+
+- job:
+ name: tempest-multinode-full
+ parent: devstack-tempest
+ nodeset: openstack-two-node
+ # Until the devstack changes are backported, only run this on master
+ branches:
+ - master
+ description: |
+ Base multinode integration test with Neutron networking and py27.
+ Former names for this job where:
+ * neutron-tempest-multinode-full
+ * legacy-tempest-dsvm-neutron-multinode-full
+ * gate-tempest-dsvm-neutron-multinode-full-ubuntu-xenial-nv
+ This job includes two nodes, controller / tempest plus a subnode, but
+ it can be used with different topologies, as long as a controller node
+ and a tempest one exist.
+ vars:
+ tox_envlist: full
+ devstack_localrc:
+ FORCE_CONFIG_DRIVE: False
+ NOVA_ALLOW_MOVE_TO_SAME_HOST: false
+ LIVE_MIGRATION_AVAILABLE: true
+ USE_BLOCK_MIGRATION_FOR_LIVE_MIGRATION: true
+ group-vars:
+ peers:
+ devstack_localrc:
+ NOVA_ALLOW_MOVE_TO_SAME_HOST: false
+ LIVE_MIGRATION_AVAILABLE: true
+ USE_BLOCK_MIGRATION_FOR_LIVE_MIGRATION: true
+
+- 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
+
+# TODO(gmann): needs to migrate this to zuulv3
+- job:
+ name: tempest-scenario-multinode-lvm-multibackend
+ parent: legacy-dsvm-base-multinode
+ run: playbooks/tempest-scenario-multinode-lvm-multibackend/run.yaml
+ post-run: playbooks/tempest-scenario-multinode-lvm-multibackend/post.yaml
+ timeout: 10800
+ required-projects:
+ - openstack-infra/devstack-gate
+ - openstack/neutron
+ - openstack/tempest
+ nodeset: ubuntu-xenial-2-node
+
+- job:
+ name: tempest-full-queens
+ parent: tempest-full
+ override-checkout: stable/queens
+
+- job:
+ name: tempest-full-queens-py3
+ parent: tempest-full-py3
+ override-checkout: stable/queens
+
+- job:
+ name: tempest-full-pike
+ parent: tempest-full
+ override-checkout: stable/pike
+
+- job:
+ name: tempest-tox-plugin-sanity-check
+ parent: tox
+ description: |
+ Run tempest plugin sanity check script using tox.
+ nodeset: ubuntu-xenial
+ vars:
+ tox_envlist: plugin-sanity-check
+ voting: false
+ timeout: 5000
+ irrelevant-files:
+ - ^.*\.rst$
+ - ^doc/.*$
+ - ^etc/.*$
+ - ^releasenotes/.*$
+ - ^tempest/hacking/.*$
+ - ^tempest/tests/.*$
+ required-projects:
+ - openstack/almanach
+ - openstack/aodh
+ - openstack/barbican-tempest-plugin
+ - openstack/ceilometer
+ - openstack/cinder
+ - openstack/congress
+ - openstack/designate-tempest-plugin
+ - openstack/ec2-api
+ - openstack/freezer
+ - openstack/freezer-api
+ - openstack/freezer-tempest-plugin
+ - openstack/gce-api
+ - openstack/glare
+ - openstack/heat
+ - openstack/intel-nfv-ci-tests
+ - openstack/ironic
+ - openstack/ironic-inspector
+ - openstack/keystone-tempest-plugin
+ - openstack/kingbird
+ - openstack/kuryr-tempest-plugin
+ - openstack/magnum
+ - openstack/magnum-tempest-plugin
+ - openstack/manila
+ - openstack/manila-tempest-plugin
+ - openstack/mistral
+ - openstack/mogan
+ - openstack/monasca-api
+ - openstack/monasca-log-api
+ - openstack/murano
+ - openstack/networking-bgpvpn
+ - openstack/networking-cisco
+ - openstack/networking-fortinet
+ - openstack/networking-generic-switch
+ - openstack/networking-l2gw
+ - openstack/networking-midonet
+ - openstack/networking-plumgrid
+ - openstack/networking-sfc
+ - openstack/neutron
+ - openstack/neutron-dynamic-routing
+ - openstack/neutron-fwaas
+ - openstack/neutron-lbaas
+ - openstack/neutron-tempest-plugin
+ - openstack/neutron-vpnaas
+ - openstack/nova-lxd
+ - openstack/novajoin-tempest-plugin
+ - openstack/octavia-tempest-plugin
+ - openstack/oswin-tempest-plugin
+ - openstack/panko
+ - openstack/patrole
+ - openstack/qinling
+ - openstack/requirements
+ - openstack/sahara-tests
+ - openstack/senlin
+ - openstack/senlin-tempest-plugin
+ - openstack/tap-as-a-service
+ - openstack/tempest-horizon
+ - openstack/trio2o
+ - openstack/trove
+ - openstack/valet
+ - openstack/vitrage
+ - openstack/vmware-nsx-tempest-plugin
+ - openstack/watcher-tempest-plugin
+ - openstack/zaqar-tempest-plugin
+ - openstack/zun-tempest-plugin
+
+- job:
+ name: tempest-cinder-v2-api
+ parent: devstack-tempest
+ branches:
+ - master
+ description: |
+ This job runs the cinder API test against v2 endpoint.
+ vars:
+ tox_envlist: all
+ tempest_test_regex: api.*volume
+ devstack_localrc:
+ TEMPEST_VOLUME_TYPE: volumev2
+
+- project:
+ check:
+ jobs:
+ - devstack-tempest:
+ files:
+ - ^playbooks/
+ - ^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$
+ - ^.*\.rst$
+ - ^doc/.*$
+ - ^etc/.*$
+ - ^releasenotes/.*$
+ - ^setup.cfg$
+ - ^tempest/hacking/.*$
+ - ^tempest/tests/.*$
+ - tempest-full-queens-py3:
+ irrelevant-files:
+ - ^(test-|)requirements.txt$
+ - ^.*\.rst$
+ - ^doc/.*$
+ - ^etc/.*$
+ - ^releasenotes/.*$
+ - ^setup.cfg$
+ - ^tempest/hacking/.*$
+ - ^tempest/tests/.*$
+ - tempest-full-pike:
+ irrelevant-files:
+ - ^(test-|)requirements.txt$
+ - ^.*\.rst$
+ - ^doc/.*$
+ - ^etc/.*$
+ - ^releasenotes/.*$
+ - ^setup.cfg$
+ - ^tempest/hacking/.*$
+ - ^tempest/tests/.*$
+ - tempest-multinode-full:
+ irrelevant-files:
+ - ^(test-|)requirements.txt$
+ - ^.*\.rst$
+ - ^doc/.*$
+ - ^etc/.*$
+ - ^releasenotes/.*$
+ - ^setup.cfg$
+ - ^tempest/hacking/.*$
+ - ^tempest/tests/.*$
+ - tempest-tox-plugin-sanity-check
+ - tempest-scenario-multinode-lvm-multibackend:
+ voting: false
+ irrelevant-files:
+ - ^(test-|)requirements.txt$
+ - ^.*\.rst$
+ - ^doc/.*$
+ - ^etc/.*$
+ - ^releasenotes/.*$
+ - ^setup.cfg$
+ - ^tempest/hacking/.*$
+ - ^tempest/tests/.*$
+ - nova-cells-v1:
+ irrelevant-files:
+ - ^(test-|)requirements.txt$
+ - ^.*\.rst$
+ - ^doc/.*$
+ - ^etc/.*$
+ - ^releasenotes/.*$
+ - ^setup.cfg$
+ - ^tempest/hacking/.*$
+ - ^tempest/tests/.*$
+ gate:
+ jobs:
+ - nova-multiattach
+ experimental:
+ jobs:
+ - nova-live-migration:
+ irrelevant-files:
+ - ^(test-|)requirements.txt$
+ - ^.*\.rst$
+ - ^doc/.*$
+ - ^etc/.*$
+ - ^releasenotes/.*$
+ - ^setup.cfg$
+ - ^tempest/hacking/.*$
+ - ^tempest/tests/.*$
+ - tempest-cinder-v2-api:
+ irrelevant-files:
+ - ^(test-|)requirements.txt$
+ - ^.*\.rst$
+ - ^doc/.*$
+ - ^etc/.*$
+ - ^releasenotes/.*$
+ - ^setup.cfg$
+ - ^tempest/hacking/.*$
+ - ^tempest/tests/.*$
+ periodic-stable:
+ jobs:
+ - tempest-full-queens
+ - tempest-full-queens-py3
+ - tempest-full-pike
diff --git a/HACKING.rst b/HACKING.rst
index 0962f80..bb55ac5 100644
--- a/HACKING.rst
+++ b/HACKING.rst
@@ -2,23 +2,29 @@
====================
- Step 1: Read the OpenStack Style Commandments
- http://docs.openstack.org/developer/hacking/
+ https://docs.openstack.org/hacking/latest/
- Step 2: Read on
Tempest Specific Commandments
------------------------------
- [T102] Cannot import OpenStack python clients in tempest/api &
- tempest/scenario tests
+ tempest/scenario tests
- [T104] Scenario tests require a services decorator
- [T105] Tests cannot use setUpClass/tearDownClass
- [T106] vim configuration should not be kept in source files.
- [T107] Check that a service tag isn't in the module path
- [T108] Check no hyphen at the end of rand_name() argument
- [T109] Cannot use testtools.skip decorator; instead use
- decorators.skip_because from tempest-lib
+ decorators.skip_because from tempest.lib
- [T110] Check that service client names of GET should be consistent
+- [T111] Check that service client names of DELETE should be consistent
+- [T112] Check that tempest.lib should not import local tempest code
+- [T113] Check that tests use data_utils.rand_uuid() instead of uuid.uuid4()
+- [T114] Check that tempest.lib does not use tempest config
+- [T115] Check that admin tests should exist under admin path
- [N322] Method's default argument shouldn't be mutable
+- [T116] Unsupported 'message' Exception attribute in PY3
Test Data/Configuration
-----------------------
@@ -78,7 +84,7 @@
It is recommended to use testtools `matcher`_ for the more tricky assertions.
You can implement your own specific `matcher`_ as well.
-.. _matcher: http://testtools.readthedocs.org/en/latest/for-test-authors.html#matchers
+.. _matcher: https://testtools.readthedocs.org/en/latest/for-test-authors.html#matchers
If the test case fails you can see the related logs and the information
carried by the exception (exception class, backtrack and exception info).
@@ -97,48 +103,56 @@
Service Tagging
---------------
Service tagging is used to specify which services are exercised by a particular
-test method. You specify the services with the tempest.test.services decorator.
-For example:
+test method. You specify the services with the ``tempest.common.utils.services``
+decorator. For example:
-@services('compute', 'image')
+@utils.services('compute', 'image')
Valid service tag names are the same as the list of directories in tempest.api
that have tests.
-For scenario tests having a service tag is required. For the api tests service
-tags are only needed if the test method makes an api call (either directly or
+For scenario tests having a service tag is required. For the API tests service
+tags are only needed if the test method makes an API call (either directly or
indirectly through another service) that differs from the parent directory
-name. For example, any test that make an api call to a service other than nova
-in tempest.api.compute would require a service tag for those services, however
-they do not need to be tagged as compute.
+name. For example, any test that make an API call to a service other than Nova
+in ``tempest.api.compute`` would require a service tag for those services,
+however they do not need to be tagged as ``compute``.
Test fixtures and resources
---------------------------
Test level resources should be cleaned-up after the test execution. Clean-up
-is best scheduled using `addCleanup` which ensures that the resource cleanup
+is best scheduled using ``addCleanup`` which ensures that the resource cleanup
code is always invoked, and in reverse order with respect to the creation
order.
-Test class level resources should be defined in the `resource_setup` method of
-the test class, except for any credential obtained from the credentials
-provider, which should be set-up in the `setup_credentials` method.
+Test class level resources should be defined in the ``resource_setup`` method
+of the test class, except for any credential obtained from the credentials
+provider, which should be set-up in the ``setup_credentials`` method.
+Cleanup is best scheduled using ``addClassResourceCleanup`` which ensures that
+the cleanup code is always invoked, and in reverse order with respect to the
+creation order.
-The test base class `BaseTestCase` defines Tempest framework for class level
-fixtures. `setUpClass` and `tearDownClass` are defined here and cannot be
+In both cases - test level and class level cleanups - a wait loop should be
+scheduled before the actual delete of resources with an asynchronous delete.
+
+The test base class ``BaseTestCase`` defines Tempest framework for class level
+fixtures. ``setUpClass`` and ``tearDownClass`` are defined here and cannot be
overwritten by subclasses (enforced via hacking rule T105).
Set-up is split in a series of steps (setup stages), which can be overwritten
by test classes. Set-up stages are:
-- `skip_checks`
-- `setup_credentials`
-- `setup_clients`
-- `resource_setup`
+
+- ``skip_checks``
+- ``setup_credentials``
+- ``setup_clients``
+- ``resource_setup``
Tear-down is also split in a series of steps (teardown stages), which are
stacked for execution only if the corresponding setup stage had been
reached during the setup phase. Tear-down stages are:
-- `clear_credentials` (defined in the base test class)
-- `resource_cleanup`
+
+- ``clear_credentials`` (defined in the base test class)
+- ``resource_cleanup``
Skipping Tests
--------------
@@ -154,39 +168,44 @@
Negative Tests
--------------
-Newly added negative tests should use the negative test framework. First step
-is to create an interface description in a python file under
-`tempest/api_schema/request/`. These descriptions consists of two important
-sections for the test (one of those is mandatory):
+Error handling is an important aspect of API design and usage. Negative
+tests are a way to ensure that an application can gracefully handle
+invalid or unexpected input. However, as a black box integration test
+suite, Tempest is not suitable for handling all negative test cases, as
+the wide variety and complexity of negative tests can lead to long test
+runs and knowledge of internal implementation details. The bulk of
+negative testing should be handled with project function tests.
+All negative tests should be based on `API-WG guideline`_ . Such negative
+tests can block any changes from accurate failure code to invalid one.
- - A resource (part of the URL of the request): Resources needed for a test
- must be created in `setUpClass` and registered with `set_resource` e.g.:
- `cls.set_resource("server", server['id'])`
+.. _API-WG guideline: https://specs.openstack.org/openstack/api-wg/guidelines/http.html#failure-code-clarifications
- - A json schema: defines properties for a request.
+If facing some gray area which is not clarified on the above guideline, propose
+a new guideline to the API-WG. With a proposal to the API-WG we will be able to
+build a consensus across all OpenStack projects and improve the quality and
+consistency of all the APIs.
-After that a test class must be added to automatically generate test scenarios
-out of the given interface description::
+In addition, we have some guidelines for additional negative tests.
- load_tests = test.NegativeAutoTest.load_tests
+- About BadRequest(HTTP400) case: We can add a single negative tests of
+ BadRequest for each resource and method(POST, PUT).
+ Please don't implement more negative tests on the same combination of
+ resource and method even if API request parameters are different from
+ the existing test.
+- About NotFound(HTTP404) case: We can add a single negative tests of
+ NotFound for each resource and method(GET, PUT, DELETE, HEAD).
+ Please don't implement more negative tests on the same combination
+ of resource and method.
- @test.SimpleNegativeAutoTest
- class SampleTestNegativeTestJSON(<your base class>, test.NegativeAutoTest):
- _service = 'compute'
- _schema = <your schema file>
-
-The class decorator `SimpleNegativeAutoTest` will automatically generate test
-cases out of the given schema in the attribute `_schema`.
-
-All negative tests should be added into a separate negative test file.
-If such a file doesn't exist for the particular resource being tested a new
-test file should be added.
+The above guidelines don't cover all cases and we will grow these guidelines
+organically over time. Patches outside of the above guidelines are left up to
+the reviewers' discretion and if we face some conflicts between reviewers, we
+will expand the guideline based on our discussion and experience.
Test skips because of Known Bugs
--------------------------------
-
If a test is broken because of a bug it is appropriate to skip the test until
-bug has been fixed. You should use the skip_because decorator so that
+bug has been fixed. You should use the ``skip_because`` decorator so that
Tempest's skip tracking tool can watch the bug status.
Example::
@@ -212,45 +231,22 @@
things to watch out for to try to avoid issues when running your tests in
parallel.
-- Resources outside of a tenant scope still have the potential to conflict. This
+- Resources outside of a project scope still have the potential to conflict. This
is a larger concern for the admin tests since most resources and actions that
- require admin privileges are outside of tenants.
+ require admin privileges are outside of projects.
- Races between methods in the same class are not a problem because
- parallelization in tempest is at the test class level, but if there is a json
+ parallelization in Tempest is at the test class level, but if there is a json
and xml version of the same test class there could still be a race between
methods.
-- The rand_name() function from tempest.common.utils.data_utils should be used
- anywhere a resource is created with a name. Static naming should be avoided
- to prevent resource conflicts.
+- The rand_name() function from tempest.lib.common.utils.data_utils should be
+ used anywhere a resource is created with a name. Static naming should be
+ avoided to prevent resource conflicts.
- If the execution of a set of tests is required to be serialized then locking
- can be used to perform this. See AggregatesAdminTest in
- tempest.api.compute.admin for an example of using locking.
-
-Stress Tests in Tempest
------------------------
-Any tempest test case can be flagged as a stress test. With this flag it will
-be automatically discovery and used in the stress test runs. The stress test
-framework itself is a facility to spawn and control worker processes in order
-to find race conditions (see ``tempest/stress/`` for more information). Please
-note that these stress tests can't be used for benchmarking purposes since they
-don't measure any performance characteristics.
-
-Example::
-
- @stresstest(class_setup_per='process')
- def test_this_and_that(self):
- ...
-
-This will flag the test ``test_this_and_that`` as a stress test. The parameter
-``class_setup_per`` gives control when the setUpClass function should be called.
-
-Good candidates for stress tests are:
-
-- Scenario tests
-- API tests that have a wide focus
+ can be used to perform this. See usage of ``LockFixture`` for examples of
+ using locking.
Sample Configuration File
-------------------------
@@ -258,11 +254,11 @@
to the config variables in tempest/config.py then the sample config file must be
regenerated. This can be done running::
- tox -egenconfig
+ tox -e genconfig
Unit Tests
----------
-Unit tests are a separate class of tests in tempest. They verify tempest
+Unit tests are a separate class of tests in Tempest. They verify Tempest
itself, and thus have a different set of guidelines around them:
1. They can not require anything running externally. All you should need to
@@ -325,15 +321,15 @@
qualified test name and track test functionality through refactoring. The
format of the metadata looks like::
- @test.idempotent_id('585e934c-448e-43c4-acbf-d06a9b899997')
+ @decorators.idempotent_id('585e934c-448e-43c4-acbf-d06a9b899997')
def test_list_servers_with_detail(self):
# The created server should be in the detailed list of all servers
...
-Tempest-lib includes a ``check-uuid`` tool that will test for the existence
+Tempest.lib includes a ``check-uuid`` tool that will test for the existence
and uniqueness of idempotent_id metadata for every test. If you have
-tempest-lib installed you run the tool against Tempest by calling from the
-tempest repo::
+Tempest installed you run the tool against Tempest by calling from the
+Tempest repo::
check-uuid
@@ -348,7 +344,7 @@
check-uuid --fix
-The ``check-uuid`` tool is used as part of the tempest gate job
+The ``check-uuid`` tool is used as part of the Tempest gate job
to ensure that all tests have an ``idempotent_id`` decorator.
Branchless Tempest Considerations
@@ -361,19 +357,30 @@
proposed commits to Tempest must work against both the master and all the
currently supported stable branches of the projects. As such there are a few
special considerations that have to be accounted for when pushing new changes
-to tempest.
+to Tempest.
1. New Tests for new features
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
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 @test.requires_ext() decorator to check
-if the required extension (or discoverable optional API) is enabled or 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.
+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.
+
+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
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -381,14 +388,22 @@
When trying to land a bug fix which changes a tested API you'll have to use the
following procedure::
- - Propose change to the project, get a +2 on the change even with failing
- - Propose skip on Tempest which will only be approved after the
+ 1. Propose change to the project, get a +2 on the change even with failing
+ 2. Propose skip on Tempest which will only be approved after the
corresponding change in the project has a +2 on change
- - Land project change in master and all open stable branches (if required)
- - Land changed test in Tempest
+ 3. Land project change in master and all open stable branches (if required)
+ 4. Land changed test in Tempest
Otherwise the bug fix won't be able to land in the project.
+Handily, `Zuul's cross-repository dependencies
+<https://docs.openstack.org/infra/zuul/user/gating.html#cross-project-dependencies>`_.
+can be leveraged to do without step 2 and to have steps 3 and 4 happen
+"atomically". To do that, make the patch written in step 1 to depend (refer to
+Zuul's documentation above) on the patch written in step 4. The commit message
+for the Tempest change should have a link to the Gerrit review that justifies
+that change.
+
3. New Tests for existing features
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/README.rst b/README.rst
index 71e185f..2243536 100644
--- a/README.rst
+++ b/README.rst
@@ -1,9 +1,21 @@
+========================
+Team and repository tags
+========================
+
+.. image:: https://governance.openstack.org/tc/badges/tempest.svg
+ :target: https://governance.openstack.org/tc/reference/tags/index.html
+
+.. Change things from this point on
+
Tempest - The OpenStack Integration Test Suite
==============================================
+The documentation for Tempest is officially hosted at:
+https://docs.openstack.org/tempest/latest/
+
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
@@ -11,14 +23,13 @@
Tempest Design Principles that we strive to live by.
- Tempest should be able to run against any OpenStack cloud, be it a
- one node devstack install, a 20 node lxc cloud, or a 1000 node kvm
+ one node DevStack install, a 20 node LXC cloud, or a 1000 node KVM
cloud.
- Tempest should be explicit in testing features. It is easy to auto
discover features of a cloud incorrectly, and give people an
incorrect assessment of their cloud. Explicit is always better.
- Tempest uses OpenStack public interfaces. Tests in Tempest should
- only touch public interfaces, API calls (native or 3rd party),
- or libraries.
+ only touch public OpenStack APIs.
- Tempest should not touch private or implementation specific
interfaces. This means not directly going to the database, not
directly hitting the hypervisors, not testing extensions not
@@ -50,65 +61,121 @@
#. You first need to install Tempest. This is done with pip after you check out
the Tempest repo::
- $ git clone https://github.com/openstack/tempest/
+ $ git clone https://git.openstack.org/openstack/tempest
$ pip install tempest/
This can be done within a venv, but the assumption for this guide is that
- the Tempest cli entry point will be in your shell's PATH.
+ the Tempest CLI entry point will be in your shell's PATH.
-#. Installing Tempest will create a /etc/tempest dir which will contain the
- sample config file packaged with Tempest. The contents of /etc/tempest will
- be copied to all local working dirs, so if there is any common configuration
- you'd like to be shared between anyone setting up local Tempest working dirs
- it's recommended that you copy or rename tempest.conf.sample to tempest.conf
- and make those changes to that file in /etc/tempest
+#. Installing Tempest may create a ``/etc/tempest dir``, however if one isn't
+ created you can create one or use ``~/.tempest/etc`` or ``~/.config/tempest`` in
+ place of ``/etc/tempest``. If none of these dirs are created Tempest will create
+ ``~/.tempest/etc`` when it's needed. The contents of this dir will always
+ automatically be copied to all ``etc/`` dirs in local workspaces as an initial
+ setup step. So if there is any common configuration you'd like to be shared
+ between local Tempest workspaces it's recommended that you pre-populate it
+ before running ``tempest init``.
-#. Setup a local working Tempest dir. This is done using the tempest init
+#. Setup a local Tempest workspace. This is done by using the tempest init
command::
- tempest init cloud-01
+ $ tempest init cloud-01
- works the same as::
+ which also works the same as::
- mkdir cloud-01 && cd cloud-01 && tempest init
+ $ mkdir cloud-01 && cd cloud-01 && tempest init
This will create a new directory for running a single Tempest configuration.
If you'd like to run Tempest against multiple OpenStack deployments the idea
is that you'll create a new working directory for each to maintain separate
configuration files and local artifact storage for each.
-#. Then cd into the newly created working dir and also modify the local
- config files located in the etc/ subdir created by the ``tempest init``
- command. Tempest is expecting a tempest.conf file in etc/ so if only a
+#. Then ``cd`` into the newly created working dir and also modify the local
+ config files located in the ``etc/`` subdir created by the ``tempest init``
+ command. Tempest is expecting a ``tempest.conf`` file in etc/ so if only a
sample exists you must rename or copy it to tempest.conf before making
- any changes to it otherwise Tempest will not know how to load it.
+ any changes to it otherwise Tempest will not know how to load it. For
+ details on configuring Tempest refer to the :ref:`tempest-configuration`.
#. Once the configuration is done you're now ready to run Tempest. This can
- be done with testr directly or any `testr`_ based test runner, like
- `ostestr`_. For example, from the working dir running::
+ be done using the :ref:`tempest_run` command. This can be done by either
+ running::
- $ ostestr --regex '(?!.*\[.*\bslow\b.*\])(^tempest\.(api|scenario))'
+ $ tempest run
+
+ from the Tempest workspace directory. Or you can use the ``--workspace``
+ argument to run in the workspace you created regardless of your current
+ working directory. For example::
+
+ $ tempest run --workspace cloud-01
+
+ There is also the option to use testr directly, or any `testr`_ based test
+ runner, like `ostestr`_. For example, from the workspace dir run::
+
+ $ ostestr --regex '(?!.*\[.*\bslow\b.*\])(^tempest\.(api|scenario))'
will run the same set of tests as the default gate jobs.
.. _testr: https://testrepository.readthedocs.org/en/latest/MANUAL.html
-.. _ostestr: http://docs.openstack.org/developer/os-testr/
+.. _ostestr: https://docs.openstack.org/os-testr/latest/
+
+Library
+-------
+Tempest exposes a library interface. This interface is a stable interface and
+should be backwards compatible (including backwards compatibility with the
+old tempest-lib package, with the exception of the import). If you plan to
+directly consume Tempest in your project you should only import code from the
+Tempest library interface, other pieces of Tempest do not have the same
+stable interface and there are no guarantees on the Python API unless otherwise
+stated.
+
+For more details refer to the library documentation here: :ref:`library`
+
+Release Versioning
+------------------
+`Tempest Release Notes <https://docs.openstack.org/releasenotes/tempest>`_
+shows what changes have been released on each version.
+
+Tempest's released versions are broken into 2 sets of information. Depending on
+how you intend to consume Tempest you might need
+
+The version is a set of 3 numbers:
+
+X.Y.Z
+
+While this is almost `semver`_ like, the way versioning is handled is slightly
+different:
+
+X is used to represent the supported OpenStack releases for Tempest tests
+in-tree, and to signify major feature changes to Tempest. It's a monotonically
+increasing integer where each version either indicates a new supported OpenStack
+release, the drop of support for an OpenStack release (which will coincide with
+the upstream stable branch going EOL), or a major feature lands (or is removed)
+from Tempest.
+
+Y.Z is used to represent library interface changes. This is treated the same
+way as minor and patch versions from `semver`_ but only for the library
+interface. When Y is incremented we've added functionality to the library
+interface and when Z is incremented it's a bug fix release for the library.
+Also note that both Y and Z are reset to 0 at each increment of X.
+
+.. _semver: http://semver.org/
Configuration
-------------
Detailed configuration of Tempest is beyond the scope of this
document see :ref:`tempest-configuration` for more details on configuring
-Tempest. The etc/tempest.conf.sample attempts to be a self-documenting version
-of the configuration.
+Tempest. The ``etc/tempest.conf.sample`` attempts to be a self-documenting
+version of the configuration.
You can generate a new sample tempest.conf file, run the following
-command from the top level of the Tempest directory:
+command from the top level of the Tempest directory::
- tox -egenconfig
+ $ tox -e genconfig
-The most important pieces that are needed are the user ids, openstack
-endpoint, and basic flavors and images needed to run tests.
+The most important pieces that are needed are the user ids, OpenStack
+endpoints, and basic flavors and images needed to run tests.
Unit Tests
----------
@@ -116,41 +183,41 @@
Tempest also has a set of unit tests which test the Tempest code itself. These
tests can be run by specifying the test discovery path::
- $> OS_TEST_PATH=./tempest/tests testr run --parallel
+ $ stestr --test-path ./tempest/tests run
-By setting OS_TEST_PATH to ./tempest/tests it specifies that test discover
-should only be run on the unit test directory. The default value of OS_TEST_PATH
-is OS_TEST_PATH=./tempest/test_discover which will only run test discover on the
+By setting ``--test-path`` option to ./tempest/tests it specifies that test discover
+should only be run on the unit test directory. The default value of ``test_path``
+is ``test_path=./tempest/test_discover`` which will only run test discover on the
Tempest suite.
-Alternatively, you can use the run_tests.sh script which will create a venv and
-run the unit tests. There are also the py27 and py34 tox jobs which will run
-the unit tests with the corresponding version of python.
+Alternatively, there are the py27 and py35 tox jobs which will run the unit
+tests with the corresponding version of python.
-Python 2.6
+One common activity is to just run a single test, you can do this with tox
+simply by specifying to just run py27 or py35 tests against a single test::
+
+ $ tox -e py27 -- -n tempest.tests.test_microversions.TestMicroversionsTestsClass.test_config_version_none_23
+
+Or all tests in the test_microversions.py file::
+
+ $ tox -e py27 -- -n tempest.tests.test_microversions
+
+You may also use regular expressions to run any matching tests::
+
+ $ tox -e py27 -- test_microversions
+
+Additionally, when running a single test, or test-file, the ``-n/--no-discover``
+argument is no longer required, however it may perform faster if included.
+
+For more information on these options and details about stestr, please see the
+`stestr documentation <http://stestr.readthedocs.io/en/latest/MANUAL.html>`_.
+
+Python 3.x
----------
-Starting in the kilo release the OpenStack services dropped all support for
-python 2.6. This change has been mirrored in Tempest, starting after the
-tempest-2 tag. This means that proposed changes to Tempest which only fix
-python 2.6 compatibility will be rejected, and moving forward more features not
-present in python 2.6 will be used. If you're running your OpenStack services
-on an earlier release with python 2.6 you can easily run Tempest against it
-from a remote system running python 2.7. (or deploy a cloud guest in your cloud
-that has python 2.7)
-
-Python 3.4
-----------
-
-Starting during the Liberty release development cycle work began on enabling
-Tempest to run under both Python 2.7 and Python 3.4. Tempest strives to fully
-support running with Python 3.4. A gating unit test job was added to also run
-Tempest's unit tests under Python 3.4. This means that the Tempest code at
-least imports under Python 3.4 and things that have unit test coverage will
-work on Python 3.4. However, because large parts of Tempest are self-verifying
-there might be uncaught issues running on Python 3.4. So until there is a gating
-job which does a full Tempest run using Python 3.4 there isn't any guarantee
-that running Tempest under Python 3.4 is bug free.
+Starting during the Pike cycle Tempest has a gating CI job that runs Tempest
+with Python 3. Any Tempest release after 15.0.0 should fully support running
+under Python 3 as well as Python 2.7.
Legacy run method
-----------------
@@ -167,9 +234,9 @@
To start you need to create a configuration file. The easiest way to create a
configuration file is to generate a sample in the ``etc/`` directory ::
- $> cd $TEMPEST_ROOT_DIR
- $> oslo-config-generator --config-file \
- etc/config-generator.tempest.conf \
+ $ cd $TEMPEST_ROOT_DIR
+ $ oslo-config-generator --config-file \
+ tempest/cmd/config-generator.tempest.conf \
--output-file etc/tempest.conf
After that, open up the ``etc/tempest.conf`` file and edit the
@@ -179,10 +246,10 @@
.. note::
- If you have a running devstack environment, Tempest will be
+ If you have a running DevStack environment, Tempest will be
automatically configured and placed in ``/opt/stack/tempest``. It
will have a configuration file already set up to work with your
- devstack installation.
+ DevStack installation.
Tempest is not tied to any single test runner, but `testr`_ is the most commonly
used tool. Also, the nosetests test runner is **not** recommended to run Tempest.
@@ -190,21 +257,19 @@
After setting up your configuration file, you can execute the set of Tempest
tests by using ``testr`` ::
- $> testr run --parallel
+ $ testr run --parallel
To run one single test serially ::
- $> testr run tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_reboot_non_existent_server
+ $ testr run tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_reboot_non_existent_server
-Alternatively, you can use the run_tempest.sh script which will create a venv
-and run the tests or use tox to do the same. Tox also contains several existing
-job configurations. For example::
+Tox also contains several existing job configurations. For example::
- $> tox -efull
+ $ tox -e full
which will run the same set of tests as the OpenStack gate. (it's exactly how
the gate invokes Tempest) Or::
- $> tox -esmoke
+ $ tox -e smoke
to run the tests tagged as smoke.
diff --git a/REVIEWING.rst b/REVIEWING.rst
index f7334ad..a880181 100644
--- a/REVIEWING.rst
+++ b/REVIEWING.rst
@@ -2,7 +2,7 @@
======================
To start read the `OpenStack Common Review Checklist
-<https://wiki.openstack.org/wiki/ReviewChecklist#Common_Review_Checklist>`_
+<https://docs.openstack.org/infra/manual/developers.html#peer-review>`_
Ensuring code is executed
@@ -13,6 +13,22 @@
it. Tests which aren't executed either because of configuration or skips should
not be accepted.
+If a new test is added that depends on a new config option (like a feature
+flag), the commit message must reference a change in DevStack or DevStack-Gate
+that enables the execution of this newly introduced test. This reference could
+either be a `Cross-Repository Dependency <https://docs.openstack.org/infra/
+manual/developers.html#cross-repository-dependencies>`_ or a simple link
+to a Gerrit review.
+
+
+Execution time
+--------------
+While checking in the job logs that a new test is actually executed, also
+pay attention to the execution time of that test. Keep in mind that each test
+is going to be executed hundreds of time each day, because Tempest tests
+run in many OpenStack projects. It's worth considering how important/critical
+the feature under test is with how costly the new test is.
+
Unit Tests
----------
@@ -26,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.
@@ -41,6 +57,17 @@
abstract the duplicated code into a function or method.
+Tests overlap
+-------------
+When a new test is being proposed, question whether this feature is not already
+tested with Tempest. Tempest has more than 1200 tests, spread amongst many
+directories, so it's easy to introduce test duplication. For example, testing
+volume attachment to a server could be a compute test or a volume test, depending
+on how you see it. So one must look carefully in the entire code base for possible
+overlap. As a rule of thumb, the older a feature is, the more likely it's
+already tested.
+
+
Being explicit
--------------
When tests are being added that depend on a configurable feature or extension,
@@ -53,8 +80,8 @@
Configuration Options
---------------------
-With the introduction of the tempest external test plugin interface we needed
-to provide a stable contract for tempest's configuration options. This means
+With the introduction of the Tempest external test plugin interface we needed
+to provide a stable contract for Tempest's configuration options. This means
we can no longer simply remove a configuration option when it's no longer used.
Patches proposed that remove options without a deprecation cycle should not
be approved. Similarly when changing default values with configuration we need
@@ -72,11 +99,30 @@
scenario tests this is up to the reviewers discretion whether a docstring is
required or not.
+Release Notes
+-------------
+Release notes are how we indicate to users and other consumers of Tempest what
+has changed in a given release. Since Tempest 10.0.0 we've been using `reno`_
+to manage and build the release notes. There are certain types of changes that
+require release notes and we should not approve them without including a release
+note. These include but aren't limited to, any addition, deprecation or removal
+from the lib interface, any change to configuration options (including
+deprecation), CLI additions or deprecations, major feature additions, and
+anything backwards incompatible or would require a user to take note or do
+something extra.
+
+.. _reno: https://docs.openstack.org/reno/latest/
+
+Deprecated Code
+---------------
+Sometimes we have some bugs in deprecated code. Basically, we leave it. Because
+we don't need to maintain it. However, if the bug is critical, we might need to
+fix it. When it will happen, we will deal with it on a case-by-case basis.
When to approve
---------------
- * Every patch needs two +2s before being approved.
- * Its ok to hold off on an approval until a subject matter expert reviews it
- * If a patch has already been approved but requires a trivial rebase to merge,
- you do not have to wait for a second +2, since the patch has already had
- two +2s.
+* Every patch needs two +2s before being approved.
+* Its ok to hold off on an approval until a subject matter expert reviews it
+* If a patch has already been approved but requires a trivial rebase to merge,
+ you do not have to wait for a second +2, since the patch has already had
+ two +2s.
diff --git a/bindep.txt b/bindep.txt
new file mode 100644
index 0000000..efd3a10
--- /dev/null
+++ b/bindep.txt
@@ -0,0 +1,13 @@
+# This file contains runtime (non-python) dependencies
+# More info at: https://docs.openstack.org/infra/bindep/readme.html
+
+libffi-dev [platform:dpkg]
+libffi-devel [platform:rpm]
+gcc [platform:rpm]
+gcc [platform:dpkg]
+python-dev [platform:dpkg]
+python-devel [platform:rpm]
+python3-dev [platform:dpkg]
+python3-devel [platform:rpm]
+openssl-devel [platform:rpm]
+libssl-dev [platform:dpkg]
diff --git a/doc/requirements.txt b/doc/requirements.txt
new file mode 100644
index 0000000..d959d44
--- /dev/null
+++ b/doc/requirements.txt
@@ -0,0 +1,6 @@
+# The order of packages is significant, because pip processes them in the order
+# of appearance. Changing the order has an impact on the overall integration
+# process, which may cause wedges in the gate later.
+openstackdocstheme>=1.18.1 # Apache-2.0
+reno>=2.5.0 # Apache-2.0
+sphinx!=1.6.6,!=1.6.7,>=1.6.2 # BSD
diff --git a/doc/source/_extra/.htaccess b/doc/source/_extra/.htaccess
new file mode 100644
index 0000000..7745594
--- /dev/null
+++ b/doc/source/_extra/.htaccess
@@ -0,0 +1 @@
+redirectmatch 301 ^/developer/tempest/(.*) /tempest/latest/$1
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 12d1d40..c2ea628 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -1,5 +1,16 @@
-# -*- coding: utf-8 -*-
+# 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.
+
# Tempest documentation build configuration file, created by
# sphinx-quickstart on Tue May 21 17:43:32 2013.
#
@@ -11,10 +22,21 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
-import sys
import os
import subprocess
+# Build the plugin registry
+def build_plugin_registry(app):
+ root_dir = os.path.dirname(
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+ subprocess.call(['tools/generate-tempest-plugins-list.sh'], cwd=root_dir)
+
+def setup(app):
+ if os.getenv('GENERATE_TEMPEST_PLUGIN_LIST', 'true').lower() == 'true':
+ app.connect('builder-inited', build_plugin_registry)
+
+
+
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
@@ -30,15 +52,23 @@
extensions = ['sphinx.ext.autodoc',
'sphinx.ext.todo',
'sphinx.ext.viewcode',
- 'oslosphinx',
+ 'openstackdocstheme',
'oslo_config.sphinxconfiggen',
]
-config_generator_config_file = '../../etc/config-generator.tempest.conf'
+config_generator_config_file = '../../tempest/cmd/config-generator.tempest.conf'
sample_config_basename = '_static/tempest'
todo_include_todos = True
+# openstackdocstheme options
+repository_name = 'openstack/tempest'
+bug_project = 'tempest'
+bug_tag = 'doc'
+
+# Must set this variable to include year, month, day, hours, and minutes.
+html_last_updated_fmt = '%Y-%m-%d %H:%M'
+
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@@ -94,7 +124,7 @@
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
-html_theme = 'nature'
+html_theme = 'openstackdocs'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
@@ -124,15 +154,12 @@
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
+# Add any paths that contain "extra" files, such as .htaccess or
+# robots.txt.
+html_extra_path = ['_extra']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
-git_cmd = "git log --pretty=format:'%ad, commit %h' --date=local -n1"
-html_last_updated_fmt = os.popen(git_cmd).read()
-
-# If true, SmartyPants will be used to convert quotes and dashes to
-# typographically correct entities.
-#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
@@ -167,123 +194,5 @@
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
-# Output file base name for HTML help builder.
-htmlhelp_basename = 'Tempestdoc'
-
-
-# -- Options for LaTeX output --------------------------------------------------
-
-latex_elements = {
-# The paper size ('letterpaper' or 'a4paper').
-#'papersize': 'letterpaper',
-
-# The font size ('10pt', '11pt' or '12pt').
-#'pointsize': '10pt',
-
-# Additional stuff for the LaTeX preamble.
-#'preamble': '',
-}
-
-# Grouping the document tree into LaTeX files. List of tuples
-# (source start file, target name, title, author, documentclass [howto/manual]).
-latex_documents = [
- ('index', 'Tempest.tex', u'Tempest Documentation',
- u'OpenStack QA Team', 'manual'),
-]
-
-# The name of an image file (relative to this directory) to place at the top of
-# the title page.
-#latex_logo = None
-
-# For "manual" documents, if this is true, then toplevel headings are parts,
-# not chapters.
-#latex_use_parts = False
-
-# If true, show page references after internal links.
-#latex_show_pagerefs = False
-
-# If true, show URL addresses after external links.
-#latex_show_urls = False
-
-# Documents to append as an appendix to all manuals.
-#latex_appendices = []
-
-# If false, no module index is generated.
-#latex_domain_indices = True
-
-
-# -- Options for manual page output --------------------------------------------
-
-# One entry per manual page. List of tuples
-# (source start file, name, description, authors, manual section).
-man_pages = [
- ('index', 'tempest', u'Tempest Documentation',
- [u'OpenStack QA Team'], 1)
-]
-
-# If true, show URL addresses after external links.
-#man_show_urls = False
-
-
-# -- Options for Texinfo output ------------------------------------------------
-
-# Grouping the document tree into Texinfo files. List of tuples
-# (source start file, target name, title, author,
-# dir menu entry, description, category)
-texinfo_documents = [
- ('index', 'Tempest', u'Tempest Documentation',
- u'OpenStack QA Team', 'Tempest', 'One line description of project.',
- 'Miscellaneous'),
-]
-
-# Documents to append as an appendix to all manuals.
-#texinfo_appendices = []
-
-# If false, no module index is generated.
-#texinfo_domain_indices = True
-
-# How to display URL addresses: 'footnote', 'no', or 'inline'.
-#texinfo_show_urls = 'footnote'
-
-
-# -- Options for Epub output ---------------------------------------------------
-
-# Bibliographic Dublin Core info.
-epub_title = u'Tempest'
-epub_author = u'Sean Dague'
-epub_publisher = u'OpenStack QA Team'
-epub_copyright = u'2013, OpenStack QA Team'
-
-# The language of the text. It defaults to the language option
-# or en if the language is not set.
-#epub_language = ''
-
-# The scheme of the identifier. Typical schemes are ISBN or URL.
-#epub_scheme = ''
-
-# The unique identifier of the text. This can be a ISBN number
-# or the project homepage.
-#epub_identifier = ''
-
-# A unique identification for the text.
-#epub_uid = ''
-
-# A tuple containing the cover image and cover page html template filenames.
-#epub_cover = ()
-
-# HTML files that should be inserted before the pages created by sphinx.
-# The format is a list of tuples containing the path and title.
-#epub_pre_files = []
-
-# HTML files shat should be inserted after the pages created by sphinx.
-# The format is a list of tuples containing the path and title.
-#epub_post_files = []
-
-# A list of files that should not be packed into the epub file.
-#epub_exclude_files = []
-
-# The depth of the table of contents in toc.ncx.
-#epub_tocdepth = 3
-
-# Allow duplicate toc entries.
-#epub_tocdup = True
+# A list of warning types to suppress arbitrary warning messages.
+suppress_warnings = ['image.nonlocal_uri']
diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst
index 7f1e292..d0d7320 100644
--- a/doc/source/configuration.rst
+++ b/doc/source/configuration.rst
@@ -3,92 +3,108 @@
Tempest Configuration Guide
===========================
-This guide is a starting point for configuring tempest. It aims to elaborate
+This guide is a starting point for configuring Tempest. It aims to elaborate
on and explain some of the mandatory and common configuration settings and how
they are used in conjunction. The source of truth on each option is the sample
config file which explains the purpose of each individual option. You can see
the sample config file here: :ref:`tempest-sampleconf`
-Auth/Credentials
+.. _tempest_cred_provider_conf:
+
+Test Credentials
----------------
-Tempest currently has 2 different ways in configuration to provide credentials
-to use when running tempest. One is a traditional set of configuration options
-in the tempest.conf file. These options are in the auth and identity sections
-and let you specify a global admin user, a regular user and an alternate user
-set of credentials. (which consist of a username, password, and project/tenant
-name) These options should be clearly labelled in the sample config file in the
-auth and identity sections.
+Tempest allows for configuring a set of admin credentials in the ``auth``
+section, via the following parameters:
-The other method to provide credentials is using the accounts.yaml file. This
-file is used to specify an arbitrary number of users available to run tests
-with. You can specify the location of the file in the
-auth section in the tempest.conf file. To see the specific format used in
-the file please refer to the accounts.yaml.sample file included in tempest.
-Currently users that are specified in the accounts.yaml file are assumed to
-have the same set of roles which can be used for executing all the tests you
-are running. This will be addressed in the future, but is a current limitation.
-Eventually the config options for providing credentials to tempest will be
-deprecated and removed in favor of the accounts.yaml file.
+#. ``admin_username``
+#. ``admin_password``
+#. ``admin_project_name``
+#. ``admin_domain_name``
+
+Admin credentials are not mandatory to run Tempest, but when provided they
+can be used to:
+
+- Run tests for admin APIs
+- Generate test credentials on the fly (see `Dynamic Credentials`_)
+
+When Keystone uses a policy that requires domain scoped tokens for admin
+actions, the flag ``admin_domain_scope`` must be set to ``True``.
+The admin user configured, if any, must have a role assigned to the domain to
+be usable.
+
+Tempest allows for configuring pre-provisioned test credentials as well.
+This can be done using the accounts.yaml file (see
+`Pre-Provisioned Credentials`_). This file is used to specify an arbitrary
+number of users available to run tests with.
+You can specify the location of the file in the ``auth`` section in the
+tempest.conf file. To see the specific format used in the file please refer to
+the ``accounts.yaml.sample`` file included in Tempest.
Keystone Connection Info
^^^^^^^^^^^^^^^^^^^^^^^^
-In order for tempest to be able to talk to your OpenStack deployment you need
+In order for Tempest to be able to talk to your OpenStack deployment you need
to provide it with information about how it communicates with keystone.
-This involves configuring the following options in the identity section:
+This involves configuring the following options in the ``identity`` section:
- #. auth_version
- #. uri
- #. uri_v3
+- ``auth_version``
+- ``uri``
+- ``uri_v3``
-The *auth_version* option is used to tell tempest whether it should be using
-keystone's v2 or v3 api for communicating with keystone. (except for the
-identity api tests which will test a specific version) The 2 uri options are
-used to tell tempest the url of the keystone endpoint. The *uri* option is used
-for keystone v2 request and *uri_v3* is used for keystone v3. You want to ensure
-that which ever version you set for *auth_version* has its uri option defined.
-
+The ``auth_version`` option is used to tell Tempest whether it should be using
+Keystone's v2 or v3 api for communicating with Keystone. The two uri options are
+used to tell Tempest the url of the keystone endpoint. The ``uri`` option is
+used for Keystone v2 request and ``uri_v3`` is used for Keystone v3. You want to
+ensure that which ever version you set for ``auth_version`` has its uri option
+defined.
Credential Provider Mechanisms
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-Tempest currently also has 3 different internal methods for providing
-authentication to tests. Dynamic credentials, locking test accounts, and
-non-locking test accounts. Depending on which one is in use the configuration
-of tempest is slightly different.
+Tempest currently has two different internal methods for providing authentication
+to tests: dynamic credentials and pre-provisioned credentials.
+Depending on which one is in use the configuration of Tempest is slightly different.
Dynamic Credentials
"""""""""""""""""""
Dynamic Credentials (formerly known as Tenant isolation) was originally created
-to enable running tempest in parallel.
-For each test class it creates a unique set of user credentials to use for the
-tests in the class. It can create up to 3 sets of username, password, and
-tenant/project names for a primary user, an admin user, and an alternate user.
-To enable and use dynamic credentials you only need to configure 2 things:
+to enable running Tempest in parallel. For each test class it creates a unique
+set of user credentials to use for the tests in the class. It can create up to
+three sets of username, password, and project names for a primary user,
+an admin user, and an alternate user. To enable and use dynamic credentials you
+only need to configure two things:
- #. A set of admin credentials with permissions to create users and
- tenants/projects. This is specified in the auth section with the
- admin_username, admin_tenant_name, admin_domain_name and admin_password
- options
- #. To enable dynamic_creds in the auth section with the
- use_dynamic_credentials option.
+#. A set of admin credentials with permissions to create users and
+ projects. This is specified in the ``auth`` section with the
+ ``admin_username``, ``admin_project_name``, ``admin_domain_name`` and
+ ``admin_password`` options
+#. To enable dynamic credentials in the ``auth`` section with the
+ ``use_dynamic_credentials`` option.
-This is also the currently the default credential provider enabled by tempest,
-due to it's common use and ease of configuration.
+This is also currently the default credential provider enabled by Tempest, due
+to its common use and ease of configuration.
It is worth pointing out that depending on your cloud configuration you might
need to assign a role to each of the users created by Tempest's dynamic
-credentials.
-This can be set using the *tempest_roles* option. It takes in a list of role
-names each of which will be assigned to each of the users created by dynamic
-credentials. This option will not have any effect when set and tempest is not
+credentials. This can be set using the ``tempest_roles`` option. It takes in a
+list of role names each of which will be assigned to each of the users created
+by dynamic credentials. This option will not have any effect when Tempest is not
configured to use dynamic credentials.
+When the ``admin_domain_scope`` option is set to ``True``, provisioned admin
+accounts will be assigned a role on domain configured in
+``default_credentials_domain_name``. This will make the accounts provisioned
+usable in a cloud where domain scoped tokens are required by Keystone for
+admin operations. Note that the initial pre-provision admin accounts,
+configured in tempest.conf, must have a role on the same domain as well, for
+Dynamic Credentials to work.
-Locking Test Accounts (aka accounts.yaml or accounts file)
-""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
+
+Pre-Provisioned Credentials
+"""""""""""""""""""""""""""
+
For a long time using dynamic credentials was the only method available if you
-wanted to enable parallel execution of tempest tests. However this was
+wanted to enable parallel execution of Tempest tests. However, this was
insufficient for certain use cases because of the admin credentials requirement
to create the credential sets on demand. To get around that the accounts.yaml
file was introduced and with that a new internal credential provider to enable
@@ -99,125 +115,107 @@
To enable and use locking test accounts you need do a few things:
- #. Create a accounts.yaml file which contains the set of pre-existing
- credentials to use for testing. To make sure you don't have a credentials
- starvation issue when running in parallel make sure you have at least 2
- times the number of worker processes you are using to execute tempest
- available in the file. (if running serially the worker count is 1)
+#. Create an accounts.yaml file which contains the set of pre-existing
+ credentials to use for testing. To make sure you don't have a credentials
+ starvation issue when running in parallel make sure you have at least two
+ times the number of worker processes you are using to execute Tempest
+ available in the file. (If running serially the worker count is 1.)
- You can check the sample file packaged in tempest for the yaml format
- #. Provide tempest with the location of your accounts.yaml file with the
- test_accounts_file option in the auth section
+ You can check the accounts.yaml.sample file packaged in Tempest for the yaml
+ format.
+#. Provide Tempest with the location of your accounts.yaml file with the
+ ``test_accounts_file`` option in the ``auth`` section
- *NOTE: Be sure to use a full path for the file otherwise Tempest will
- likely not find it.*
+ *NOTE: Be sure to use a full path for the file; otherwise Tempest will
+ likely not find it.*
- #. Set use_dynamic_credentials = False in the auth group
+#. Set ``use_dynamic_credentials = False`` in the ``auth`` group
It is worth pointing out that each set of credentials in the accounts.yaml
-should have a unique tenant. This is required to provide proper isolation
+should have a unique project. This is required to provide proper isolation
to the tests using the credentials, and failure to do this will likely cause
-unexpected failures in some tests.
+unexpected failures in some tests. Also, ensure that these projects and users
+used do not have any pre-existing resources created. Tempest assumes all
+tenants it's using are empty and may sporadically fail if there are unexpected
+resources present.
+When the Keystone in the target cloud requires domain scoped tokens to
+perform admin actions, all pre-provisioned admin users must have a role
+assigned on the domain where test accounts a provisioned.
+The option ``admin_domain_scope`` is used to tell Tempest that domain scoped
+tokens shall be used. ``default_credentials_domain_name`` is the domain where
+test accounts are expected to be provisioned if no domain is specified.
-Legacy test accounts (aka credentials config options)
-"""""""""""""""""""""""""""""""""""""""""""""""""""""
-**Starting in the Liberty release this mechanism was deprecated and will be
-removed in a future release**
+Note that if credentials are pre-provisioned via ``tempest account-generator``
+the role on the domain will be assigned automatically for you, as long as
+``admin_domain_scope`` as ``default_credentials_domain_name`` are configured
+properly in tempest.conf.
-When Tempest was refactored to allow for locking test accounts, the original
-non-tenant isolated case was converted to internally work similarly to the
-accounts.yaml file. This mechanism was then called the legacy test accounts
-provider. To use the legacy test accounts provider you can specify the sets of
-credentials in the configuration file like detailed above with following 6
-options in the identity section:
-
- #. username
- #. password
- #. tenant_name
- #. alt_username
- #. alt_password
- #. alt_tenant_name
-
-And in the auth section:
-
- #. admin_username
- #. admin_password
- #. admin_tenant_name
- #. use_dynamic_credentials = False
- #. comment out 'test_accounts_file' or keep it as empty
-
-It only makes sense to use it if parallel execution isn't needed, since tempest
-won't be able to properly isolate tests using this. Additionally, using the
-traditional config options for credentials is not able to provide credentials to
-tests which requires specific roles on accounts. This is because the config
-options do not give sufficient flexibility to describe the roles assigned to a
-user for running the tests. There are additional limitations with regard to
-network configuration when using this credential provider mechanism, see the
-`Networking`_ section below.
+Pre-Provisioned Credentials are also known as accounts.yaml or accounts file.
Compute
-------
Flavors
^^^^^^^
-For tempest to be able to create servers you need to specify flavors that it
-can use to boot the servers with. There are 2 options in the tempest config
+For Tempest to be able to create servers you need to specify flavors that it
+can use to boot the servers with. There are two options in the Tempest config
for doing this:
- #. flavor_ref
- #. flavor_ref_alt
+#. ``flavor_ref``
+#. ``flavor_ref_alt``
-Both of these options are in the compute section of the config file and take
-in the flavor id (not the name) from nova. The *flavor_ref* option is what will
-be used for booting almost all of the guests, *flavor_ref_alt* is only used in
-tests where 2 different sized servers are required. (for example a resize test)
+Both of these options are in the ``compute`` section of the config file and take
+in the flavor id (not the name) from Nova. The ``flavor_ref`` option is what
+will be used for booting almost all of the guests; ``flavor_ref_alt`` is only
+used in tests where two different-sized servers are required (for example, a
+resize test).
-Using a smaller flavor is generally recommended, when larger flavors are used
+Using a smaller flavor is generally recommended. When larger flavors are used,
the extra time required to bring up servers will likely affect total run time
and probably require tweaking timeout values to ensure tests have ample time to
finish.
Images
^^^^^^
-Just like with flavors, tempest needs to know which images to use for booting
-servers. There are 2 options in the compute section just like with flavors:
+Just like with flavors, Tempest needs to know which images to use for booting
+servers. There are two options in the compute section just like with flavors:
- #. image_ref
- #. image_ref_alt
+#. ``image_ref``
+#. ``image_ref_alt``
-Both options are expecting an image id (not name) from nova. The *image_ref*
-option is what will be used for booting the majority of servers in tempest.
-*image_ref_alt* is used for tests that require 2 images such as rebuild. If 2
-images are not available you can set both options to the same image_ref and
+Both options are expecting an image id (not name) from Nova. The ``image_ref``
+option is what will be used for booting the majority of servers in Tempest.
+``image_ref_alt`` is used for tests that require two images such as rebuild. If
+two images are not available you can set both options to the same image id and
those tests will be skipped.
-There are also options in the scenario section for images:
+There are also options in the ``scenario`` section for images:
- #. img_file
- #. img_dir
- #. aki_img_file
- #. ari_img_file
- #. ami_img_file
- #. img_container_format
- #. img_disk_format
+#. ``img_file``
+#. ``img_dir``
+#. ``aki_img_file``
+#. ``ari_img_file``
+#. ``ami_img_file``
+#. ``img_container_format``
+#. ``img_disk_format``
-however unlike the other image options these are used for a very small subset
+However, unlike the other image options, these are used for a very small subset
of scenario tests which are uploading an image. These options are used to tell
-tempest where an image file is located and describe it's metadata for when it's
+Tempest where an image file is located and describe its metadata for when it is
uploaded.
-The behavior of these options is a bit convoluted (which will likely be fixed
-in future versions). You first need to specify *img_dir*, which is the directory
-tempest will look for the image files in. First it will check if the filename
-set for *img_file* could be found in *img_dir*. If it is found then the
-*img_container_format* and *img_disk_format* options are used to upload that
-image to glance. However if it's not found tempest will look for the 3 uec image
-file name options as a fallback. If neither is found the tests requiring an
-image to upload will fail.
+The behavior of these options is a bit convoluted (which will likely be fixed in
+future versions). You first need to specify ``img_dir``, which is the directory
+in which Tempest will look for the image files. First it will check if the
+filename set for ``img_file`` could be found in ``img_dir``. If it is found then
+the ``img_container_format`` and ``img_disk_format`` options are used to upload
+that image to glance. However, if it is not found, Tempest will look for the
+three uec image file name options as a fallback. If neither is found, the tests
+requiring an image to upload will fail.
It is worth pointing out that using `cirros`_ is a very good choice for running
-tempest. It's what is used for upstream testing, they boot quickly and have a
+Tempest. It's what is used for upstream testing, they boot quickly and have a
small footprint.
.. _cirros: https://launchpad.net/cirros
@@ -225,40 +223,45 @@
Networking
----------
OpenStack has a myriad of different networking configurations possible and
-depending on which of the 2 network backends, nova-network or neutron, you are
+depending on which of the two network backends, nova-network or Neutron, you are
using things can vary drastically. Due to this complexity Tempest has to provide
-a certain level of flexibility in it's configuration to ensure it will work
+a certain level of flexibility in its configuration to ensure it will work
against any cloud. This ends up causing a large number of permutations in
Tempest's config around network configuration.
Enabling Remote Access to Created Servers
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. _tempest_conf_network_allocation:
+
+Network Creation/Usage for Servers
+""""""""""""""""""""""""""""""""""
When Tempest creates servers for testing, some tests require being able to
connect those servers. Depending on the configuration of the cloud, the methods
for doing this can be different. In certain configurations it is required to
specify a single network with server create calls. Accordingly, Tempest provides
a few different methods for providing this information in configuration to try
-and ensure that regardless of the clouds configuration it'll still be able to
+and ensure that regardless of the cloud's configuration it'll still be able to
run. This section covers the different methods of configuring Tempest to provide
a network when creating servers.
Fixed Network Name
-""""""""""""""""""
+''''''''''''''''''
This is the simplest method of specifying how networks should be used. You can
just specify a single network name/label to use for all server creations. The
-limitation with this is that all tenants/projects and users must be able to see
-that network name/label if they were to perform a network list and be able to
-use it.
+limitation with this is that all projects and users must be able to see
+that network name/label if they are to perform a network list and be able to use
+it.
If no network name is assigned in the config file and none of the below
alternatives are used, then Tempest will not specify a network on server
creations, which depending on the cloud configuration might prevent them from
booting.
-To set a fixed network name simply do:
+To set a fixed network name simply:
- #. Set the fixed_network_name option in the compute group
+#. Set the ``fixed_network_name`` option in the ``compute`` group
In the case that the configured fixed network name can not be found by a user
network list call, it will be treated like one was not provided except that a
@@ -266,14 +269,14 @@
Accounts File
-"""""""""""""
+'''''''''''''
If you are using an accounts file to provide credentials for running Tempest
then you can leverage it to also specify which network should be used with
-server creations on a per tenant/project and user pair basis. This provides
+server creations on a per project and user pair basis. This provides
the necessary flexibility to work with more intricate networking configurations
by enabling the user to specify exactly which network to use for which
-tenants/projects. You can refer to the accounts.yaml sample file included in
-the tempest repo for the syntax around specifying networks in the file.
+projects. You can refer to the accounts.yaml.sample file included in
+the Tempest repo for the syntax around specifying networks in the file.
However, specifying a network is not required when using an accounts file. If
one is not specified you can use a fixed network name to specify the network to
@@ -291,30 +294,58 @@
With Dynamic Credentials
-""""""""""""""""""""""""
-With dynamic credentials enabled and using nova-network then nothing changes.
-Your only option for configuration is to either set a fixed network name or not.
-However, in most cases it shouldn't matter because nova-network should have no
-problem booting a server with multiple networks. If this is not the case for
-your cloud then using an accounts file is recommended because it provides the
-necessary flexibility to describe your configuration. Dynamic credentials is not
-able to dynamically allocate things as necessary if neutron is not enabled.
+''''''''''''''''''''''''
+With dynamic credentials enabled and using nova-network, your only option for
+configuration is to either set a fixed network name or not. However, in most
+cases it shouldn't matter because nova-network should have no problem booting a
+server with multiple networks. If this is not the case for your cloud then using
+an accounts file is recommended because it provides the necessary flexibility to
+describe your configuration. Dynamic credentials is not able to dynamically
+allocate things as necessary if Neutron is not enabled.
-With neutron and dynamic credentials enabled there should not be any additional
+With Neutron and dynamic credentials enabled there should not be any additional
configuration necessary to enable Tempest to create servers with working
-networking, assuming you have properly configured the network section to work
-for your cloud. Tempest will dynamically create the neutron resources necessary
-to enable using servers with that network. Also, just as with the accounts
-file, if you specify a fixed network name while using neutron and dynamic
-credentials it will enable running tests which require a static network and it
-will additionally be used as a fallback for server creation. However, unlike
-accounts.yaml this should never be triggered.
+networking, assuming you have properly configured the ``network`` section to
+work for your cloud. Tempest will dynamically create the Neutron resources
+necessary to enable using servers with that network. Also, just as with the
+accounts file, if you specify a fixed network name while using Neutron and
+dynamic credentials it will enable running tests which require a static network
+and it will additionally be used as a fallback for server creation. However,
+unlike accounts.yaml this should never be triggered.
-However, there is an option *create_isolated_networks* to disable dynamic
-credentials's automatic provisioning of network resources. If this option is
-used you will have to either rely on there only being a single/default network
-available for the server creation, or use *fixed_network_name* to inform
-Tempest which network to use.
+However, there is an option ``create_isolated_networks`` to disable dynamic
+credentials's automatic provisioning of network resources. If this option is set
+to ``False`` you will have to either rely on there only being a single/default
+network available for the server creation, or use ``fixed_network_name`` to
+inform Tempest which network to use.
+
+SSH Connection Configuration
+""""""""""""""""""""""""""""
+There are also several different ways to actually establish a connection and
+authenticate/login on the server. After a server is booted with a provided
+network there are still details needed to know how to actually connect to
+the server. The ``validation`` group gathers all the options regarding
+connecting to and remotely accessing the created servers.
+
+To enable remote access to servers, there are 3 options at a minimum that are used:
+
+#. ``run_validation``
+#. ``connect_method``
+#. ``auth_method``
+
+The ``run_validation`` is used to enable or disable ssh connectivity for
+all tests (with the exception of scenario tests which do not have a flag for
+enabling or disabling ssh) To enable ssh connectivity this needs be set to ``True``.
+
+The ``connect_method`` option is used to tell Tempest what kind of IP to use for
+establishing a connection to the server. Two methods are available: ``fixed``
+and ``floating``, the later being set by default. If this is set to floating
+Tempest will create a floating ip for the server before attempted to connect
+to it. The IP for the floating ip is what is used for the connection.
+
+For the ``auth_method`` option there is currently, only one valid option,
+``keypair``. With this set to ``keypair`` Tempest will create an ssh keypair
+and use that for authenticating against the created server.
Configuring Available Services
------------------------------
@@ -325,67 +356,71 @@
out which tests it is able to run and certain setup steps which differ based
on the available services.
-The *service_available* section of the config file is used to set which
+The ``service_available`` section of the config file is used to set which
services are available. It contains a boolean option for each service (except
-for keystone which is a hard requirement) set it to True if the service is
-available or False if it is not.
+for Keystone which is a hard requirement) set it to ``True`` if the service is
+available or ``False`` if it is not.
Service Catalog
^^^^^^^^^^^^^^^
Each project which has its own REST API contains an entry in the service
catalog. Like most things in OpenStack this is also completely configurable.
-However, for tempest to be able to figure out the endpoints to send REST API
-calls for each service to it needs to know how that project is defined in the
-service catalog. There are 3 options for each service section to accomplish
+However, for Tempest to be able to figure out which endpoints should get REST
+API calls for each service, it needs to know how that project is defined in the
+service catalog. There are three options for each service section to accomplish
this:
- #. catalog_type
- #. endpoint_type
- #. region
+#. ``catalog_type``
+#. ``endpoint_type``
+#. ``region``
-Setting *catalog_type* and *endpoint_type* should normally give Tempest enough
-information to determine which endpoint it should pull from the service
-catalog to use for talking to that particular service. However, if you're cloud
-has multiple regions available and you need to specify a particular one to use
-a service you can set the *region* option in that service's section.
+Setting ``catalog_type`` and ``endpoint_type`` should normally give Tempest
+enough information to determine which endpoint it should pull from the service
+catalog to use for talking to that particular service. However, if your cloud
+has multiple regions available and you need to specify a particular one to use a
+service you can set the ``region`` option in that service's section.
It should also be noted that the default values for these options are set
-to what devstack uses. (which is a de facto standard for service catalog
-entries) So often nothing actually needs to be set on these options to enable
+to what DevStack uses (which is a de facto standard for service catalog
+entries). So often nothing actually needs to be set on these options to enable
communication to a particular service. It is only if you are either not using
-the same *catalog_type* as devstack or you want Tempest to talk to a different
-endpoint type instead of publicURL for a service that these need to be changed.
+the same ``catalog_type`` as DevStack or you want Tempest to talk to a different
+endpoint type instead of ``publicURL`` for a service that these need to be
+changed.
.. note::
- Tempest does not serve all kind of fancy URLs in the service catalog.
- Service catalog should be in a standard format (which is going to be
- standardized at keystone level).
- Tempest expects URLs in the Service catalog in below format:
- * http://example.com:1234/<version-info>
- Examples:
- * Good - http://example.com:1234/v2.0
- * Wouldn’t work - http://example.com:1234/xyz/v2.0/
- (adding prefix/suffix around version etc)
+ Tempest does not serve all kinds of fancy URLs in the service catalog. The
+ service catalog should be in a standard format (which is going to be
+ standardized at the Keystone level).
+ Tempest expects URLs in the Service catalog in the following format:
-Service feature configuration
+ * ``http://example.com:1234/<version-info>``
+
+ Examples:
+
+ * Good - ``http://example.com:1234/v2.0``
+ * Wouldn't work - ``http://example.com:1234/xyz/v2.0/``
+ (adding prefix/suffix around version etc)
+
+Service Feature Configuration
-----------------------------
-OpenStack provides its deployers a myriad of different configuration options
-to enable anyone deploying it to create a cloud tailor-made for any individual
-use case. It provides options for several different backend type, databases,
+OpenStack provides its deployers a myriad of different configuration options to
+enable anyone deploying it to create a cloud tailor-made for any individual use
+case. It provides options for several different backend types, databases,
message queues, etc. However, the downside to this configurability is that
certain operations and features aren't supported depending on the configuration.
These features may or may not be discoverable from the API so the burden is
-often on the user to figure out what the cloud they're talking to supports.
-Besides the obvious interoperability issues with this it also leaves Tempest
-in an interesting situation trying to figure out which tests are expected to
-work. However, Tempest tests do not rely on dynamic api discovery for a feature
-(assuming one exists). Instead Tempest has to be explicitly configured as to
-which optional features are enabled. This is in order to prevent bugs in the
-discovery mechanisms from masking failures.
+often on the user to figure out what is supported by the cloud they're talking
+to. Besides the obvious interoperability issues with this it also leaves
+Tempest in an interesting situation trying to figure out which tests are
+expected to work. However, Tempest tests do not rely on dynamic API discovery
+for a feature (assuming one exists). Instead Tempest has to be explicitly
+configured as to which optional features are enabled. This is in order to
+prevent bugs in the discovery mechanisms from masking failures.
-The service feature-enabled config sections are how Tempest addresses the
+The service ``feature-enabled`` config sections are how Tempest addresses the
optional feature question. Each service that has tests for optional features
contains one of these sections. The only options in it are boolean options
with the name of a feature which is used. If it is set to false any test which
@@ -395,10 +430,10 @@
API Extensions
^^^^^^^^^^^^^^
-The service feature-enabled sections often contain an *api-extensions* option
-(or in the case of swift a *discoverable_apis* option) this is used to tell
-tempest which api extensions (or configurable middleware) is used in your
-deployment. It has 2 valid config states, either it contains a single value
-"all" (which is the default) which means that every api extension is assumed
+The service feature-enabled sections often contain an ``api-extensions`` option
+(or in the case of Swift a ``discoverable_apis`` option). This is used to tell
+Tempest which api extensions (or configurable middleware) is used in your
+deployment. It has two valid config states: either it contains a single value
+``all`` (which is the default) which means that every api extension is assumed
to be enabled, or it is set to a list of each individual extension that is
enabled for that service.
diff --git a/doc/source/data/tempest-plugins-registry.header b/doc/source/data/tempest-plugins-registry.header
new file mode 100644
index 0000000..9821e8e
--- /dev/null
+++ b/doc/source/data/tempest-plugins-registry.header
@@ -0,0 +1,23 @@
+..
+ Note to patch submitters: this file is covered by a periodic proposal
+ job. You should edit the files data/tempest-plugins-registry.footer
+ and data/tempest-plugins-registry.header instead of this one.
+
+==========================
+ Tempest Plugin Registry
+==========================
+
+Since we've created the external plugin mechanism, it's gotten used by
+a lot of projects. The following is a list of plugins that currently
+exist.
+
+Detected Plugins
+================
+
+The following are plugins that a script has found in the openstack/
+namespace, which includes but is not limited to official OpenStack
+projects.
+
++----------------------------+-------------------------------------------------------------------------+
+|Plugin Name |URL |
++----------------------------+-------------------------------------------------------------------------+
diff --git a/doc/source/field_guide/stress.rst b/doc/source/field_guide/stress.rst
deleted file mode 120000
index d39d0f8..0000000
--- a/doc/source/field_guide/stress.rst
+++ /dev/null
@@ -1 +0,0 @@
-../../../tempest/stress/README.rst
\ No newline at end of file
diff --git a/doc/source/index.rst b/doc/source/index.rst
index 32e6e51..fecf98a 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -2,19 +2,16 @@
Tempest Testing Project
=======================
-Contents:
+Overview
+========
.. toctree::
:maxdepth: 2
overview
- HACKING
- REVIEWING
- plugin
-------------
Field Guides
-------------
+============
Tempest contains tests of many different types, the field guides
attempt to explain these in a way that makes it easy to understand
where your test contributions should go.
@@ -25,10 +22,11 @@
field_guide/index
field_guide/api
field_guide/scenario
- field_guide/stress
field_guide/unit_tests
----------------------------
+Users Guide
+===========
+
Tempest Configuration Guide
---------------------------
@@ -38,7 +36,6 @@
configuration
sampleconf
----------------------
Command Documentation
---------------------
@@ -47,9 +44,50 @@
account_generator
cleanup
- javelin
+ subunit_describe_calls
+ workspace
+ run
-==================
+Developers Guide
+================
+
+Development
+-----------
+
+.. toctree::
+ :maxdepth: 2
+
+ HACKING
+ REVIEWING
+ microversion_testing
+ test_removal
+ write_tests
+
+Plugins
+-------
+
+.. toctree::
+ :maxdepth: 2
+
+ plugin
+ plugin-registry
+
+Library
+-------
+
+.. toctree::
+ :maxdepth: 2
+
+ library
+
+Support Policy
+--------------
+
+.. toctree::
+ :maxdepth: 2
+
+ stable_branch_support_policy
+
Indices and tables
==================
diff --git a/doc/source/javelin.rst b/doc/source/javelin.rst
deleted file mode 100644
index 01090ca..0000000
--- a/doc/source/javelin.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-----------------------------------------------------------
-Javelin2 - How to check that resources survived an upgrade
-----------------------------------------------------------
-
-.. automodule:: tempest.cmd.javelin
diff --git a/doc/source/library.rst b/doc/source/library.rst
new file mode 100644
index 0000000..14415ae
--- /dev/null
+++ b/doc/source/library.rst
@@ -0,0 +1,72 @@
+.. _library:
+
+Tempest Library Documentation
+=============================
+
+Tempest provides a stable library interface that provides external tools or
+test suites an interface for reusing pieces of tempest code. Any public
+interface that lives in tempest/lib in the tempest repo is treated as a stable
+public interface and it should be safe to external consume that. Every effort
+goes into maintaining backwards compatibility with any change.
+The library is self contained and doesn't have any dependency on
+other tempest internals outside of lib (including no usage of tempest
+configuration).
+
+Stability
+---------
+Any code that lives in tempest/lib will be treated as a stable interface.
+This means that any public interface under the tempest/lib directory is
+expected to be a stable interface suitable for public consumption. However, for
+any interfaces outside of tempest/lib in the tempest tree (unless otherwise
+noted) or any private interfaces the same stability guarantees don't apply.
+
+Adding Interfaces
+'''''''''''''''''
+When adding an interface to tempest/lib we have to make sure there are no
+dependencies on any pieces of tempest outside of tempest/lib. This means if
+for example there is a dependency on the configuration file we need remove that.
+The other aspect when adding an interface is to make sure it's really an
+interface ready for external consumption and something we want to commit to
+supporting.
+
+Making changes
+''''''''''''''
+When making changes to tempest/lib you have to be conscious of the effect of
+any changes on external consumers. If your proposed changeset will change the
+default behaviour of any interface, or make something which previously worked
+not after your change, then it is not acceptable. Every effort needs to go into
+preserving backwards compatibility in changes.
+
+Reviewing
+'''''''''
+When reviewing a proposed change to tempest/lib code we need to be careful to
+ensure that we don't break backwards compatibility. For patches that change
+existing interfaces we have to be careful to make sure we don't break any
+external consumers. Some common red flags are:
+
+* a change to an existing API requires a change outside the library directory
+ where the interface is being consumed
+* a unit test has to be significantly changed to make the proposed change pass
+
+Testing
+'''''''
+When adding a new interface to the library we need to at a minimum have unit
+test coverage. A proposed change to add an interface to tempest/lib that
+doesn't have unit tests shouldn't be accepted. Ideally these unit tests will
+provide sufficient coverage to ensure a stable interface moving forward.
+
+Current Library APIs
+--------------------
+
+.. toctree::
+ :maxdepth: 2
+
+ library/cli
+ library/decorators
+ library/rest_client
+ library/utils
+ library/api_microversion_testing
+ library/auth
+ library/clients
+ library/credential_providers
+ library/validation_resources
diff --git a/doc/source/library/api_microversion_testing.rst b/doc/source/library/api_microversion_testing.rst
new file mode 100644
index 0000000..8be924d
--- /dev/null
+++ b/doc/source/library/api_microversion_testing.rst
@@ -0,0 +1,29 @@
+.. _api_microversion_testing:
+
+API Microversion Testing Support in Tempest
+===========================================
+
+---------------------------------------------
+Framework to support API Microversion testing
+---------------------------------------------
+
+Many of the OpenStack components have implemented API microversions.
+It is important to test those microversions in Tempest or external plugins.
+Tempest now provides stable interfaces to support to test the API microversions.
+Based on the microversion range coming from the combination of both configuration
+and each test case, APIs request will be made with selected microversion.
+
+This document explains the interfaces needed for microversion testing.
+
+
+The api_version_request module
+""""""""""""""""""""""""""""""
+
+.. automodule:: tempest.lib.common.api_version_request
+ :members:
+
+The api_version_utils module
+""""""""""""""""""""""""""""
+
+.. automodule:: tempest.lib.common.api_version_utils
+ :members:
diff --git a/doc/source/library/auth.rst b/doc/source/library/auth.rst
new file mode 100644
index 0000000..e1d92ed
--- /dev/null
+++ b/doc/source/library/auth.rst
@@ -0,0 +1,11 @@
+.. _auth:
+
+Authentication Framework Usage
+==============================
+
+---------------
+The auth module
+---------------
+
+.. automodule:: tempest.lib.auth
+ :members:
diff --git a/doc/source/library/cli.rst b/doc/source/library/cli.rst
new file mode 100644
index 0000000..6bd7881
--- /dev/null
+++ b/doc/source/library/cli.rst
@@ -0,0 +1,18 @@
+.. _cli:
+
+CLI Testing Framework Usage
+===========================
+
+-------------------
+The cli.base module
+-------------------
+
+.. automodule:: tempest.lib.cli.base
+ :members:
+
+----------------------------
+The cli.output_parser module
+----------------------------
+
+.. automodule:: tempest.lib.cli.output_parser
+ :members:
diff --git a/doc/source/library/clients.rst b/doc/source/library/clients.rst
new file mode 100644
index 0000000..0f4ba4c
--- /dev/null
+++ b/doc/source/library/clients.rst
@@ -0,0 +1,33 @@
+.. _clients:
+
+Service Clients Usage
+=====================
+
+Tests make requests against APIs using service clients. Service clients are
+specializations of the ``RestClient`` class. The service clients that cover the
+APIs exposed by a service should be grouped in a service clients module.
+A service clients module is python module where all service clients are
+defined. If major API versions are available, submodules should be defined,
+one for each version.
+
+The ``ClientsFactory`` class helps initializing all clients of a specific
+service client module from a set of shared parameters.
+
+The ``ServiceClients`` class provides a convenient way to get access to all
+available service clients initialized with a provided set of credentials.
+
+-----------------------------
+The clients management module
+-----------------------------
+
+.. automodule:: tempest.lib.services.clients
+ :members:
+
+------------------------------
+Compute service client modules
+------------------------------
+
+.. toctree::
+ :maxdepth: 2
+
+ service_clients/compute_clients
diff --git a/doc/source/library/credential_providers.rst b/doc/source/library/credential_providers.rst
new file mode 100644
index 0000000..d25f85c
--- /dev/null
+++ b/doc/source/library/credential_providers.rst
@@ -0,0 +1,147 @@
+.. _cred_providers:
+
+Credential Providers
+====================
+
+These library interfaces are used to deal with allocating credentials on demand
+either dynamically by calling keystone to allocate new credentials, or from
+a list of preprovisioned credentials. These 2 modules are implementations of
+the same abstract credential providers class and can be used interchangeably.
+However, each implementation has some additional parameters that are used to
+influence the behavior of the modules. The API reference at the bottom of this
+doc shows the interface definitions for both modules, however that may be a bit
+opaque. You can see some examples of how to leverage this interface below.
+
+Initialization Example
+----------------------
+This example is from Tempest itself (from tempest/common/credentials_factory.py
+just modified slightly) and is how it initializes the credential provider based
+on config::
+
+ from tempest import config
+ from tempest.lib.common import dynamic_creds
+ from tempest.lib.common import preprov_creds
+
+ CONF = config.CONF
+
+ def get_credentials_provider(name, network_resources=None,
+ force_tenant_isolation=False,
+ identity_version=None):
+ # If a test requires a new account to work, it can have it via forcing
+ # dynamic credentials. A new account will be produced only for that test.
+ # In case admin credentials are not available for the account creation,
+ # the test should be skipped else it would fail.
+ identity_version = identity_version or CONF.identity.auth_version
+ if CONF.auth.use_dynamic_credentials or force_tenant_isolation:
+ admin_creds = get_configured_admin_credentials(
+ fill_in=True, identity_version=identity_version)
+ return dynamic_creds.DynamicCredentialProvider(
+ name=name,
+ network_resources=network_resources,
+ identity_version=identity_version,
+ admin_creds=admin_creds,
+ identity_admin_domain_scope=CONF.identity.admin_domain_scope,
+ identity_admin_role=CONF.identity.admin_role,
+ extra_roles=CONF.auth.tempest_roles,
+ neutron_available=CONF.service_available.neutron,
+ project_network_cidr=CONF.network.project_network_cidr,
+ project_network_mask_bits=CONF.network.project_network_mask_bits,
+ public_network_id=CONF.network.public_network_id,
+ create_networks=(CONF.auth.create_isolated_networks and not
+ CONF.network.shared_physical_network),
+ resource_prefix='tempest',
+ credentials_domain=CONF.auth.default_credentials_domain_name,
+ admin_role=CONF.identity.admin_role,
+ identity_uri=CONF.identity.uri_v3,
+ identity_admin_endpoint_type=CONF.identity.v3_endpoint_type)
+ else:
+ if CONF.auth.test_accounts_file:
+ # Most params are not relevant for pre-created accounts
+ return preprov_creds.PreProvisionedCredentialProvider(
+ name=name, identity_version=identity_version,
+ accounts_lock_dir=lockutils.get_lock_path(CONF),
+ test_accounts_file=CONF.auth.test_accounts_file,
+ object_storage_operator_role=CONF.object_storage.operator_role,
+ object_storage_reseller_admin_role=reseller_admin_role,
+ credentials_domain=CONF.auth.default_credentials_domain_name,
+ admin_role=CONF.identity.admin_role,
+ identity_uri=CONF.identity.uri_v3,
+ identity_admin_endpoint_type=CONF.identity.v3_endpoint_type)
+ else:
+ raise exceptions.InvalidConfiguration(
+ 'A valid credential provider is needed')
+
+This function just returns an initialized credential provider class based on the
+config file. The consumer of this function treats the output as the same
+regardless of whether it's a dynamic or preprovisioned provider object.
+
+Dealing with Credentials
+------------------------
+
+Once you have a credential provider object created the access patterns for
+allocating and removing credentials are the same across both the dynamic
+and preprovisioned credentials. These are defined in the abstract
+CredentialProvider class. At a high level the credentials provider enables
+you to get 3 basic types of credentials at once (per object): a primary, alt,
+and admin. You're also able to allocate a credential by role. These credentials
+are tracked by the provider object and delete must manually be called otherwise
+the created resources will not be deleted (or returned to the pool in the case
+of preprovisioned creds)
+
+Examples
+''''''''
+
+Continuing from the example above, to allocate credentials by the 3 basic types
+you can do the following::
+
+ provider = get_credentials_provider('my_tests')
+ primary_creds = provider.get_primary_creds()
+ alt_creds = provider.get_alt_creds()
+ admin_creds = provider.get_admin_creds()
+ # Make sure to delete the credentials when you're finished
+ provider.clear_creds()
+
+To create and interact with credentials by role you can do the following::
+
+ provider = get_credentials_provider('my_tests')
+ my_role_creds = provider.get_creds_by_role({'roles': ['my_role']})
+ # provider.clear_creds() will clear all creds including those allocated by
+ # role
+ provider.clear_creds()
+
+When multiple roles are specified a set of creds with all the roles assigned
+will be allocated::
+
+ provider = get_credentials_provider('my_tests')
+ my_role_creds = provider.get_creds_by_role({'roles': ['my_role',
+ 'my_other_role']})
+ # provider.clear_creds() will clear all creds including those allocated by
+ # role
+ provider.clear_creds()
+
+If you need multiple sets of credentials with the same roles you can also do
+this by leveraging the ``force_new`` kwarg::
+
+ provider = get_credentials_provider('my_tests')
+ my_role_creds = provider.get_creds_by_role({'roles': ['my_role']})
+ my_role_other_creds = provider.get_creds_by_role({'roles': ['my_role']},
+ force_new=True)
+ # provider.clear_creds() will clear all creds including those allocated by
+ # role
+ provider.clear_creds()
+
+
+API Reference
+-------------
+
+The dynamic credentials module
+''''''''''''''''''''''''''''''
+
+.. automodule:: tempest.lib.common.dynamic_creds
+ :members:
+
+The pre-provisioned credentials module
+''''''''''''''''''''''''''''''''''''''
+
+.. automodule:: tempest.lib.common.preprov_creds
+ :members:
diff --git a/doc/source/library/decorators.rst b/doc/source/library/decorators.rst
new file mode 100644
index 0000000..a173967
--- /dev/null
+++ b/doc/source/library/decorators.rst
@@ -0,0 +1,13 @@
+.. _decorators:
+
+Decorators Usage Guide
+======================
+
+---------------------
+The decorators module
+---------------------
+
+.. automodule:: tempest.lib.decorators
+ :members:
+
+
diff --git a/doc/source/library/rest_client.rst b/doc/source/library/rest_client.rst
new file mode 100644
index 0000000..3045694
--- /dev/null
+++ b/doc/source/library/rest_client.rst
@@ -0,0 +1,11 @@
+.. _rest_client:
+
+Rest Client Usage
+=================
+
+----------------------
+The rest_client module
+----------------------
+
+.. automodule:: tempest.lib.common.rest_client
+ :members:
diff --git a/doc/source/library/service_clients/compute_clients.rst b/doc/source/library/service_clients/compute_clients.rst
new file mode 100644
index 0000000..4ca55d4
--- /dev/null
+++ b/doc/source/library/service_clients/compute_clients.rst
@@ -0,0 +1,7 @@
+.. _servers_client:
+
+Compute Client Usage
+====================
+
+.. automodule:: tempest.lib.services.compute.servers_client
+ :members:
diff --git a/doc/source/library/utils.rst b/doc/source/library/utils.rst
new file mode 100644
index 0000000..bc2f79b
--- /dev/null
+++ b/doc/source/library/utils.rst
@@ -0,0 +1,11 @@
+.. _utils:
+
+Utils Usage
+===========
+
+---------------
+The misc module
+---------------
+
+.. automodule:: tempest.lib.common.utils.misc
+ :members:
diff --git a/doc/source/library/validation_resources.rst b/doc/source/library/validation_resources.rst
new file mode 100644
index 0000000..9b36476
--- /dev/null
+++ b/doc/source/library/validation_resources.rst
@@ -0,0 +1,11 @@
+.. _validation_resources:
+
+Validation Resources
+====================
+
+-------------------------------
+The validation_resources module
+-------------------------------
+
+.. automodule:: tempest.lib.common.validation_resources
+ :members:
diff --git a/doc/source/microversion_testing.rst b/doc/source/microversion_testing.rst
new file mode 100644
index 0000000..ea868ae
--- /dev/null
+++ b/doc/source/microversion_testing.rst
@@ -0,0 +1,413 @@
+=================================
+Microversion Testing With Tempest
+=================================
+
+Many OpenStack Services provide their APIs with `microversion`_
+support and want to test them in Tempest.
+
+.. _microversion: http://specs.openstack.org/openstack/api-wg/guidelines/microversion_specification.html
+
+This document covers how to test microversions for each project and
+whether tests should live in Tempest or on project side.
+
+Tempest Scope For Microversion Testing
+""""""""""""""""""""""""""""""""""""""
+APIs microversions for any OpenStack service grow rapidly and
+testing each and every microversion in Tempest is not feasible and
+efficient way.
+Also not every API microversion changes the complete system behavior
+and many of them only change the API or DB layer to accept and return more
+data on API.
+
+Tempest is an integration test suite, but not all API microversion testing fall under this category.
+As a result, Tempest mainly covers integration test cases for microversions, Other testing coverage
+for microversion should be hosted on project side as functional tests or via Tempest plugin as per
+project guidelines.
+
+.. note:: Integration tests are those tests which involve more than one service to
+ verify the expected behavior by single or combination of API requests.
+ If a test is just to verify the API behavior as success and failure cases
+ or verify its expected response object, then it does not fall under integration
+ tests.
+
+Tempest will cover only integration testing of applicable microversions with
+below exceptions:
+
+#. Test covers a feature which is important for interoperability. This covers tests requirement
+ from Defcore.
+#. Test needed to fill Schema gaps.
+ Tempest validates API responses with defined JSON schema. API responses can be different on
+ each microversion and the JSON schemas need to be defined separately for the microversion.
+ While implementing new integration tests for a specific microversion, there
+ may be a gap in the JSON schemas (caused by previous microversions) implemented
+ in Tempest.
+ Filling that gap while implementing the new integration test cases is not efficient due to
+ many reasons:
+
+ * Hard to review
+ * Sync between multiple integration tests patches which try to fill the same schema gap at same
+ time
+ * Might delay the microversion change on project side where project team wants Tempest
+ tests to verify the results.
+
+ Tempest will allow to fill the schema gaps at the end of each cycle, or more
+ often if required.
+ Schema gap can be filled with testing those with a minimal set of tests. Those
+ tests might not be integration tests and might be already covered on project
+ side also.
+ This exception is needed because:
+
+ * Allow to create microversion response schema in Tempest at the same time that projects are
+ implementing their API microversions. This will make implementation easier for adding
+ required tests before a new microversion change can be merged in the corresponding project
+ and hence accelerate the development of microversions.
+ * New schema must be verified by at least one test case which exercises such schema.
+
+ For example:
+ If any projects implemented 4 API microversion say- v2.3, v2.4, v2.5, v2.6
+ Assume microversion v2.3, v2.4, v2.6 change the API Response which means Tempest
+ needs to add JSON schema for v2.3, v2.4, v2.6.
+ In that case if only 1 or 2 tests can verify all new schemas then we do not need
+ separate tests for each new schemas. In worst case, we have to add 3 separate tests.
+#. Test covers service behavior at large scale with involvement of more deep layer like hypervisor
+ etc not just API/DB layer. This type of tests will be added case by case basis and
+ with project team consultation about why it cannot be covered on project side and worth to test
+ in Tempest.
+
+Project Scope For Microversion Testing
+""""""""""""""""""""""""""""""""""""""
+All microversions testing which are not covered under Tempest as per above section, should be
+tested on project side as functional tests or as Tempest plugin as per project decision.
+
+
+Configuration options for Microversion
+""""""""""""""""""""""""""""""""""""""
+
+* Add configuration options for specifying test target Microversions.
+ We need to specify test target Microversions because the supported
+ Microversions may be different between OpenStack clouds. For operating
+ multiple Microversion tests in a single Tempest operation, configuration
+ options should represent the range of test target Microversions.
+ New configuration options are:
+
+ * min_microversion
+ * max_microversion
+
+ Those should be defined under respective section of each service.
+ For example:
+
+ .. code-block:: ini
+
+ [compute]
+ min_microversion = None
+ max_microversion = latest
+
+
+How To Implement Microversion Tests
+"""""""""""""""""""""""""""""""""""
+
+Tempest provides stable interfaces to test API Microversion.
+For Details, see: `API Microversion testing Framework`_
+This document explains how to implement Microversion tests using those
+interfaces.
+
+.. _API Microversion testing Framework: https://docs.openstack.org/tempest/latest/library/api_microversion_testing.html
+
+
+Step1: Add skip logic based on configured Microversion range
+''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
+
+Add logic to skip the tests based on Tests class and configured Microversion
+range.
+api_version_utils.check_skip_with_microversion function can be used
+to automatically skip the tests which do not fall under configured
+Microversion range.
+For example:
+
+.. code-block:: python
+
+ class BaseTestCase1(api_version_utils.BaseMicroversionTest):
+
+ [..]
+ @classmethod
+ def skip_checks(cls):
+ super(BaseTestCase1, cls).skip_checks()
+ api_version_utils.check_skip_with_microversion(cls.min_microversion,
+ cls.max_microversion,
+ CONF.compute.min_microversion,
+ CONF.compute.max_microversion)
+
+Skip logic can be added in tests base class or any specific test class depends on
+tests class structure.
+
+Step2: Selected API request microversion
+''''''''''''''''''''''''''''''''''''''''
+
+Select appropriate Microversion which needs to be used
+to send with API request.
+api_version_utils.select_request_microversion function can be used
+to select the appropriate Microversion which will be used for API request.
+For example:
+
+.. code-block:: python
+
+ @classmethod
+ def resource_setup(cls):
+ super(BaseTestCase1, cls).resource_setup()
+ cls.request_microversion = (
+ api_version_utils.select_request_microversion(
+ cls.min_microversion,
+ CONF.compute.min_microversion))
+
+
+Step3: Set Microversion on Service Clients
+''''''''''''''''''''''''''''''''''''''''''
+
+Microversion selected by Test Class in previous step needs to be set on
+service clients so that APIs can be requested with selected Microversion.
+
+Microversion can be defined as global variable on service clients which
+can be set using fixture.
+Also Microversion header name needs to be defined on service clients which
+should be constant because it is not supposed to be changed by project
+as per API contract.
+For example:
+
+.. code-block:: python
+
+ COMPUTE_MICROVERSION = None
+
+ class BaseClient1(rest_client.RestClient):
+ api_microversion_header_name = 'X-OpenStack-Nova-API-Version'
+
+Now test class can set the selected Microversion on required service clients
+using fixture which can take care of resetting the same once tests is completed.
+For example:
+
+.. code-block:: python
+
+ def setUp(self):
+ super(BaseTestCase1, self).setUp()
+ self.useFixture(api_microversion_fixture.APIMicroversionFixture(
+ self.request_microversion))
+
+Service clients needs to add set Microversion in API request header which
+can be done by overriding the get_headers() method of rest_client.
+For example:
+
+.. code-block:: python
+
+ COMPUTE_MICROVERSION = None
+
+ class BaseClient1(rest_client.RestClient):
+ api_microversion_header_name = 'X-OpenStack-Nova-API-Version'
+
+ def get_headers(self):
+ headers = super(BaseClient1, self).get_headers()
+ if COMPUTE_MICROVERSION:
+ headers[self.api_microversion_header_name] = COMPUTE_MICROVERSION
+ return headers
+
+
+Step4: Separate Test classes for each Microversion
+''''''''''''''''''''''''''''''''''''''''''''''''''
+
+This is last step to implement Microversion test class.
+
+For any Microversion tests, basically we need to implement a
+separate test class. In addition, each test class defines its
+Microversion range with class variable like min_microversion
+and max_microversion. Tests will be valid for that defined range.
+If that range is out of configured Microversion range then, test
+will be skipped.
+
+.. note:: Microversion testing is supported at test class level not at
+ individual test case level.
+
+For example:
+
+Below test is applicable for Microversion from 2.2 till 2.9:
+
+.. code-block:: python
+
+ class BaseTestCase1(api_version_utils.BaseMicroversionTest,
+ tempest.test.BaseTestCase):
+
+ [..]
+
+
+ class Test1(BaseTestCase1):
+ min_microversion = '2.2'
+ max_microversion = '2.9'
+
+ [..]
+
+Below test is applicable for Microversion from 2.10 till latest:
+
+.. code-block:: python
+
+ class Test2(BaseTestCase1):
+ min_microversion = '2.10'
+ max_microversion = 'latest'
+
+ [..]
+
+
+Notes about Compute Microversion Tests
+""""""""""""""""""""""""""""""""""""""
+
+Some of the compute Microversion tests have been already implemented
+with the Microversion testing framework. So for further tests only
+step 4 is needed.
+
+Along with that JSON response schema might need versioning if needed.
+
+Compute service clients strictly validate the response against defined JSON
+schema and does not allow additional elements in response.
+So if that Microversion changed the API response then schema needs to be versioned.
+New JSON schema file needs to be defined with new response attributes and service
+client methods will select the schema based on requested microversion.
+
+If Microversion tests are implemented randomly meaning not
+in sequence order(v2.20 tests added and previous Microversion tests are not yet added)
+then, still schema might need to be version for older Microversion if they changed
+the response.
+This is because Nova Microversion includes all the previous Microversions behavior.
+
+For Example:
+ Implementing the v2.20 Microversion tests before v2.9 and 2.19-
+ v2.20 API request will respond as latest behavior of Nova till v2.20,
+ and in v2.9 and 2.19, server response has been changed so response schema needs
+ to be versioned accordingly.
+
+That can be done by using the get_schema method in below module:
+
+The base_compute_client module
+''''''''''''''''''''''''''''''
+
+.. automodule:: tempest.lib.services.compute.base_compute_client
+ :members:
+
+
+Microversion tests implemented in Tempest
+"""""""""""""""""""""""""""""""""""""""""
+
+* Compute
+
+ * `2.1`_
+
+ .. _2.1: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id1
+
+ * `2.2`_
+
+ .. _2.2: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id2
+
+ * `2.6`_
+
+ .. _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
+
+ * `2.21`_
+
+ .. _2.21: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id19
+
+ * `2.25`_
+
+ .. _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
+
+ * `2.37`_
+
+ .. _2.37: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id34
+
+ * `2.42`_
+
+ .. _2.42: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#maximum-in-ocata
+
+ * `2.47`_
+
+ .. _2.47: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id42
+
+ * `2.48`_
+
+ .. _2.48: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id43
+
+ * `2.49`_
+
+ .. _2.49: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id44
+
+ * `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.57`_
+
+ .. _2.57: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id51
+
+ * `2.60`_
+
+ .. _2.60: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#maximum-in-queens
+
+ * `2.63`_
+
+ .. _2.63: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id57
+
+* Volume
+
+ * `3.3`_
+
+ .. _3.3: https://docs.openstack.org/cinder/latest/contributor/api_microversion_history.html#id3
+
+ * `3.9`_
+
+ .. _3.9: https://docs.openstack.org/cinder/latest/contributor/api_microversion_history.html#id9
+
+ * `3.11`_
+
+ .. _3.11: https://docs.openstack.org/cinder/latest/contributor/api_microversion_history.html#id11
+
+ * `3.12`_
+
+ .. _3.12: https://docs.openstack.org/cinder/latest/contributor/api_microversion_history.html#id12
+
+ * `3.13`_
+
+ .. _3.13: https://docs.openstack.org/cinder/latest/contributor/api_microversion_history.html#id13
+
+ * `3.14`_
+
+ .. _3.14: https://docs.openstack.org/cinder/latest/contributor/api_microversion_history.html#id14
+
+ * `3.19`_
+
+ .. _3.19: https://docs.openstack.org/cinder/latest/contributor/api_microversion_history.html#id18
+
+ * `3.20`_
+
+ .. _3.20: https://docs.openstack.org/cinder/latest/contributor/api_microversion_history.html#id19
diff --git a/doc/source/plugin.rst b/doc/source/plugin.rst
index 29653a6..6f6621d 100644
--- a/doc/source/plugin.rst
+++ b/doc/source/plugin.rst
@@ -1,3 +1,5 @@
+.. _tempest_plugin:
+
=============================
Tempest Test Plugin Interface
=============================
@@ -11,14 +13,29 @@
=================
Creating a plugin is fairly straightforward and doesn't require much additional
-effort on top of creating a test suite using tempest-lib. One thing to note with
+effort on top of creating a test suite using tempest.lib. One thing to note with
doing this is that the interfaces exposed by tempest are not considered stable
(with the exception of configuration variables which ever effort goes into
ensuring backwards compatibility). You should not need to import anything from
-tempest itself except where explicitly noted. If there is an interface from
-tempest that you need to rely on in your plugin it likely needs to be migrated
-to tempest-lib. In that situation, file a bug, push a migration patch, etc. to
-expedite providing the interface in a reliable manner.
+tempest itself except where explicitly noted.
+
+Stable Tempest APIs plugins may use
+-----------------------------------
+
+As noted above, several tempest APIs are acceptable to use from plugins, while
+others are not. A list of stable APIs available to plugins is provided below:
+
+* tempest.lib.*
+* tempest.config
+* tempest.test_discover.plugins
+* tempest.common.credentials_factory
+* tempest.clients
+* tempest.test
+
+If there is an interface from tempest that you need to rely on in your plugin
+which is not listed above, it likely needs to be migrated to tempest.lib. In
+that situation, file a bug, push a migration patch, etc. to expedite providing
+the interface in a reliable manner.
Plugin Cookiecutter
-------------------
@@ -49,12 +66,45 @@
to the "tempest.test_plugins" namespace.
If you are using pbr this is fairly straightforward, in the setup.cfg just add
-something like the following::
+something like the following:
+
+.. code-block:: ini
[entry_points]
tempest.test_plugins =
plugin_name = module.path:PluginClass
+Standalone Plugin vs In-repo Plugin
+-----------------------------------
+
+Since all that's required for a plugin to be detected by tempest is a valid
+setuptools entry point in the proper namespace there is no difference from the
+tempest perspective on either creating a separate python package to
+house the plugin or adding the code to an existing python project. However,
+there are tradeoffs to consider when deciding which approach to take when
+creating a new plugin.
+
+If you create a separate python project for your plugin this makes a lot of
+things much easier. Firstly it makes packaging and versioning much simpler, you
+can easily decouple the requirements for the plugin from the requirements for
+the other project. It lets you version the plugin independently and maintain a
+single version of the test code across project release boundaries (see the
+`Branchless Tempest Spec`_ for more details on this). It also greatly
+simplifies the install time story for external users. Instead of having to
+install the right version of a project in the same python namespace as tempest
+they simply need to pip install the plugin in that namespace. It also means
+that users don't have to worry about inadvertently installing a tempest plugin
+when they install another package.
+
+.. _Branchless Tempest Spec: http://specs.openstack.org/openstack/qa-specs/specs/tempest/implemented/branchless-tempest.html
+
+The sole advantage to integrating a plugin into an existing python project is
+that it enables you to land code changes at the same time you land test changes
+in the plugin. This reduces some of the burden on contributors by not having
+to land 2 changes to add a new API feature and then test it and doing it as a
+single combined commit.
+
+
Plugin Class
============
@@ -62,19 +112,17 @@
your plugin you need to create a plugin class which tempest will load and call
to get information when it needs. To simplify creating this tempest provides an
abstract class that should be used as the parent for your plugin. To use this
-you would do something like the following::
+you would do something like the following:
+
+.. code-block:: python
from tempest.test_discover import plugins
class MyPlugin(plugins.TempestPlugin):
-Then you need to ensure you locally define all of the methods in the abstract
-class, you can refer to the api doc below for a reference of what that entails.
-
-Also, note eventually this abstract class will likely live in tempest-lib, when
-that migration occurs a deprecation shim will be added to tempest so as to not
-break any existing plugins. But, when that occurs migrating to using tempest-lib
-as the source for the abstract class will be prudent.
+Then you need to ensure you locally define all of the mandatory methods in the
+abstract class, you can refer to the api doc below for a reference of what that
+entails.
Abstract Plugin Class
---------------------
@@ -84,7 +132,7 @@
Plugin Structure
================
-While there are no hard and fast rules for the structure a plugin, there are
+While there are no hard and fast rules for the structure of a plugin, there are
basically no constraints on what the plugin looks like as long as the 2 steps
above are done. However, there are some recommended patterns to follow to make
it easy for people to contribute and work with your plugin. For example, if you
@@ -126,6 +174,152 @@
CONF object from tempest. This enables the plugin to add options to both
existing sections and also create new configuration sections for new options.
+Service Clients
+---------------
+
+If a plugin defines a service client, it is beneficial for it to implement the
+``get_service_clients`` method in the plugin class. All service clients which
+are exposed via this interface will be automatically configured and be
+available in any instance of the service clients class, defined in
+``tempest.lib.services.clients.ServiceClients``. In case multiple plugins are
+installed, all service clients from all plugins will be registered, making it
+easy to write tests which rely on multiple APIs whose service clients are in
+different plugins.
+
+Example implementation of ``get_service_clients``:
+
+.. code-block:: python
+
+ def get_service_clients(self):
+ # Example implementation with two service clients
+ my_service1_config = config.service_client_config('my_service')
+ params_my_service1 = {
+ 'name': 'my_service_v1',
+ 'service_version': 'my_service.v1',
+ 'module_path': 'plugin_tempest_tests.services.my_service.v1',
+ 'client_names': ['API1Client', 'API2Client'],
+ }
+ params_my_service1.update(my_service_config)
+ my_service2_config = config.service_client_config('my_service')
+ params_my_service2 = {
+ 'name': 'my_service_v2',
+ 'service_version': 'my_service.v2',
+ 'module_path': 'plugin_tempest_tests.services.my_service.v2',
+ 'client_names': ['API1Client', 'API2Client'],
+ }
+ params_my_service2.update(my_service2_config)
+ return [params_my_service1, params_my_service2]
+
+Parameters:
+
+* **name**: Name of the attribute used to access the ``ClientsFactory`` from
+ the ``ServiceClients`` instance. See example below.
+* **service_version**: Tempest enforces a single implementation for each
+ service client. Available service clients are held in a ``ClientsRegistry``
+ singleton, and registered with ``service_version``, which means that
+ ``service_version`` must be unique and it should represent the service API
+ and version implemented by the service client.
+* **module_path**: Relative to the service client module, from the root of the
+ plugin.
+* **client_names**: Name of the classes that implement service clients in the
+ service clients module.
+
+Example usage of the service clients in tests:
+
+.. code-block:: python
+
+ # my_creds is instance of tempest.lib.auth.Credentials
+ # identity_uri is v2 or v3 depending on the configuration
+ from tempest.lib.services import clients
+
+ my_clients = clients.ServiceClients(my_creds, identity_uri)
+ my_service1_api1_client = my_clients.my_service_v1.API1Client()
+ my_service2_api1_client = my_clients.my_service_v2.API1Client(my_args='any')
+
+Automatic configuration and registration of service clients imposes some extra
+constraints on the structure of the configuration options exposed by the
+plugin.
+
+First ``service_version`` should be in the format `service_config[.version]`.
+The `.version` part is optional, and should only be used if there are multiple
+versions of the same API available. The `service_config` must match the name of
+a configuration options group defined by the plugin. Different versions of one
+API must share the same configuration group.
+
+Second the configuration options group `service_config` must contain the
+following options:
+
+* `catalog_type`: corresponds to `service` in the catalog
+* `endpoint_type`
+
+The following options will be honoured if defined, but they are not mandatory,
+as they do not necessarily apply to all service clients.
+
+* `region`: default to identity.region
+* `build_timeout` : default to compute.build_timeout
+* `build_interval`: default to compute.build_interval
+
+Third the service client classes should inherit from ``RestClient``, should
+accept generic keyword arguments, and should pass those arguments to the
+``__init__`` method of ``RestClient``. Extra arguments can be added. For
+instance:
+
+.. code-block:: python
+
+ class MyAPIClient(rest_client.RestClient):
+
+ def __init__(self, auth_provider, service, region,
+ my_arg, my_arg2=True, **kwargs):
+ super(MyAPIClient, self).__init__(
+ auth_provider, service, region, **kwargs)
+ self.my_arg = my_arg
+ self.my_args2 = my_arg
+
+Finally the service client should be structured in a python module, so that all
+service client classes are importable from it. Each major API version should
+have its own module.
+
+The following folder and module structure is recommended for a single major
+API version::
+
+ plugin_dir/
+ services/
+ __init__.py
+ client_api_1.py
+ client_api_2.py
+
+The content of __init__.py module should be:
+
+.. code-block:: python
+
+ from client_api_1.py import API1Client
+ from client_api_2.py import API2Client
+
+ __all__ = ['API1Client', 'API2Client']
+
+The following folder and module structure is recommended for multiple major
+API version::
+
+ plugin_dir/
+ services/
+ v1/
+ __init__.py
+ client_api_1.py
+ client_api_2.py
+ v2/
+ __init__.py
+ client_api_1.py
+ client_api_2.py
+
+The content each of __init__.py module under vN should be:
+
+.. code-block:: python
+
+ from client_api_1.py import API1Client
+ from client_api_2.py import API2Client
+
+ __all__ = ['API1Client', 'API2Client']
+
Using Plugins
=============
diff --git a/doc/source/run.rst b/doc/source/run.rst
new file mode 100644
index 0000000..ce7f03e
--- /dev/null
+++ b/doc/source/run.rst
@@ -0,0 +1,7 @@
+.. _tempest_run:
+
+-----------
+Tempest Run
+-----------
+
+.. automodule:: tempest.cmd.run
diff --git a/doc/source/stable_branch_support_policy.rst b/doc/source/stable_branch_support_policy.rst
new file mode 100644
index 0000000..87e3ad1
--- /dev/null
+++ b/doc/source/stable_branch_support_policy.rst
@@ -0,0 +1,30 @@
+Stable Branch Support Policy
+============================
+
+Since the `Extended Maintenance policy`_ for stable branches was adopted
+OpenStack projects will keep stable branches around after a "stable" or
+"maintained" period for a phase of indeterminate length called "Extended
+Maintenance". Prior to this resolution Tempest supported all stable branches
+which were supported upstream. This policy does not scale under the new model
+as Tempest would be responsible for gating proposed changes against an ever
+increasing number of branches. Therefore due to resource constraints, Tempest
+will only provide support for branches in the "Maintained" phase from the
+documented `Support Phases`_. When a branch moves from the *Maintained* to the
+*Extended Maintenance* phase, Tempest will tag the removal of support for that
+branch as it has in the past when a branch goes end of life.
+
+The expectation for *Extended Maintenance* phase branches is that they will continue
+running Tempest during that phase of support. Since the REST APIs are stable
+interfaces across release boundaries, branches in these phases should run
+Tempest from master as long as possible. But, because we won't be actively
+testing branches in these phases, it's possible that we'll introduce changes to
+Tempest on master which will break support on *Extended Maintenance* phase
+branches. When this happens the expectation for those branches is to either
+switch to running Tempest from a tag with support for the branch, or blacklist
+a newly introduced test (if that is the cause of the issue). Tempest will not
+be creating stable branches to support *Extended Maintenance* phase branches, as
+the burden is on the *Extended Maintenance* phase branche maintainers, not the Tempest
+project, to support that branch.
+
+.. _Extended Maintenance policy: https://governance.openstack.org/tc/resolutions/20180301-stable-branch-eol.html
+.. _Support Phases: https://docs.openstack.org/project-team-guide/stable-branches.html#maintenance-phases
diff --git a/doc/source/subunit_describe_calls.rst b/doc/source/subunit_describe_calls.rst
new file mode 100644
index 0000000..2bda50c
--- /dev/null
+++ b/doc/source/subunit_describe_calls.rst
@@ -0,0 +1,5 @@
+------------------------------
+Subunit Describe Calls Utility
+------------------------------
+
+.. automodule:: tempest.cmd.subunit_describe_calls
diff --git a/doc/source/test_removal.rst b/doc/source/test_removal.rst
new file mode 100644
index 0000000..ddae6e2
--- /dev/null
+++ b/doc/source/test_removal.rst
@@ -0,0 +1,193 @@
+Tempest Test Removal Procedure
+==============================
+
+Historically tempest was the only way of doing functional testing and
+integration testing in OpenStack. This was mostly only an artifact of tempest
+being the only proven pattern for doing this, not an artifact of a design
+decision. However, moving forward as functional testing is being spun up in
+each individual project we really only want tempest to be the integration test
+suite it was intended to be; testing the high level interactions between
+projects through REST API requests. In this model there are probably existing
+tests that aren't the best fit living in tempest. However, since tempest is
+largely still the only gating test suite in this space we can't carelessly rip
+out everything from the tree. This document outlines the procedure which was
+developed to ensure we minimize the risk for removing something of value from
+the tempest tree.
+
+This procedure might seem overly conservative and slow paced, but this is by
+design to try and ensure we don't remove something that is actually providing
+value. Having potential duplication between testing is not a big deal
+especially compared to the alternative of removing something which is actually
+providing value and is actively catching bugs, or blocking incorrect patches
+from landing.
+
+Proposing a test removal
+------------------------
+
+3 prong rule for removal
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+In the proposal etherpad we'll be looking for answers to 3 questions
+
+#. The tests proposed for removal must have equiv. coverage in a different
+ project's test suite (whether this is another gating test project, or an in
+ tree functional test suite). For API tests preferably the other project will
+ have a similar source of friction in place to prevent breaking api changes
+ so that we don't regress and let breaking api changes slip through the
+ gate.
+#. The test proposed for removal has a failure rate < 0.50% in the gate over
+ the past release (the value and interval will likely be adjusted in the
+ future)
+
+ .. _`prong #3`:
+#. There must not be an external user/consumer of tempest
+ that depends on the test proposed for removal
+
+The answers to 1 and 2 are easy to verify. For 1 just provide a link to the new
+test location. If you are linking to the tempest removal patch please also put
+a Depends-On in the commit message for the commit which moved the test into
+another repo.
+
+For prong 2 you can use OpenStack-Health:
+
+Using OpenStack-Health
+""""""""""""""""""""""
+
+Go to: http://status.openstack.org/openstack-health and then navigate to a per
+test page for six months. You'll end up with a page that will graph the success
+and failure rates on the bottom graph. For example, something like `this URL`_.
+
+.. _this URL: http://status.openstack.org/openstack-health/#/test/tempest.scenario.test_volume_boot_pattern.TestVolumeBootPatternV2.test_volume_boot_pattern?groupKey=project&resolutionKey=day&duration=P6M
+
+The Old Way using subunit2sql directly
+""""""""""""""""""""""""""""""""""""""
+
+``SELECT * from tests where test_id like "%test_id%";``
+(where ``$test_id`` is the full test_id, but truncated to the class because of
+``setUpClass`` or ``tearDownClass`` failures)
+
+You can access the infra mysql subunit2sql db w/ read-only permissions with:
+
+* hostname: logstash.openstack.org
+* username: query
+* password: query
+* db_name: subunit2sql
+
+For example if you were trying to remove the test with the id:
+``tempest.api.compute.admin.test_flavors_negative.FlavorsAdminNegativeTestJSON.test_get_flavor_details_for_deleted_flavor``
+you would run the following:
+
+#. run the command: ``mysql -u query -p -h logstash.openstack.org subunit2sql``
+ to connect to the subunit2sql db
+#. run the query:
+
+ .. code-block:: console
+
+ MySQL [subunit2sql]> select * from tests where test_id like \
+ "tempest.api.compute.admin.test_flavors_negative.FlavorsAdminNegativeTestJSON%";
+
+ which will return a table of all the tests in the class (but it will also
+ catch failures in ``setUpClass`` and ``tearDownClass``)
+#. paste the output table with numbers and the mysql command you ran to
+ generate it into the etherpad.
+
+Eventually a cli interface will be created to make that a bit more friendly.
+Also a dashboard is in the works so we don't need to manually run the command.
+
+The intent of the 2nd prong is to verify that moving the test into a project
+specific testing is preventing bugs (assuming the tempest tests were catching
+issues) from bubbling up a layer into tempest jobs. If we're seeing failure
+rates above a certain threshold in the gate checks that means the functional
+testing isn't really being effective in catching that bug (and therefore
+blocking it from landing) and having the testing run in tempest still has
+value.
+
+However for the 3rd prong verification is a bit more subjective. The original
+intent of this prong was mostly for refstack/defcore and also for things that
+running on the stable branches. We don't want to remove any tests if that
+would break our api consistency checking between releases, or something that
+defcore/refstack is depending on being in tempest. It's worth pointing out
+that if a test is used in defcore as part of interop testing then it will
+probably have continuing value being in tempest as part of the
+integration/integrated tests in general. This is one area where some overlap
+is expected between testing in projects and tempest, which is not a bad thing.
+
+Discussing the 3rd prong
+""""""""""""""""""""""""
+
+There are 2 approaches to addressing the 3rd prong. Either it can be raised
+during a qa meeting during the tempest discussion. Please put it on the agenda
+well ahead of the scheduled meeting. Since the meeting time will be well known
+ahead of time anyone who depends on the tests will have ample time beforehand
+to outline any concerns on the before the meeting. To give ample time for
+people to respond to removal proposals please add things to the agenda by the
+Monday before the meeting.
+
+The other option is to raise the removal on the openstack-dev mailing list.
+(for example see: http://lists.openstack.org/pipermail/openstack-dev/2016-February/086218.html )
+This will raise the issue to the wider community and attract at least the same
+(most likely more) attention than discussing it during the irc meeting. The
+only downside is that it might take more time to get a response, given the
+nature of ML.
+
+Exceptions to this procedure
+----------------------------
+
+For the most part all tempest test removals have to go through this procedure
+there are a couple of exceptions though:
+
+#. The class of testing has been decided to be outside the scope of tempest.
+#. A revert for a patch which added a broken test, or testing which didn't
+ actually run in the gate (basically any revert for something which
+ shouldn't have been added)
+#. Tests that would become out of scope as a consequence of an API change,
+ as described in `API Compatibility`_.
+ Such tests cannot live in Tempest because of the branchless nature of
+ Tempest. Such test must still honor `prong #3`_.
+
+For the first exception type the only types of testing in tree which have been
+declared out of scope at this point are:
+
+* The CLI tests (which should be completely removed at this point)
+* Neutron Adv. Services testing (which should be completely removed at this
+ point)
+* XML API Tests (which should be completely removed at this point)
+* EC2 API/boto tests (which should be completely removed at this point)
+
+For tests that fit into this category the only criteria for removal is that
+there is equivalent testing elsewhere.
+
+Tempest Scope
+^^^^^^^^^^^^^
+
+Starting in the liberty cycle tempest has defined a set of projects which
+are defined as in scope for direct testing in tempest. As of today that list
+is:
+
+* Keystone
+* Nova
+* Glance
+* Cinder
+* Neutron
+* Swift
+
+anything that lives in tempest which doesn't test one of these projects can be
+removed assuming there is equivalent testing elsewhere. Preferably using the
+`tempest plugin mechanism`_
+to maintain continuity after migrating the tests out of tempest.
+
+.. _tempest plugin mechanism: https://docs.openstack.org/tempest/latest/plugin.html
+
+API Compatibility
+"""""""""""""""""
+
+If an API introduces a non-discoverable, backward incompatible change, and
+such change is not backported to all versions supported by Tempest, tests for
+that API cannot live in Tempest anymore.
+This is because tests would not be able to know or control which API response
+to expect, and thus would not be able to enforce a specific behavior.
+
+If a test exists in Tempest that would meet this criteria as consequence of a
+change, the test must be removed according to the procedure discussed into
+this document. The API change should not be merged until all conditions
+required for test removal can be met.
diff --git a/doc/source/workspace.rst b/doc/source/workspace.rst
new file mode 100644
index 0000000..41325b2
--- /dev/null
+++ b/doc/source/workspace.rst
@@ -0,0 +1,5 @@
+-----------------
+Tempest Workspace
+-----------------
+
+.. automodule:: tempest.cmd.workspace
diff --git a/doc/source/write_tests.rst b/doc/source/write_tests.rst
new file mode 100644
index 0000000..fff2405
--- /dev/null
+++ b/doc/source/write_tests.rst
@@ -0,0 +1,324 @@
+.. _tempest_test_writing:
+
+Tempest Test Writing Guide
+##########################
+
+This guide serves as a starting point for developers working on writing new
+Tempest tests. At a high level tests in Tempest are just tests that conform to
+the standard python `unit test`_ framework. But there are several aspects of
+that are unique to Tempest and its role as an integration test suite running
+against a real cloud.
+
+.. _unit test: https://docs.python.org/3.6/library/unittest.html
+
+.. note:: This guide is for writing tests in the Tempest repository. While many
+ parts of this guide are also applicable to Tempest plugins, not all
+ the APIs mentioned are considered stable or recommended for use in
+ plugins. Please refer to :ref:`tempest_plugin` for details about
+ writing plugins
+
+
+Adding a New TestCase
+=====================
+
+The base unit of testing in Tempest is the `TestCase`_ (also called the test
+class). Each TestCase contains test methods which are the individual tests that
+will be executed by the test runner. But, the TestCase is the smallest self
+contained unit for tests from the Tempest perspective. It's also the level at
+which Tempest is parallel safe. In other words, multiple TestCases can be
+executed in parallel, but individual test methods in the same TestCase can not.
+Also, all test methods within a TestCase are assumed to be executed serially. As
+such you can use the test case to store variables that are shared between
+methods.
+
+.. _TestCase: https://docs.python.org/3.6/library/unittest.html#unittest.TestCase
+
+In standard unittest the lifecycle of a TestCase can be described in the
+following phases:
+
+#. setUpClass
+#. setUp
+#. Test Execution
+#. tearDown
+#. doCleanups
+#. tearDownClass
+
+setUpClass
+----------
+
+The setUpClass phase is the first phase executed by the test runner and is used
+to perform any setup required for all the test methods to be executed. In
+Tempest this is a very important step and will automatically do the necessary
+setup for interacting with the configured cloud.
+
+To accomplish this you do **not** define a setUpClass function, instead there
+are a number of predefined phases to setUpClass that are used. The phases are:
+
+* skip_checks
+* setup_credentials
+* setup_clients
+* resource_setup
+
+which is executed in that order. Cleanup of resources provisioned during
+the resource_setup must be scheduled right after provisioning using
+the addClassResourceCleanup helper. The resource cleanups stacked this way
+are executed in reverse order during tearDownClass, before the cleanup of
+test credentials takes place. An example of a TestCase which defines all
+of these would be::
+
+ from tempest.common import waiters
+ from tempest import config
+ from tempest.lib.common.utils import test_utils
+ from tempest import test
+
+ CONF = config.CONF
+
+
+ class TestExampleCase(test.BaseTestCase):
+
+ @classmethod
+ def skip_checks(cls):
+ """This section is used to evaluate config early and skip all test
+ methods based on these checks
+ """
+ super(TestExampleCase, cls).skip_checks()
+ if not CONF.section.foo
+ cls.skip('A helpful message')
+
+ @classmethod
+ def setup_credentials(cls):
+ """This section is used to do any manual credential allocation and also
+ in the case of dynamic credentials to override the default network
+ resource creation/auto allocation
+ """
+ # This call is used to tell the credential allocator to not create any
+ # network resources for this test case. It also enables selective
+ # creation of other neutron resources. NOTE: it must go before the
+ # super call
+ cls.set_network_resources()
+ super(TestExampleCase, cls).setup_credentials()
+
+ @classmethod
+ def setup_clients(cls):
+ """This section is used to setup client aliases from the manager object
+ or to initialize any additional clients. Except in a few very
+ specific situations you should not need to use this.
+ """
+ super(TestExampleCase, cls).setup_clients()
+ cls.servers_client = cls.os_primary.servers_client
+
+ @classmethod
+ def resource_setup(cls):
+ """This section is used to create any resources or objects which are
+ going to be used and shared by **all** test methods in the
+ TestCase. Note then anything created in this section must also be
+ destroyed in the corresponding resource_cleanup() method (which will
+ be run during tearDownClass())
+ """
+ super(TestExampleCase, cls).resource_setup()
+ cls.shared_server = cls.servers_client.create_server(...)
+ cls.addClassResourceCleanup(waiters.wait_for_server_termination,
+ cls.servers_client,
+ cls.shared_server['id'])
+ cls.addClassResourceCleanup(
+ test_utils.call_and_ignore_notfound_exc(
+ cls.servers_client.delete_server,
+ cls.shared_server['id']))
+
+.. _credentials:
+
+Allocating Credentials
+''''''''''''''''''''''
+
+Since Tempest tests are all about testing a running cloud, every test will need
+credentials to be able to make API requests against the cloud. Since this is
+critical to operation and, when running in parallel, easy to make a mistake,
+the base TestCase class will automatically allocate a regular user for each
+TestCase during the setup_credentials() phase. During this process it will also
+initialize a client manager object using those credentials, which will be your
+entry point into interacting with the cloud. For more details on how credentials
+are allocated the :ref:`tempest_cred_provider_conf` section of the Tempest
+Configuration Guide provides more details on the operation of this.
+
+There are some cases when you need more than a single set of credentials, or
+credentials with a more specialized set of roles. To accomplish this you have
+to set a class variable ``credentials`` on the TestCase directly. For example::
+
+ from tempest import test
+
+ class TestExampleAdmin(test.BaseTestCase):
+
+ credentials = ['primary', 'admin']
+
+ @classmethod
+ def skip_checks(cls):
+ ...
+
+In this example the ``TestExampleAdmin`` TestCase will allocate 2 sets of
+credentials, one regular user and one admin user. The corresponding manager
+objects will be set as class variables ``cls.os_primary`` and ``cls.os_admin``
+respectively. You can also allocate a second user by putting **'alt'** in the
+list too. A set of alt credentials are the same as primary but can be used
+for tests cases that need a second user/project.
+
+You can also specify credentials with specific roles assigned. This is useful
+for cases where there are specific RBAC requirements hard coded into an API.
+The canonical example of this are swift tests which often want to test swift's
+concepts of operator and reseller_admin. An actual example from Tempest on how
+to do this is::
+
+ class PublicObjectTest(base.BaseObjectTest):
+
+ credentials = [['operator', CONF.object_storage.operator_role],
+ ['operator_alt', CONF.object_storage.operator_role]]
+
+ @classmethod
+ def setup_credentials(cls):
+ super(PublicObjectTest, cls).setup_credentials()
+ ...
+
+In this case the manager objects will be set to ``cls.os_roles_operator`` and
+``cls.os_roles_operator_alt`` respectively.
+
+
+There is no limit to how many credentials you can allocate in this manner,
+however in almost every case you should **not** need more than 3 sets of
+credentials per test case.
+
+To figure out the mapping of manager objects set on the TestCase and the
+requested credentials you can reference:
+
++-------------------+---------------------+
+| Credentials Entry | Manager Variable |
++===================+=====================+
+| primary | cls.os_primary |
++-------------------+---------------------+
+| admin | cls.os_admin |
++-------------------+---------------------+
+| alt | cls.os_alt |
++-------------------+---------------------+
+| [$label, $role] | cls.os_roles_$label |
++-------------------+---------------------+
+
+By default cls.os_primary is available since it is allocated in the base Tempest test
+class (located in tempest/test.py). If your TestCase inherits from a different
+direct parent class (it'll still inherit from the BaseTestCase, just not
+directly) be sure to check if that class overrides allocated credentials.
+
+Dealing with Network Allocation
+'''''''''''''''''''''''''''''''
+
+When Neutron is enabled and a testing requires networking this isn't normally
+automatically setup when a tenant is created. Since Tempest needs isolated
+tenants to function properly it also needs to handle network allocation. By
+default the base test class will allocate a network, subnet, and router
+automatically (this depends on the configured credential provider, for more
+details see: :ref:`tempest_conf_network_allocation`). However, there are
+situations where you do no need all of these resources allocated (or your
+TestCase inherits from a class that overrides the default in tempest/test.py).
+There is a class level mechanism to override this allocation and specify which
+resources you need. To do this you need to call `cls.set_network_resources()`
+in the `setup_credentials()` method before the `super()`. For example::
+
+ from tempest import test
+
+
+ class TestExampleCase(test.BaseTestCase):
+
+ @classmethod
+ def setup_credentials(cls):
+ cls.set_network_resources(network=True, subnet=True, router=False)
+ super(TestExampleCase, cls).setup_credentials()
+
+There are 2 quirks with the usage here. First for the set_network_resources
+function to work properly it **must be called before super()**. This is so
+that children classes' settings are always used instead of a parent classes'.
+The other quirk here is that if you do not want to allocate any network
+resources for your test class simply call `set_network_resources()` without
+any arguments. For example::
+
+ from tempest import test
+
+
+ class TestExampleCase(test.BaseTestCase):
+
+ @classmethod
+ def setup_credentials(cls):
+ cls.set_network_resources()
+ super(TestExampleCase, cls).setup_credentials()
+
+This will not allocate any networking resources. This is because by default all
+the arguments default to False.
+
+It's also worth pointing out that it is common for base test classes for
+different services (and scenario tests) to override this setting. When
+inheriting from classes other than the base TestCase in tempest/test.py it is
+worth checking the immediate parent for what is set to determine if your
+class needs to override that setting.
+
+Interacting with Credentials and Clients
+========================================
+
+Once you have your basic TestCase setup you'll want to start writing tests. To
+do that you need to interact with an OpenStack deployment. This section will
+cover how credentials and clients are used inside of Tempest tests.
+
+
+Manager Objects
+---------------
+
+The primary interface with which you interact with both credentials and
+API clients is the client manager object. These objects are created
+automatically by the base test class as part of credential setup (for more
+details see the previous :ref:`credentials` section). Each manager object is
+initialized with a set of credentials and has each client object already setup
+to use that set of credentials for making all the API requests. Each client is
+accessible as a top level attribute on the manager object. So to start making
+API requests you just access the client's method for making that call and the
+credentials are already setup for you. For example if you wanted to make an API
+call to create a server in Nova::
+
+ from tempest import test
+
+
+ class TestExampleCase(test.BaseTestCase):
+ def test_example_create_server(self):
+ self.os_primary.servers_client.create_server(...)
+
+is all you need to do. As described previously, in the above example the
+``self.os_primary`` is created automatically because the base test class sets the
+``credentials`` attribute to allocate a primary credential set and initializes
+the client manager as ``self.os_primary``. This same access pattern can be used
+for all of the clients in Tempest.
+
+Credentials Objects
+-------------------
+
+In certain cases you need direct access to the credentials (the most common
+use case would be an API request that takes a user or project id in the request
+body). If you're in a situation where you need to access this you'll need to
+access the ``credentials`` object which is allocated from the configured
+credential provider in the base test class. This is accessible from the manager
+object via the manager's ``credentials`` attribute. For example::
+
+ from tempest import test
+
+
+ class TestExampleCase(test.BaseTestCase):
+ def test_example_create_server(self):
+ credentials = self.os_primary.credentials
+
+The credentials object provides access to all of the credential information you
+would need to make API requests. For example, building off the previous
+example::
+
+ from tempest import test
+
+
+ class TestExampleCase(test.BaseTestCase):
+ def test_example_create_server(self):
+ credentials = self.os_primary.credentials
+ username = credentials.username
+ user_id = credentials.user_id
+ password = credentials.password
+ tenant_id = credentials.tenant_id
diff --git a/etc/accounts.yaml.sample b/etc/accounts.yaml.sample
index decc659..3dbed79 100644
--- a/etc/accounts.yaml.sample
+++ b/etc/accounts.yaml.sample
@@ -3,7 +3,25 @@
# This is required to provide isolation between test for running in parallel
#
# Valid fields for credentials are defined in the descendants of
-# auth.Credentials - see KeystoneV[2|3]Credentials.CONF_ATTRIBUTES
+# lib.auth.Credentials - see KeystoneV[2|3]Credentials.ATTRIBUTES
+#
+# The fields in KeystoneV3Credentials behave as follows:
+#
+# tenant_[id|name] also sets project_[id|name].
+#
+# project_[id|name] also sets tenant_[id|name].
+#
+# Providing distinct values for both tenant_[id|name] and project_[id|name]
+# will result in an InvalidCredentials exception.
+#
+# The value of project_domain_[id|name] is used for user_domain_[id|name] if
+# the latter is not specified.
+#
+# The value of user_domain_[id|name] is used for project_domain_[id|name] if
+# the latter is not specified.
+#
+# The value of domain_[id|name] is used for project_domain_[id|name] if not
+# specified and user_domain_[id|name] if not specified.
- username: 'user_1'
tenant_name: 'test_tenant_1'
diff --git a/etc/config-generator.tempest.conf b/etc/config-generator.tempest.conf
deleted file mode 100644
index d718f93..0000000
--- a/etc/config-generator.tempest.conf
+++ /dev/null
@@ -1,8 +0,0 @@
-[DEFAULT]
-output_file = etc/tempest.conf.sample
-namespace = tempest.config
-namespace = oslo.concurrency
-namespace = oslo.i18n
-namespace = oslo.log
-namespace = oslo.serialization
-namespace = oslo.utils
diff --git a/etc/javelin-resources.yaml.sample b/etc/javelin-resources.yaml.sample
deleted file mode 100644
index fb270a4..0000000
--- a/etc/javelin-resources.yaml.sample
+++ /dev/null
@@ -1,65 +0,0 @@
-tenants:
- - javelin
- - discuss
-
-users:
- - name: javelin
- pass: gungnir
- tenant: javelin
- - name: javelin2
- pass: gungnir2
- tenant: discuss
-
-secgroups:
- - name: secgroup1
- owner: javelin
- description: SecurityGroup1
- rules:
- - 'icmp -1 -1 0.0.0.0/0'
- - 'tcp 22 22 0.0.0.0/0'
- - name: secgroup2
- owner: javelin2
- description: SecurityGroup2
- rules:
- - 'tcp 80 80 0.0.0.0/0'
-
-images:
- - name: cirros1
- owner: javelin
- imgdir: images
- file: cirros.img
- container_format: bare
- disk_format: qcow2
- - name: cirros2
- owner: javelin2
- imgdir: files/images/cirros-0.3.2-x86_64-uec
- file: cirros-0.3.2-x86_64-blank.img
- container_format: ami
- disk_format: ami
- aki: cirros-0.3.2-x86_64-vmlinuz
- ari: cirros-0.3.2-x86_64-initrd
-
-networks:
- - name: network1
- owner: javelin
- - name: network2
- owner: javelin2
-
-subnets:
- - name: net1-subnet1
- range: 10.1.0.0/24
- network: network1
- owner: javelin
- - name: net2-subnet2
- range: 192.168.1.0/24
- network: network2
- owner: javelin2
-
-objects:
- - container: container1
- name: object1
- owner: javelin
- file: /etc/hosts
- swift_role: Member
-
-telemetry: true
\ No newline at end of file
diff --git a/etc/logging.conf.sample b/etc/logging.conf.sample
index 36cd324..c131b07 100644
--- a/etc/logging.conf.sample
+++ b/etc/logging.conf.sample
@@ -1,5 +1,5 @@
[loggers]
-keys=root,tempest_stress
+keys=root
[handlers]
keys=file,devel,syslog
@@ -11,11 +11,6 @@
level=DEBUG
handlers=file
-[logger_tempest_stress]
-level=DEBUG
-handlers=file,devel
-qualname=tempest.stress
-
[handler_file]
class=FileHandler
level=DEBUG
diff --git a/openstack-common.conf b/openstack-common.conf
deleted file mode 100644
index acb1437..0000000
--- a/openstack-common.conf
+++ /dev/null
@@ -1,9 +0,0 @@
-[DEFAULT]
-
-# The list of modules to copy from openstack-common
-module=install_venv_common
-module=with_venv
-module=install_venv
-
-# The base module to hold the copy of openstack.common
-base=tempest
diff --git a/playbooks/devstack-tempest.yaml b/playbooks/devstack-tempest.yaml
new file mode 100644
index 0000000..01155a8
--- /dev/null
+++ b/playbooks/devstack-tempest.yaml
@@ -0,0 +1,14 @@
+# Changes that run through devstack-tempest are likely to have an impact on
+# the devstack part of the job, so we keep devstack in the main play to
+# avoid zuul retrying on legitimate failures.
+- hosts: all
+ roles:
+ - orchestrate-devstack
+
+# We run tests only on one node, regardless how many nodes are in the system
+- hosts: tempest
+ roles:
+ - setup-tempest-run-dir
+ - setup-tempest-data-dir
+ - acl-devstack-files
+ - run-tempest
diff --git a/playbooks/post-tempest.yaml b/playbooks/post-tempest.yaml
new file mode 100644
index 0000000..6e0bcad
--- /dev/null
+++ b/playbooks/post-tempest.yaml
@@ -0,0 +1,6 @@
+- hosts: tempest
+ become: true
+ roles:
+ - role: fetch-subunit-output
+ zuul_work_dir: '{{ devstack_base_dir }}/tempest'
+ - role: process-stackviz
diff --git a/playbooks/tempest-scenario-multinode-lvm-multibackend/post.yaml b/playbooks/tempest-scenario-multinode-lvm-multibackend/post.yaml
new file mode 100644
index 0000000..e07f551
--- /dev/null
+++ b/playbooks/tempest-scenario-multinode-lvm-multibackend/post.yaml
@@ -0,0 +1,15 @@
+- hosts: primary
+ tasks:
+
+ - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
+ synchronize:
+ src: '{{ ansible_user_dir }}/workspace/'
+ dest: '{{ zuul.executor.log_root }}'
+ mode: pull
+ copy_links: true
+ verify_host: true
+ rsync_opts:
+ - --include=/logs/**
+ - --include=*/
+ - --exclude=*
+ - --prune-empty-dirs
diff --git a/playbooks/tempest-scenario-multinode-lvm-multibackend/run.yaml b/playbooks/tempest-scenario-multinode-lvm-multibackend/run.yaml
new file mode 100644
index 0000000..03f64f9
--- /dev/null
+++ b/playbooks/tempest-scenario-multinode-lvm-multibackend/run.yaml
@@ -0,0 +1,65 @@
+- hosts: primary
+ name: Autoconverted job tempest-scenario-multinode-lvm-multibackend
+ from old job gate-tempest-dsvm-neutron-scenario-multinode-lvm-multibackend-ubuntu-xenial-nv
+ tasks:
+
+ - name: Ensure legacy workspace directory
+ file:
+ path: '{{ ansible_user_dir }}/workspace'
+ state: directory
+
+ - shell:
+ cmd: |
+ set -e
+ set -x
+ cat > clonemap.yaml << EOF
+ clonemap:
+ - name: openstack-infra/devstack-gate
+ dest: devstack-gate
+ EOF
+ /usr/zuul-env/bin/zuul-cloner -m clonemap.yaml --cache-dir /opt/git \
+ git://git.openstack.org \
+ openstack-infra/devstack-gate
+ executable: /bin/bash
+ chdir: '{{ ansible_user_dir }}/workspace'
+ environment: '{{ zuul | zuul_legacy_vars }}'
+
+ - shell:
+ cmd: |
+ set -e
+ set -x
+ cat << 'EOF' >>"/tmp/dg-local.conf"
+ [[local|localrc]]
+ ENABLE_IDENTITY_V2=False
+ TEMPEST_USE_TEST_ACCOUNTS=True
+ # Enable lvm multiple backends to run multi backend slow scenario tests.
+ # Note: multi backend experimental job exclude the slow scenario tests.
+ CINDER_ENABLED_BACKENDS=lvm:lvmdriver-1,lvm:lvmdriver-2
+
+ EOF
+ executable: /bin/bash
+ chdir: '{{ ansible_user_dir }}/workspace'
+ environment: '{{ zuul | zuul_legacy_vars }}'
+
+ - shell:
+ cmd: |
+ set -e
+ set -x
+ export PYTHONUNBUFFERED=true
+ export DEVSTACK_GATE_TEMPEST=1
+ # Run scenario and nova migration tests with concurrency 2
+ export DEVSTACK_GATE_TEMPEST_REGEX='(^tempest\.(scenario|api\.compute\.admin\.test_(live_|)migration))'
+ export TEMPEST_CONCURRENCY=2
+ export DEVSTACK_GATE_NEUTRON=1
+ export DEVSTACK_GATE_TLSPROXY=1
+ export BRANCH_OVERRIDE=default
+ if [ "$BRANCH_OVERRIDE" != "default" ] ; then
+ export OVERRIDE_ZUUL_BRANCH=$BRANCH_OVERRIDE
+ fi
+ export DEVSTACK_GATE_TOPOLOGY="multinode"
+
+ cp devstack-gate/devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh
+ ./safe-devstack-vm-gate-wrap.sh
+ executable: /bin/bash
+ chdir: '{{ ansible_user_dir }}/workspace'
+ environment: '{{ zuul | zuul_legacy_vars }}'
diff --git a/releasenotes/notes/10/10.0-supported-openstack-releases-b88db468695348f6.yaml b/releasenotes/notes/10/10.0-supported-openstack-releases-b88db468695348f6.yaml
new file mode 100644
index 0000000..217c2f6
--- /dev/null
+++ b/releasenotes/notes/10/10.0-supported-openstack-releases-b88db468695348f6.yaml
@@ -0,0 +1,13 @@
+---
+other:
+ - OpenStack Releases Supported at this time are the same as in the
+ previous release 9,
+ **Kilo** and
+ **Liberty**.
+
+
+ The release under current development as of this tag is Mitaka,
+ meaning that every Tempest commit is also tested against master during
+ the Mitaka cycle. However, this does not necessarily mean that using
+ Tempest as of this tag will work against a Mitaka (or future releases)
+ cloud.
diff --git a/releasenotes/notes/10/10.0.0-Tempest-library-interface-0eb680b810139a50.yaml b/releasenotes/notes/10/10.0.0-Tempest-library-interface-0eb680b810139a50.yaml
new file mode 100644
index 0000000..c1edd63
--- /dev/null
+++ b/releasenotes/notes/10/10.0.0-Tempest-library-interface-0eb680b810139a50.yaml
@@ -0,0 +1,11 @@
+---
+prelude: |
+ This release includes the addition of the stable library interface for
+ tempest. This behaves just as tempest-lib did prior to this, but instead
+ it lives directly in the tempest project. For more information refer to
+ the `library docs`_.
+
+ .. _library docs: https://docs.openstack.org/tempest/latest/library.html#current-library-apis
+
+features:
+ - Tempest library interface
diff --git a/releasenotes/notes/10/10.0.0-start-using-reno-ed9518126fd0e1a3.yaml b/releasenotes/notes/10/10.0.0-start-using-reno-ed9518126fd0e1a3.yaml
new file mode 100644
index 0000000..0bc9af5
--- /dev/null
+++ b/releasenotes/notes/10/10.0.0-start-using-reno-ed9518126fd0e1a3.yaml
@@ -0,0 +1,3 @@
+---
+other:
+ - Start using reno for managing release notes.
diff --git a/releasenotes/notes/11/11.0.0-api-microversion-testing-support-2ceddd2255670932.yaml b/releasenotes/notes/11/11.0.0-api-microversion-testing-support-2ceddd2255670932.yaml
new file mode 100644
index 0000000..e98671a
--- /dev/null
+++ b/releasenotes/notes/11/11.0.0-api-microversion-testing-support-2ceddd2255670932.yaml
@@ -0,0 +1,3 @@
+---
+features:
+ - Tempest library interface addition(API Microversion testing interfaces).
\ No newline at end of file
diff --git a/releasenotes/notes/11/11.0.0-compute-microversion-support-e0b23f960f894b9b.yaml b/releasenotes/notes/11/11.0.0-compute-microversion-support-e0b23f960f894b9b.yaml
new file mode 100644
index 0000000..de1b35e
--- /dev/null
+++ b/releasenotes/notes/11/11.0.0-compute-microversion-support-e0b23f960f894b9b.yaml
@@ -0,0 +1,3 @@
+---
+features:
+ - Compute Microversion testing support in Service Clients.
diff --git a/releasenotes/notes/11/11.0.0-supported-openstack-releases-1e5d7295d939d439.yaml b/releasenotes/notes/11/11.0.0-supported-openstack-releases-1e5d7295d939d439.yaml
new file mode 100644
index 0000000..09ff15d
--- /dev/null
+++ b/releasenotes/notes/11/11.0.0-supported-openstack-releases-1e5d7295d939d439.yaml
@@ -0,0 +1,12 @@
+---
+prelude: >
+ This release is marking the start of Mitaka release support in tempest
+other:
+ - OpenStack Releases Supported at this time are **Kilo**, **Liberty**,
+ **Mitaka**
+
+ The release under current development as of this tag is Newton,
+ meaning that every Tempest commit is also tested against master during
+ the Newton cycle. However, this does not necessarily mean that using
+ Tempest as of this tag will work against a Newton (or future releases)
+ cloud.
diff --git a/releasenotes/notes/12/12.0.0-supported-openstack-releases-f10aac381d933dd1.yaml b/releasenotes/notes/12/12.0.0-supported-openstack-releases-f10aac381d933dd1.yaml
new file mode 100644
index 0000000..9baf035
--- /dev/null
+++ b/releasenotes/notes/12/12.0.0-supported-openstack-releases-f10aac381d933dd1.yaml
@@ -0,0 +1,12 @@
+---
+prelude: >
+ This release is marking the end of Kilo release support in Tempest
+other:
+ - OpenStack Releases Supported after this release are **Liberty**
+ and **Mitaka**
+
+ The release under current development as of this tag is Newton,
+ meaning that every Tempest commit is also tested against master during
+ the Newton cycle. However, this does not necessarily mean that using
+ Tempest as of this tag will work against a Newton (or future releases)
+ cloud.
diff --git a/releasenotes/notes/12/12.1.0-add-network-versions-client-d90e8334e1443f5c.yaml b/releasenotes/notes/12/12.1.0-add-network-versions-client-d90e8334e1443f5c.yaml
new file mode 100644
index 0000000..07e3151
--- /dev/null
+++ b/releasenotes/notes/12/12.1.0-add-network-versions-client-d90e8334e1443f5c.yaml
@@ -0,0 +1,4 @@
+---
+features:
+ - Adds a network version client for querying
+ Neutron's API version discovery URL ("GET /").
diff --git a/releasenotes/notes/12/12.1.0-add-scope-to-auth-b5a82493ea89f41e.yaml b/releasenotes/notes/12/12.1.0-add-scope-to-auth-b5a82493ea89f41e.yaml
new file mode 100644
index 0000000..297279f
--- /dev/null
+++ b/releasenotes/notes/12/12.1.0-add-scope-to-auth-b5a82493ea89f41e.yaml
@@ -0,0 +1,7 @@
+---
+features:
+ - Tempest library auth interface now supports scope. Scope allows to control
+ the scope of tokens requested via the identity API. Identity V2 supports
+ unscoped and project scoped tokens, but only the latter are implemented.
+ Identity V3 supports unscoped, project and domain scoped token, all three
+ are available.
\ No newline at end of file
diff --git a/releasenotes/notes/12/12.1.0-add-tempest-run-3d0aaf69c2ca4115.yaml b/releasenotes/notes/12/12.1.0-add-tempest-run-3d0aaf69c2ca4115.yaml
new file mode 100644
index 0000000..429bf52
--- /dev/null
+++ b/releasenotes/notes/12/12.1.0-add-tempest-run-3d0aaf69c2ca4115.yaml
@@ -0,0 +1,4 @@
+---
+features:
+ - Adds the tempest run command to the unified tempest CLI. This new command
+ is used for running tempest tests.
diff --git a/releasenotes/notes/12/12.1.0-add-tempest-workspaces-228a2ba4690b5589.yaml b/releasenotes/notes/12/12.1.0-add-tempest-workspaces-228a2ba4690b5589.yaml
new file mode 100644
index 0000000..9a1cef6
--- /dev/null
+++ b/releasenotes/notes/12/12.1.0-add-tempest-workspaces-228a2ba4690b5589.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - Adds tempest workspaces command and WorkspaceManager.
+ This is used to have a centralized repository for managing
+ different tempest configurations.
diff --git a/releasenotes/notes/12/12.1.0-add_subunit_describe_calls-5498a37e6cd66c4b.yaml b/releasenotes/notes/12/12.1.0-add_subunit_describe_calls-5498a37e6cd66c4b.yaml
new file mode 100644
index 0000000..092014e
--- /dev/null
+++ b/releasenotes/notes/12/12.1.0-add_subunit_describe_calls-5498a37e6cd66c4b.yaml
@@ -0,0 +1,8 @@
+---
+features:
+ - |
+ Adds subunit-describe-calls. A parser for subunit streams to determine what
+ REST API calls are made inside of a test and in what order they are called.
+
+ * Input can be piped in or a file can be specified
+ * Output is shortened for stdout, the output file has more information
diff --git a/releasenotes/notes/12/12.1.0-bug-1486834-7ebca15836ae27a9.yaml b/releasenotes/notes/12/12.1.0-bug-1486834-7ebca15836ae27a9.yaml
new file mode 100644
index 0000000..b2190f3
--- /dev/null
+++ b/releasenotes/notes/12/12.1.0-bug-1486834-7ebca15836ae27a9.yaml
@@ -0,0 +1,7 @@
+---
+features:
+ - |
+ Tempest library auth interface now supports
+ filtering with catalog name. Note that filtering by
+ name is only successful if a known service type is
+ provided.
diff --git a/releasenotes/notes/12/12.1.0-identity-clients-as-library-e663c6132fcac6c2.yaml b/releasenotes/notes/12/12.1.0-identity-clients-as-library-e663c6132fcac6c2.yaml
new file mode 100644
index 0000000..f9173a0
--- /dev/null
+++ b/releasenotes/notes/12/12.1.0-identity-clients-as-library-e663c6132fcac6c2.yaml
@@ -0,0 +1,13 @@
+---
+features:
+ - |
+ Define identity service clients as libraries
+ The following identity service clients are defined as library interface,
+ so the other projects can use these modules as stable libraries without
+ any maintenance changes.
+
+ * endpoints_client(v2)
+ * roles_client(v2)
+ * services_client(v2)
+ * tenants_client(v2)
+ * users_client(v2)
diff --git a/releasenotes/notes/12/12.1.0-image-clients-as-library-86d17caa26ce3961.yaml b/releasenotes/notes/12/12.1.0-image-clients-as-library-86d17caa26ce3961.yaml
new file mode 100644
index 0000000..1fa4ddd
--- /dev/null
+++ b/releasenotes/notes/12/12.1.0-image-clients-as-library-86d17caa26ce3961.yaml
@@ -0,0 +1,15 @@
+---
+features:
+ - |
+ Define image service clients as libraries
+ The following image service clients are defined as library interface,
+ so the other projects can use these modules as stable libraries
+ without any maintenance changes.
+
+ * image_members_client(v1)
+ * images_client(v1)
+ * image_members_client(v2)
+ * images_client(v2)
+ * namespaces_client(v2)
+ * resource_types_client(v2)
+ * schemas_client(v2)
diff --git a/releasenotes/notes/12/12.1.0-new-test-utils-module-adf34468c4d52719.yaml b/releasenotes/notes/12/12.1.0-new-test-utils-module-adf34468c4d52719.yaml
new file mode 100644
index 0000000..55df2b3
--- /dev/null
+++ b/releasenotes/notes/12/12.1.0-new-test-utils-module-adf34468c4d52719.yaml
@@ -0,0 +1,11 @@
+---
+features:
+ - A new `test_utils` module has been added to tempest.lib.common.utils. It
+ should hold any common utility functions that help writing Tempest tests.
+ - A new utility function called `call_and_ignore_notfound_exc` has been
+ added to the `test_utils` module. That function call another function
+ passed as parameter and ignore the NotFound exception if it raised.
+deprecations:
+ - tempest.lib.common.utils.misc.find_test_caller has been moved into the
+ tempest.lib.common.utils.test_utils module. Calling the find_test_caller
+ function with its old location is deprecated.
diff --git a/releasenotes/notes/12/12.1.0-remove-input-scenarios-functionality-01308e6d4307f580.yaml b/releasenotes/notes/12/12.1.0-remove-input-scenarios-functionality-01308e6d4307f580.yaml
new file mode 100644
index 0000000..4ee883f
--- /dev/null
+++ b/releasenotes/notes/12/12.1.0-remove-input-scenarios-functionality-01308e6d4307f580.yaml
@@ -0,0 +1,11 @@
+---
+upgrade:
+ - The input scenarios functionality no longer exists in tempest. This caused
+ a large number of issues for limited benefit and was only used by a single
+ test, test_server_basic_ops. If you were using this functionality you'll
+ now have to do it manually with a script and/or tempest workspaces
+deprecations:
+ - All the options in the input-scenario group are now deprecated. These were
+ only used in tree by the now removed input scenarios functionality in
+ test_server_basic_ops. They were only deprecated because there could be
+ external consumers via plugins. They will be removed during the Ocata cycle.
diff --git a/releasenotes/notes/12/12.1.0-remove-integrated-horizon-bb57551c1e5f5be3.yaml b/releasenotes/notes/12/12.1.0-remove-integrated-horizon-bb57551c1e5f5be3.yaml
new file mode 100644
index 0000000..294f6d9
--- /dev/null
+++ b/releasenotes/notes/12/12.1.0-remove-integrated-horizon-bb57551c1e5f5be3.yaml
@@ -0,0 +1,7 @@
+---
+upgrade:
+ - The integrated dashboard scenario test has been
+ removed and is now in a separate tempest plugin
+ tempest-horizon. The removed test coverage can be
+ used by installing tempest-horizon on the server
+ where you run tempest.
diff --git a/releasenotes/notes/12/12.1.0-remove-legacy-credential-providers-3d653ac3ba1ada2b.yaml b/releasenotes/notes/12/12.1.0-remove-legacy-credential-providers-3d653ac3ba1ada2b.yaml
new file mode 100644
index 0000000..89b3f41
--- /dev/null
+++ b/releasenotes/notes/12/12.1.0-remove-legacy-credential-providers-3d653ac3ba1ada2b.yaml
@@ -0,0 +1,5 @@
+---
+upgrade:
+ - The deprecated legacy credential provider has been removed. The only way to
+ configure credentials in tempest now is to use the dynamic or preprovisioned
+ credential providers
diff --git a/releasenotes/notes/12/12.1.0-remove-trove-tests-666522e9113549f9.yaml b/releasenotes/notes/12/12.1.0-remove-trove-tests-666522e9113549f9.yaml
new file mode 100644
index 0000000..7a1fc36
--- /dev/null
+++ b/releasenotes/notes/12/12.1.0-remove-trove-tests-666522e9113549f9.yaml
@@ -0,0 +1,4 @@
+---
+upgrade:
+ - All tests for the Trove project have been removed from tempest. They now
+ live as a tempest plugin in the trove project.
diff --git a/releasenotes/notes/12/12.1.0-routers-client-as-library-25a363379da351f6.yaml b/releasenotes/notes/12/12.1.0-routers-client-as-library-25a363379da351f6.yaml
new file mode 100644
index 0000000..35cf2c4
--- /dev/null
+++ b/releasenotes/notes/12/12.1.0-routers-client-as-library-25a363379da351f6.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - Define routers_client as stable library interface.
+ The routers_client module is defined as library interface,
+ so the other projects can use the module as stable library
+ without any maintenance changes.
diff --git a/releasenotes/notes/12/12.1.0-support-chunked-encoding-d71f53225f68edf3.yaml b/releasenotes/notes/12/12.1.0-support-chunked-encoding-d71f53225f68edf3.yaml
new file mode 100644
index 0000000..eb45523
--- /dev/null
+++ b/releasenotes/notes/12/12.1.0-support-chunked-encoding-d71f53225f68edf3.yaml
@@ -0,0 +1,9 @@
+---
+features:
+ - The RestClient (in tempest.lib.common.rest_client) now supports POSTing
+ and PUTing data with chunked transfer encoding. Just pass an `iterable`
+ object as the `body` argument and set the `chunked` argument to `True`.
+ - A new generator called `chunkify` is added in
+ tempest.lib.common.utils.data_utils that yields fixed-size chunks (slices)
+ from a Python sequence.
+
diff --git a/releasenotes/notes/12/12.1.0-tempest-init-global-config-dir-location-changes-12260255871d3a2b.yaml b/releasenotes/notes/12/12.1.0-tempest-init-global-config-dir-location-changes-12260255871d3a2b.yaml
new file mode 100644
index 0000000..eeda921
--- /dev/null
+++ b/releasenotes/notes/12/12.1.0-tempest-init-global-config-dir-location-changes-12260255871d3a2b.yaml
@@ -0,0 +1,12 @@
+---
+upgrade:
+ - The location on disk that the *tempest init* command looks for has changed.
+ Previously it would attempt to use python packaging's data files to guess
+ where setuptools/distutils were installing data files, which was incredibly
+ unreliable and depended on how you installed tempest and which versions of
+ setuptools, distutils, and python you had installed. Instead, now it will
+ use either /etc/tempest, $XDG_CONFIG_PATH/.config/tempest, or
+ ~/.tempest/etc (attempted in that order). If none of these exist it will
+ create an empty ~/.tempest/etc directory. If you were relying on the
+ previous behavior and none of these directories were being used you will
+ need to move the files to live in one of these directories.
diff --git a/releasenotes/notes/12/12.2.0-add-httptimeout-in-restclient-ax78061900e3f3d7.yaml b/releasenotes/notes/12/12.2.0-add-httptimeout-in-restclient-ax78061900e3f3d7.yaml
new file mode 100644
index 0000000..a360f8e
--- /dev/null
+++ b/releasenotes/notes/12/12.2.0-add-httptimeout-in-restclient-ax78061900e3f3d7.yaml
@@ -0,0 +1,7 @@
+---
+features:
+ - RestClient now supports setting timeout in urllib3.poolmanager.
+ Clients will use CONF.service_clients.http_timeout for timeout
+ value to wait for http request to response.
+ - KeystoneAuthProvider will accept http_timeout and will use it in
+ get_credentials.
diff --git a/releasenotes/notes/12/12.2.0-add-new-identity-clients-3c3afd674a395bde.yaml b/releasenotes/notes/12/12.2.0-add-new-identity-clients-3c3afd674a395bde.yaml
new file mode 100644
index 0000000..3ec8b56
--- /dev/null
+++ b/releasenotes/notes/12/12.2.0-add-new-identity-clients-3c3afd674a395bde.yaml
@@ -0,0 +1,13 @@
+---
+features:
+ - |
+ Define identity service clients as libraries.
+ The following identity service clients are defined as library interface,
+ so the other projects can use these modules as stable libraries without
+ any maintenance changes.
+
+ * endpoints_client(v3)
+ * policies_client (v3)
+ * regions_client(v3)
+ * services_client(v3)
+ * projects_client(v3)
diff --git a/releasenotes/notes/12/12.2.0-clients_module-16f3025f515bf9ec.yaml b/releasenotes/notes/12/12.2.0-clients_module-16f3025f515bf9ec.yaml
new file mode 100644
index 0000000..d07448a
--- /dev/null
+++ b/releasenotes/notes/12/12.2.0-clients_module-16f3025f515bf9ec.yaml
@@ -0,0 +1,18 @@
+---
+features:
+ - The Tempest plugin interface contains a new optional method, which allows
+ plugins to declare and automatically register any service client defined
+ in the plugin.
+ - tempest.lib exposes a new stable interface, the clients module and
+ ServiceClients class, which provides a convenient way for plugin tests to
+ access service clients defined in Tempest as well as service clients
+ defined in all loaded plugins.
+ The new ServiceClients class only exposes for now the service clients
+ which are in tempest.lib, i.e. compute, network and image. The remaining
+ service clients (identity, volume and object-storage) will be added in
+ future updates.
+deprecations:
+ - The new clients module provides a stable alternative to tempest classes
+ manager.Manager and clients.Manager. manager.Manager only exists now
+ to smoothen the transition of plugins to the new interface, but it will
+ be removed shortly without further notice.
diff --git a/releasenotes/notes/12/12.2.0-nova_cert_default-90eb7c1e3cde624a.yaml b/releasenotes/notes/12/12.2.0-nova_cert_default-90eb7c1e3cde624a.yaml
new file mode 100644
index 0000000..cfe97c5
--- /dev/null
+++ b/releasenotes/notes/12/12.2.0-nova_cert_default-90eb7c1e3cde624a.yaml
@@ -0,0 +1,8 @@
+---
+upgrade:
+
+ - The ``nova_cert`` option default is changed to ``False``. The nova
+ certification management APIs were a hold over from ec2, and are
+ not used by any other parts of nova. They are deprecated for
+ removal in nova after the newton release. This makes false a more
+ sensible default going forward.
\ No newline at end of file
diff --git a/releasenotes/notes/12/12.2.0-plugin-service-client-registration-00b19a2dd4935ba0.yaml b/releasenotes/notes/12/12.2.0-plugin-service-client-registration-00b19a2dd4935ba0.yaml
new file mode 100644
index 0000000..64f729a
--- /dev/null
+++ b/releasenotes/notes/12/12.2.0-plugin-service-client-registration-00b19a2dd4935ba0.yaml
@@ -0,0 +1,12 @@
+---
+features:
+ - A new optional interface `TempestPlugin.get_service_clients`
+ is available to plugins. It allows them to declare
+ any service client they implement. For now this is used by
+ tempest only, for auto-registration of service clients
+ in the new class `ServiceClients`.
+ - A new singleton class `clients.ClientsRegistry` is
+ available. It holds the service clients registration data
+ from all plugins. It is used by `ServiceClients` for
+ auto-registration of the service clients implemented
+ in plugins.
diff --git a/releasenotes/notes/12/12.2.0-remove-javelin-276f62d04f7e4a1d.yaml b/releasenotes/notes/12/12.2.0-remove-javelin-276f62d04f7e4a1d.yaml
new file mode 100644
index 0000000..8e893b8
--- /dev/null
+++ b/releasenotes/notes/12/12.2.0-remove-javelin-276f62d04f7e4a1d.yaml
@@ -0,0 +1,5 @@
+---
+upgrade:
+ - The previously deprecated Javelin utility has been removed from Tempest.
+ As an alternative Ansible can be used to construct similar yaml workflows
+ to what Javelin used to provide.
diff --git a/releasenotes/notes/12/12.2.0-service_client_config-8a1d7b4de769c633.yaml b/releasenotes/notes/12/12.2.0-service_client_config-8a1d7b4de769c633.yaml
new file mode 100644
index 0000000..985acb0
--- /dev/null
+++ b/releasenotes/notes/12/12.2.0-service_client_config-8a1d7b4de769c633.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - A new helper method `service_client_config` has been added
+ to the stable module config.py that returns extracts from
+ configuration into a dictionary the configuration settings
+ relevant for the initialization of a service client.
diff --git a/releasenotes/notes/12/12.2.0-volume-clients-as-library-9a3444dd63c134b3.yaml b/releasenotes/notes/12/12.2.0-volume-clients-as-library-9a3444dd63c134b3.yaml
new file mode 100644
index 0000000..cf504ad
--- /dev/null
+++ b/releasenotes/notes/12/12.2.0-volume-clients-as-library-9a3444dd63c134b3.yaml
@@ -0,0 +1,18 @@
+---
+features:
+ - |
+ Define volume service clients as libraries
+ The following volume service clients are defined as library interface,
+ so the other projects can use these modules as stable libraries
+ without any maintenance changes.
+
+ * availability_zone_client(v1)
+ * availability_zone_client(v2)
+ * extensions_client(v1)
+ * extensions_client(v2)
+ * hosts_client(v1)
+ * hosts_client(v2)
+ * quotas_client(v1)
+ * quotas_client(v2)
+ * services_client(v1)
+ * services_client(v2)
diff --git a/releasenotes/notes/13/13.0.0-add-new-identity-clients-as-library-5f7ndha733nwdsn9.yaml b/releasenotes/notes/13/13.0.0-add-new-identity-clients-as-library-5f7ndha733nwdsn9.yaml
new file mode 100644
index 0000000..9e828f6
--- /dev/null
+++ b/releasenotes/notes/13/13.0.0-add-new-identity-clients-as-library-5f7ndha733nwdsn9.yaml
@@ -0,0 +1,15 @@
+---
+features:
+ - |
+ Define identity service clients as libraries.
+ Add new service clients to the library interface so the other projects can use these modules as stable libraries without
+ any maintenance changes.
+
+ * identity_client(v2)
+ * groups_client(v3)
+ * trusts_client(v3)
+ * users_client(v3)
+ * identity_client(v3)
+ * roles_client(v3)
+ * inherited_roles_client(v3)
+ * credentials_client(v3)
diff --git a/releasenotes/notes/13/13.0.0-add-volume-clients-as-a-library-d05b6bc35e66c6ef.yaml b/releasenotes/notes/13/13.0.0-add-volume-clients-as-a-library-d05b6bc35e66c6ef.yaml
new file mode 100644
index 0000000..9cfce0d
--- /dev/null
+++ b/releasenotes/notes/13/13.0.0-add-volume-clients-as-a-library-d05b6bc35e66c6ef.yaml
@@ -0,0 +1,16 @@
+---
+features:
+ - |
+ Define volume service clients as libraries.
+ The following volume service clients are defined as library interface,
+ so the other projects can use these modules as stable libraries without
+ any maintenance changes.
+
+ * backups_client
+ * encryption_types_client (v1)
+ * encryption_types_client (v2)
+ * qos_clients (v1)
+ * qos_clients (v2)
+ * snapshots_client (v1)
+ * snapshots_client (v2)
+
diff --git a/releasenotes/notes/13/13.0.0-deprecate-get_ipv6_addr_by_EUI64-4673f07677289cf6.yaml b/releasenotes/notes/13/13.0.0-deprecate-get_ipv6_addr_by_EUI64-4673f07677289cf6.yaml
new file mode 100644
index 0000000..0884cfa
--- /dev/null
+++ b/releasenotes/notes/13/13.0.0-deprecate-get_ipv6_addr_by_EUI64-4673f07677289cf6.yaml
@@ -0,0 +1,4 @@
+---
+deprecations:
+ - Oslo.utils provides same method get_ipv6_addr_by_EUI64,
+ so deprecate it in Newton and remove it in Ocata.
diff --git a/releasenotes/notes/13/13.0.0-move-call-until-true-to-tempest-lib-c9ea70dd6fe9bd15.yaml b/releasenotes/notes/13/13.0.0-move-call-until-true-to-tempest-lib-c9ea70dd6fe9bd15.yaml
new file mode 100644
index 0000000..52c04af
--- /dev/null
+++ b/releasenotes/notes/13/13.0.0-move-call-until-true-to-tempest-lib-c9ea70dd6fe9bd15.yaml
@@ -0,0 +1,5 @@
+---
+deprecations:
+ - The ``call_until_true`` function is moved from the ``tempest.test`` module
+ to the ``tempest.lib.common.utils.test_utils`` module. Backward
+ compatibility is preserved until Ocata.
diff --git a/releasenotes/notes/13/13.0.0-start-of-newton-support-3ebb274f300f28eb.yaml b/releasenotes/notes/13/13.0.0-start-of-newton-support-3ebb274f300f28eb.yaml
new file mode 100644
index 0000000..b9b6fb5
--- /dev/null
+++ b/releasenotes/notes/13/13.0.0-start-of-newton-support-3ebb274f300f28eb.yaml
@@ -0,0 +1,13 @@
+---
+prelude: >
+ This release is marking the start of Newton release support in Tempest
+other:
+ - |
+ OpenStack releases supported at this time are **Liberty**, **Mitaka**,
+ and **Newton**.
+
+ The release under current development as of this tag is Ocata,
+ meaning that every Tempest commit is also tested against master during
+ the Ocata cycle. However, this does not necessarily mean that using
+ Tempest as of this tag will work against a Ocata (or future releases)
+ cloud.
diff --git a/releasenotes/notes/13/13.0.0-tempest-cleanup-nostandalone-39df2aafb2545d35.yaml b/releasenotes/notes/13/13.0.0-tempest-cleanup-nostandalone-39df2aafb2545d35.yaml
new file mode 100644
index 0000000..813e47f
--- /dev/null
+++ b/releasenotes/notes/13/13.0.0-tempest-cleanup-nostandalone-39df2aafb2545d35.yaml
@@ -0,0 +1,5 @@
+---
+upgrade:
+ - the already deprecated tempest-cleanup standalone command has been
+ removed. The corresponding functionalities can be accessed through
+ the unified `tempest` command (`tempest cleanup`).
diff --git a/releasenotes/notes/13/13.0.0-volume-clients-as-library-660811011be29d1a.yaml b/releasenotes/notes/13/13.0.0-volume-clients-as-library-660811011be29d1a.yaml
new file mode 100644
index 0000000..9e9eff6
--- /dev/null
+++ b/releasenotes/notes/13/13.0.0-volume-clients-as-library-660811011be29d1a.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Define the v1 and v2 types_client clients for the volume service as
+ library interfaces, allowing other projects to use these modules as
+ stable libraries without maintenance changes.
diff --git a/releasenotes/notes/14/14.0.0-add-cred-provider-abstract-class-to-lib-70ff513221f8a871.yaml b/releasenotes/notes/14/14.0.0-add-cred-provider-abstract-class-to-lib-70ff513221f8a871.yaml
new file mode 100644
index 0000000..6f7a411
--- /dev/null
+++ b/releasenotes/notes/14/14.0.0-add-cred-provider-abstract-class-to-lib-70ff513221f8a871.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - The cred_provider abstract class which serves as the basis for both
+ of tempest's cred providers, pre-provisioned credentials and dynamic
+ credentials, is now a library interface. This provides the common signature
+ required for building a credential provider.
diff --git a/releasenotes/notes/14/14.0.0-add-cred_client-to-tempest.lib-4d4af33f969c576f.yaml b/releasenotes/notes/14/14.0.0-add-cred_client-to-tempest.lib-4d4af33f969c576f.yaml
new file mode 100644
index 0000000..432a6b1
--- /dev/null
+++ b/releasenotes/notes/14/14.0.0-add-cred_client-to-tempest.lib-4d4af33f969c576f.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - The cred_client module was added to tempest.lib. This module provides a
+ wrapper to the keystone services client which provides a uniform
+ interface that abstracts out the differences between keystone api versions.
diff --git a/releasenotes/notes/14/14.0.0-add-error-code-translation-to-versions-clients-acbc78292e24b014.yaml b/releasenotes/notes/14/14.0.0-add-error-code-translation-to-versions-clients-acbc78292e24b014.yaml
new file mode 100644
index 0000000..57bf47c
--- /dev/null
+++ b/releasenotes/notes/14/14.0.0-add-error-code-translation-to-versions-clients-acbc78292e24b014.yaml
@@ -0,0 +1,6 @@
+---
+upgrade:
+ - Add an error translation to list_versions() of versions_client of both
+ compute and network. This can affect users who are expecting that these
+ clients return error status code instead of the exception. It is needed
+ to change the code for handling the exception like the other clients code.
diff --git a/releasenotes/notes/14/14.0.0-add-image-clients-af94564fb34ddca6.yaml b/releasenotes/notes/14/14.0.0-add-image-clients-af94564fb34ddca6.yaml
new file mode 100644
index 0000000..7e40fd4
--- /dev/null
+++ b/releasenotes/notes/14/14.0.0-add-image-clients-af94564fb34ddca6.yaml
@@ -0,0 +1,9 @@
+---
+features:
+ - |
+ As in the [doc]:
+ http://developer.openstack.org/api-ref/image/v2/metadefs-index.html,
+ there are some apis are not included, add them.
+
+ * namespace_properties_client(v2)
+
diff --git a/releasenotes/notes/14/14.0.0-add-role-assignments-client-as-a-library-d34b4fdf376984ad.yaml b/releasenotes/notes/14/14.0.0-add-role-assignments-client-as-a-library-d34b4fdf376984ad.yaml
new file mode 100644
index 0000000..a1edcc5
--- /dev/null
+++ b/releasenotes/notes/14/14.0.0-add-role-assignments-client-as-a-library-d34b4fdf376984ad.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - Define the identity service role_assignments_client as a library.
+ Add role_assignments_client to the library interface so the other
+ projects can use this module as a stable library without any
+ maintenance changes.
diff --git a/releasenotes/notes/14/14.0.0-add-service-provider-client-cbba77d424a30dd3.yaml b/releasenotes/notes/14/14.0.0-add-service-provider-client-cbba77d424a30dd3.yaml
new file mode 100644
index 0000000..b504c78
--- /dev/null
+++ b/releasenotes/notes/14/14.0.0-add-service-provider-client-cbba77d424a30dd3.yaml
@@ -0,0 +1,4 @@
+---
+features:
+ - A Neutron Service Providers client is added to deal with resources
+ of the '/service-providers' route.
diff --git a/releasenotes/notes/14/14.0.0-add-ssh-port-parameter-to-client-6d16c374ac4456c1.yaml b/releasenotes/notes/14/14.0.0-add-ssh-port-parameter-to-client-6d16c374ac4456c1.yaml
new file mode 100644
index 0000000..b2ad199
--- /dev/null
+++ b/releasenotes/notes/14/14.0.0-add-ssh-port-parameter-to-client-6d16c374ac4456c1.yaml
@@ -0,0 +1,4 @@
+---
+features:
+ - A new optional parameter `port` for ssh client (`tempest.lib.common.ssh.Client`)
+ to specify destination port for a host. The default value is 22.
diff --git a/releasenotes/notes/14/14.0.0-deprecate-nova-api-extensions-df16b02485dae203.yaml b/releasenotes/notes/14/14.0.0-deprecate-nova-api-extensions-df16b02485dae203.yaml
new file mode 100644
index 0000000..c2d9a9b
--- /dev/null
+++ b/releasenotes/notes/14/14.0.0-deprecate-nova-api-extensions-df16b02485dae203.yaml
@@ -0,0 +1,7 @@
+---
+deprecations:
+ - The *api_extensions* config option in the *compute-feature-enabled* group is
+ now deprecated. This option will be removed from tempest when all the
+ OpenStack releases supported by tempest no longer support the API extensions
+ mechanism. This was removed from Nova during the Newton cycle, so this will
+ be removed at the Mitaka EOL.
diff --git a/releasenotes/notes/14/14.0.0-move-cinder-v3-to-lib-service-be3ba0c20753b594.yaml b/releasenotes/notes/14/14.0.0-move-cinder-v3-to-lib-service-be3ba0c20753b594.yaml
new file mode 100644
index 0000000..9223ba5
--- /dev/null
+++ b/releasenotes/notes/14/14.0.0-move-cinder-v3-to-lib-service-be3ba0c20753b594.yaml
@@ -0,0 +1,7 @@
+features:
+ - |
+ Define the Volume v3 service clients as library interfaces,
+ allowing other projects to use these modules as stable
+ libraries without maintenance changes.
+
+ * messages_client(v3)
diff --git a/releasenotes/notes/14/14.0.0-new-volume-limit-client-517c17d9090f4df4.yaml b/releasenotes/notes/14/14.0.0-new-volume-limit-client-517c17d9090f4df4.yaml
new file mode 100644
index 0000000..033e147
--- /dev/null
+++ b/releasenotes/notes/14/14.0.0-new-volume-limit-client-517c17d9090f4df4.yaml
@@ -0,0 +1,3 @@
+---
+features:
+ - The volume_limits client was added to tempest.lib.
diff --git a/releasenotes/notes/14/14.0.0-remo-stress-tests-81052b211ad95d2e.yaml b/releasenotes/notes/14/14.0.0-remo-stress-tests-81052b211ad95d2e.yaml
new file mode 100644
index 0000000..389b29f
--- /dev/null
+++ b/releasenotes/notes/14/14.0.0-remo-stress-tests-81052b211ad95d2e.yaml
@@ -0,0 +1,13 @@
+---
+prelude: >
+ This release is marking the end of Liberty release support in Tempest
+upgrade:
+ - The Stress tests framework and all the stress tests have been removed.
+other:
+ - |
+ OpenStack releases supported at this time are **Mitaka** and **Newton**.
+
+ The release under current development as of this tag is Ocata, meaning that
+ every Tempest commit is also tested against master during the Ocata cycle.
+ However, this does not necessarily mean that using Tempest as of this tag
+ will work against a Ocata (or future releases) cloud.
diff --git a/releasenotes/notes/14/14.0.0-remove-baremetal-tests-65186d9e15d5b8fb.yaml b/releasenotes/notes/14/14.0.0-remove-baremetal-tests-65186d9e15d5b8fb.yaml
new file mode 100644
index 0000000..ca2635e
--- /dev/null
+++ b/releasenotes/notes/14/14.0.0-remove-baremetal-tests-65186d9e15d5b8fb.yaml
@@ -0,0 +1,4 @@
+---
+upgrade:
+ - All tests for the Ironic project have been removed from Tempest. Those
+ exist as a Tempest plugin in the Ironic project.
diff --git a/releasenotes/notes/14/14.0.0-remove-bootable-option-024f8944c056a3e0.yaml b/releasenotes/notes/14/14.0.0-remove-bootable-option-024f8944c056a3e0.yaml
new file mode 100644
index 0000000..e6e53af
--- /dev/null
+++ b/releasenotes/notes/14/14.0.0-remove-bootable-option-024f8944c056a3e0.yaml
@@ -0,0 +1,6 @@
+---
+deprecations:
+ - The *bootable* config option in the *volume_feature_enabled* group is
+ removed because the corresponding feature os-set_bootable has been
+ implemented 2.5 years ago and all OpenStack versions which are supported
+ by Tempest should support the feature.
diff --git a/releasenotes/notes/14/14.0.0-remove-negative-test-generator-1653f4c0f86ccf75.yaml b/releasenotes/notes/14/14.0.0-remove-negative-test-generator-1653f4c0f86ccf75.yaml
new file mode 100644
index 0000000..a734d15
--- /dev/null
+++ b/releasenotes/notes/14/14.0.0-remove-negative-test-generator-1653f4c0f86ccf75.yaml
@@ -0,0 +1,4 @@
+---
+upgrade:
+ - The Negative Tests Generator has been removed (it was not used by any
+ Tempest tests).
diff --git a/releasenotes/notes/14/14.0.0-remove-sahara-tests-1532c47c7df80e3a.yaml b/releasenotes/notes/14/14.0.0-remove-sahara-tests-1532c47c7df80e3a.yaml
new file mode 100644
index 0000000..b541cf9
--- /dev/null
+++ b/releasenotes/notes/14/14.0.0-remove-sahara-tests-1532c47c7df80e3a.yaml
@@ -0,0 +1,4 @@
+---
+upgrade:
+ - All tests for the Sahara project have been removed from Tempest. They now
+ live as a Tempest plugin in the ``openstack/sahara-tests`` repository.
diff --git a/releasenotes/notes/14/14.0.0-volume-clients-as-library-309030c7a16e62ab.yaml b/releasenotes/notes/14/14.0.0-volume-clients-as-library-309030c7a16e62ab.yaml
new file mode 100644
index 0000000..6babd93
--- /dev/null
+++ b/releasenotes/notes/14/14.0.0-volume-clients-as-library-309030c7a16e62ab.yaml
@@ -0,0 +1,12 @@
+---
+features:
+ - |
+ Define volume service clients as libraries.
+ The following volume service clients are defined as library interface,
+ so the other projects can use these modules as stable libraries without
+ any maintenance changes.
+
+ * volumes_client(v1)
+ * volumes_client(v2)
+ * capabilities_client(v2)
+ * scheduler_stats_client(v2)
diff --git a/releasenotes/notes/15/15.0.0-add-identity-v3-clients-as-a-library-d34b4fdf376984ad.yaml b/releasenotes/notes/15/15.0.0-add-identity-v3-clients-as-a-library-d34b4fdf376984ad.yaml
new file mode 100644
index 0000000..1af1939
--- /dev/null
+++ b/releasenotes/notes/15/15.0.0-add-identity-v3-clients-as-a-library-d34b4fdf376984ad.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - Define the identity v3 service client domains_client as a library.
+ Add domains_client to the library interface so the other
+ projects can use this module as a stable library without any
+ maintenance changes.
diff --git a/releasenotes/notes/15/15.0.0-add-image-clients-tests-49dbc0a0a4281a77.yaml b/releasenotes/notes/15/15.0.0-add-image-clients-tests-49dbc0a0a4281a77.yaml
new file mode 100644
index 0000000..eaab1f0
--- /dev/null
+++ b/releasenotes/notes/15/15.0.0-add-image-clients-tests-49dbc0a0a4281a77.yaml
@@ -0,0 +1,10 @@
+---
+features:
+ - |
+ As in the [doc]:
+ http://developer.openstack.org/api-ref/image/v2/metadefs-index.html,
+ there are some apis are not included, add them.
+
+ * namespace_objects_client(v2)
+ * namespace_tags_client(v2)
+
diff --git a/releasenotes/notes/15/15.0.0-add-implied-roles-to-roles-client-library-edf96408ad9ba82e.yaml b/releasenotes/notes/15/15.0.0-add-implied-roles-to-roles-client-library-edf96408ad9ba82e.yaml
new file mode 100644
index 0000000..9116ef8
--- /dev/null
+++ b/releasenotes/notes/15/15.0.0-add-implied-roles-to-roles-client-library-edf96408ad9ba82e.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Add the implied roles feature API to the roles_client library. This
+ feature enables the possibility to create inferences rules between
+ roles (a role being implied by another role).
diff --git a/releasenotes/notes/15/15.0.0-add-snapshot-manage-client-as-library-a76ffdba9d8d01cb.yaml b/releasenotes/notes/15/15.0.0-add-snapshot-manage-client-as-library-a76ffdba9d8d01cb.yaml
new file mode 100644
index 0000000..9a4e6b1
--- /dev/null
+++ b/releasenotes/notes/15/15.0.0-add-snapshot-manage-client-as-library-a76ffdba9d8d01cb.yaml
@@ -0,0 +1,8 @@
+---
+features:
+ - |
+ Define v2 snapshot_manage_client client for the volume service as
+ library interfaces, allowing other projects to use this module as
+ stable libraries without maintenance changes.
+
+ * snapshot_manage_client(v2)
diff --git a/releasenotes/notes/15/15.0.0-deprecate-allow_port_security_disabled-option-2d3d87f6bd11d03a.yaml b/releasenotes/notes/15/15.0.0-deprecate-allow_port_security_disabled-option-2d3d87f6bd11d03a.yaml
new file mode 100644
index 0000000..4acdc6d
--- /dev/null
+++ b/releasenotes/notes/15/15.0.0-deprecate-allow_port_security_disabled-option-2d3d87f6bd11d03a.yaml
@@ -0,0 +1,8 @@
+---
+upgrade:
+ - The default value for the ``allow_port_security_disabled`` option in the
+ ``compute-feature-enabled`` section has been changed from ``False``
+ to ``True``.
+deprecations:
+ - The ``allow_port_security_disabled`` option in the
+ ``compute-feature-enabled`` section is now deprecated.
diff --git a/releasenotes/notes/15/15.0.0-deprecate-identity-feature-enabled.reseller-84800a8232fe217f.yaml b/releasenotes/notes/15/15.0.0-deprecate-identity-feature-enabled.reseller-84800a8232fe217f.yaml
new file mode 100644
index 0000000..c0a06d1
--- /dev/null
+++ b/releasenotes/notes/15/15.0.0-deprecate-identity-feature-enabled.reseller-84800a8232fe217f.yaml
@@ -0,0 +1,8 @@
+---
+upgrade:
+ - The default value for the ``reseller`` option in the
+ ``identity-feature-enabled`` section has been changed from ``False``
+ to ``True``.
+deprecations:
+ - The ``reseller`` option in the ``identity-feature-enabled`` section is now
+ deprecated.
diff --git a/releasenotes/notes/15/15.0.0-deprecate-volume_feature_enabled.volume_services-dbe024ea067d5ab2.yaml b/releasenotes/notes/15/15.0.0-deprecate-volume_feature_enabled.volume_services-dbe024ea067d5ab2.yaml
new file mode 100644
index 0000000..c80f159
--- /dev/null
+++ b/releasenotes/notes/15/15.0.0-deprecate-volume_feature_enabled.volume_services-dbe024ea067d5ab2.yaml
@@ -0,0 +1,8 @@
+---
+upgrade:
+ - The default value for the ``volume_services`` option in the
+ ``volume_feature_enabled`` section has been changed from ``False``
+ to ``True``.
+deprecations:
+ - The ``volume_services`` option in the ``volume_feature_enabled`` section
+ is now deprecated.
diff --git a/releasenotes/notes/15/15.0.0-jsonschema-validator-2377ba131e12d3c7.yaml b/releasenotes/notes/15/15.0.0-jsonschema-validator-2377ba131e12d3c7.yaml
new file mode 100644
index 0000000..8817ed4
--- /dev/null
+++ b/releasenotes/notes/15/15.0.0-jsonschema-validator-2377ba131e12d3c7.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - Added customized JSON schema format checker for 'date-time' format.
+ Compute response schema will be validated against customized format
+ checker.
diff --git a/releasenotes/notes/15/15.0.0-remove-deprecated-compute-microversion-config-options-eaee6a7d2f8390a8.yaml b/releasenotes/notes/15/15.0.0-remove-deprecated-compute-microversion-config-options-eaee6a7d2f8390a8.yaml
new file mode 100644
index 0000000..b1c0c62
--- /dev/null
+++ b/releasenotes/notes/15/15.0.0-remove-deprecated-compute-microversion-config-options-eaee6a7d2f8390a8.yaml
@@ -0,0 +1,9 @@
+---
+upgrade:
+ - The deprecated compute microversion config options from
+ 'compute-feature-enabled' group have been removed. Those config options
+ are available under 'compute' group to configure the min and max
+ microversion for compute service.
+
+ * CONF.compute.min_microversion
+ * CONF.compute.max_microversion
diff --git a/releasenotes/notes/15/15.0.0-remove-deprecated-compute-validation-config-options-e3d1b89ce074d71c.yaml b/releasenotes/notes/15/15.0.0-remove-deprecated-compute-validation-config-options-e3d1b89ce074d71c.yaml
new file mode 100644
index 0000000..104bf27
--- /dev/null
+++ b/releasenotes/notes/15/15.0.0-remove-deprecated-compute-validation-config-options-e3d1b89ce074d71c.yaml
@@ -0,0 +1,25 @@
+---
+prelude: >
+ This release is marking the start of Ocata release support in Tempest
+upgrade:
+ - |
+ Below deprecated config options from compute group have been removed.
+ Corresponding config options already been available in validation group.
+
+ - ``compute.use_floatingip_for_ssh`` (available as ``validation.connect_method``)
+ - ``compute.ssh_auth_method`` (available as ``validation.auth_method``)
+ - ``compute.image_ssh_password`` (available as ``validation.image_ssh_password``)
+ - ``compute.ssh_shell_prologue`` (available as ``validation.ssh_shell_prologue``)
+ - ``compute.ping_size `` (available as ``validation.ping_size``)
+ - ``compute.ping_count `` (available as ``validation.ping_count``)
+ - ``compute.floating_ip_range `` (available as ``validation.floating_ip_range``)
+other:
+ - |
+ OpenStack releases supported at this time are **Mitaka**, **Newton**,
+ and **Ocata**.
+
+ The release under current development as of this tag is Pike,
+ meaning that every Tempest commit is also tested against master during
+ the Pike cycle. However, this does not necessarily mean that using
+ Tempest as of this tag will work against a Pike (or future releases)
+ cloud.
diff --git a/releasenotes/notes/15/15.0.0-remove-deprecated-input-scenario-config-options-414e0c5442e967e9.yaml b/releasenotes/notes/15/15.0.0-remove-deprecated-input-scenario-config-options-414e0c5442e967e9.yaml
new file mode 100644
index 0000000..371c061
--- /dev/null
+++ b/releasenotes/notes/15/15.0.0-remove-deprecated-input-scenario-config-options-414e0c5442e967e9.yaml
@@ -0,0 +1,6 @@
+---
+upgrade:
+ - The deprecated input-scenario config options and group
+ have been removed.
+ The input scenarios functionality already being removed from tempest
+ and from this release, their corresponding config options too.
diff --git a/releasenotes/notes/15/15.0.0-remove-deprecated-network-config-options-f9ce276231578fe6.yaml b/releasenotes/notes/15/15.0.0-remove-deprecated-network-config-options-f9ce276231578fe6.yaml
new file mode 100644
index 0000000..e445fb3
--- /dev/null
+++ b/releasenotes/notes/15/15.0.0-remove-deprecated-network-config-options-f9ce276231578fe6.yaml
@@ -0,0 +1,11 @@
+---
+upgrade:
+ - |
+ Below deprecated network config options have been removed.
+ Those config options already been renamed to below meaningful names.
+
+ - ``tenant_network_cidr`` (removed) -> ``project_network_cidr``
+ - ``tenant_network_mask_bits`` (removed) -> ``project_network_mask_bits``
+ - ``tenant_network_v6_cidr`` (removed) -> ``project_network_v6_cidr``
+ - ``tenant_network_v6_mask_bits`` (removed) -> ``project_network_v6_mask_bits``
+ - ``tenant_networks_reachable`` (removed) -> ``project_networks_reachable``
diff --git a/releasenotes/notes/16/16.0.0-add-OAUTH-Consumer-Client-tempest-tests-db1df7aae4a9fd4e.yaml b/releasenotes/notes/16/16.0.0-add-OAUTH-Consumer-Client-tempest-tests-db1df7aae4a9fd4e.yaml
new file mode 100644
index 0000000..6b45666
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-add-OAUTH-Consumer-Client-tempest-tests-db1df7aae4a9fd4e.yaml
@@ -0,0 +1,3 @@
+---
+features:
+ - Add a new client to handle the OAUTH consumers feature from the identity API.
diff --git a/releasenotes/notes/16/16.0.0-add-additional-methods-to-roles-client-library-178d4a6000dec72d.yaml b/releasenotes/notes/16/16.0.0-add-additional-methods-to-roles-client-library-178d4a6000dec72d.yaml
new file mode 100644
index 0000000..01136c6
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-add-additional-methods-to-roles-client-library-178d4a6000dec72d.yaml
@@ -0,0 +1,7 @@
+---
+features:
+ - |
+ Add missing API call, list all role inference rules,
+ to the roles_client library. This feature enables the
+ possibility of listing all role inference rules in the
+ system.
diff --git a/releasenotes/notes/16/16.0.0-add-cascade-parameter-to-volumes-client-ff4f7f12795003a4.yaml b/releasenotes/notes/16/16.0.0-add-cascade-parameter-to-volumes-client-ff4f7f12795003a4.yaml
new file mode 100644
index 0000000..6801858
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-add-cascade-parameter-to-volumes-client-ff4f7f12795003a4.yaml
@@ -0,0 +1,7 @@
+---
+features:
+ - |
+ Add cascade parameter to volumes_client.
+ This option provides the ability to delete a volume and have Cinder
+ handle deletion of snapshots associated with that volume by passing
+ an additional argument to volume delete, "cascade=True".
diff --git a/releasenotes/notes/16/16.0.0-add-compute-server-evaculate-client-as-a-library-ed76baf25f02c3ca.yaml b/releasenotes/notes/16/16.0.0-add-compute-server-evaculate-client-as-a-library-ed76baf25f02c3ca.yaml
new file mode 100644
index 0000000..848a21b
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-add-compute-server-evaculate-client-as-a-library-ed76baf25f02c3ca.yaml
@@ -0,0 +1,3 @@
+---
+features:
+ - Define the compute server evacuate client method in the servers_client library.
diff --git a/releasenotes/notes/16/16.0.0-add-content-type-without-spaces-b2c9b91b257814f3.yaml b/releasenotes/notes/16/16.0.0-add-content-type-without-spaces-b2c9b91b257814f3.yaml
new file mode 100644
index 0000000..9ae46fd
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-add-content-type-without-spaces-b2c9b91b257814f3.yaml
@@ -0,0 +1,9 @@
+---
+upgrade:
+ - The ``JSON_ENC`` and ``TXT_ENC`` option in the ``_error_checker``
+ section have been added with additional content-type which are
+ defined in RFC7231 but missing in the current rest_client.py file.
+ The lack of these additional content-type will cause defcore test
+ to fail for OpenStack public cloud which uses tomcat module in the
+ api gateway. The additions are ``application/json;charset=utf-8``,
+ ``text/html;charset=utf-8``, ``text/plain;charset=utf-8``
diff --git a/releasenotes/notes/16/16.0.0-add-list-auth-project-client-5905076d914a3943.yaml b/releasenotes/notes/16/16.0.0-add-list-auth-project-client-5905076d914a3943.yaml
new file mode 100644
index 0000000..471f8f0
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-add-list-auth-project-client-5905076d914a3943.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Add the list auth projects API to the identity client library. This feature
+ enables the possibility to list projects that are available to be scoped
+ to based on the X-Auth-Token provided in the request.
diff --git a/releasenotes/notes/16/16.0.0-add-list-glance-api-versions-ec5fc8081fc8a0ae.yaml b/releasenotes/notes/16/16.0.0-add-list-glance-api-versions-ec5fc8081fc8a0ae.yaml
new file mode 100644
index 0000000..acc7a41
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-add-list-glance-api-versions-ec5fc8081fc8a0ae.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Add versions_client module for image service.
+ This new module provides list_versions() method which shows API versions
+ from Image service.
diff --git a/releasenotes/notes/16/16.0.0-add-list-security-groups-by-servers-to-servers-client-library-088df48f6d81f4be.yaml b/releasenotes/notes/16/16.0.0-add-list-security-groups-by-servers-to-servers-client-library-088df48f6d81f4be.yaml
new file mode 100644
index 0000000..67f9541
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-add-list-security-groups-by-servers-to-servers-client-library-088df48f6d81f4be.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Add the list security groups by server API to the servers_client
+ library. This feature enables the possibility to list security
+ groups for a server instance.
diff --git a/releasenotes/notes/16/16.0.0-add-list-version-to-identity-client-944cb7396088a575.yaml b/releasenotes/notes/16/16.0.0-add-list-version-to-identity-client-944cb7396088a575.yaml
new file mode 100644
index 0000000..dd66ff5
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-add-list-version-to-identity-client-944cb7396088a575.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Add versions_client module for identity service.
+ This new module provides list_versions() method which shows API versions
+ from Identity service.
diff --git a/releasenotes/notes/16/16.0.0-add-list-version-to-volume-client-4769dd1bd4ab9c5e.yaml b/releasenotes/notes/16/16.0.0-add-list-version-to-volume-client-4769dd1bd4ab9c5e.yaml
new file mode 100644
index 0000000..233cc28
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-add-list-version-to-volume-client-4769dd1bd4ab9c5e.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Add versions_client module for volume service.
+ This new module provides list_versions() method which shows API versions
+ from Volume service.
diff --git a/releasenotes/notes/16/16.0.0-add-quota-sets-detail-kwarg-74b72183295b3ce7.yaml b/releasenotes/notes/16/16.0.0-add-quota-sets-detail-kwarg-74b72183295b3ce7.yaml
new file mode 100644
index 0000000..06f4fcd
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-add-quota-sets-detail-kwarg-74b72183295b3ce7.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Interface show_quota_set of compute quotas_client has been extended to include the
+ argument "detail", which allows for detailed quota set information for a project to be
+ retrieved, if set to True.
diff --git a/releasenotes/notes/16/16.0.0-add-tempest-lib-remote-client-adbeb3f42a36910b.yaml b/releasenotes/notes/16/16.0.0-add-tempest-lib-remote-client-adbeb3f42a36910b.yaml
new file mode 100644
index 0000000..1b8cda2
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-add-tempest-lib-remote-client-adbeb3f42a36910b.yaml
@@ -0,0 +1,11 @@
+---
+features:
+ - |
+ Add remote_client under tempest.lib.
+ This remote_client under tempest.lib is defined as stable
+ interface, and now this module provides the following
+ essential methods.
+
+ - exec_command
+ - validate_authentication
+ - ping_host
diff --git a/releasenotes/notes/16/16.0.0-add-tempest-run-combine-option-e94c1049ba8985d5.yaml b/releasenotes/notes/16/16.0.0-add-tempest-run-combine-option-e94c1049ba8985d5.yaml
new file mode 100644
index 0000000..e9c3694
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-add-tempest-run-combine-option-e94c1049ba8985d5.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Adds a new cli option to tempest run, ``--combine``, which is used
+ to indicate you want the subunit stream output combined with the
+ previous run's in the testr repository
diff --git a/releasenotes/notes/16/16.0.0-add-update-encryption-type-to-encryption-types-client-f3093532a0bcf9a1.yaml b/releasenotes/notes/16/16.0.0-add-update-encryption-type-to-encryption-types-client-f3093532a0bcf9a1.yaml
new file mode 100644
index 0000000..c95e77c
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-add-update-encryption-type-to-encryption-types-client-f3093532a0bcf9a1.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Add update encryption type API to the v2 encryption_types_client library.
+ This feature enables the possibility to update an encryption type for an
+ existing volume type.
diff --git a/releasenotes/notes/16/16.0.0-add-volume-manage-client-as-library-78ab198a1dc1bd41.yaml b/releasenotes/notes/16/16.0.0-add-volume-manage-client-as-library-78ab198a1dc1bd41.yaml
new file mode 100644
index 0000000..3453050
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-add-volume-manage-client-as-library-78ab198a1dc1bd41.yaml
@@ -0,0 +1,9 @@
+---
+features:
+ - |
+ Add the unmanage volume API service method in v2 volumes_client library.
+ Define v2 volume_manage_client client for the volume service as library
+ interfaces, allowing other projects to use this module as stable libraries
+ without maintenance changes.
+
+ * volume_manage_client(v2)
diff --git a/releasenotes/notes/16/16.0.0-create-server-tags-client-8c0042a77e859af6.yaml b/releasenotes/notes/16/16.0.0-create-server-tags-client-8c0042a77e859af6.yaml
new file mode 100644
index 0000000..18ad5b9
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-create-server-tags-client-8c0042a77e859af6.yaml
@@ -0,0 +1,8 @@
+---
+features:
+ - |
+ Add server tags APIs to the servers_client library.
+ This feature enables the possibility of updating, deleting
+ and checking existence of a tag on a server, as well
+ as updating and deleting all tags on a server.
+
diff --git a/releasenotes/notes/16/16.0.0-deprecate-deactivate_image-config-7a282c471937bbcb.yaml b/releasenotes/notes/16/16.0.0-deprecate-deactivate_image-config-7a282c471937bbcb.yaml
new file mode 100644
index 0000000..69c6bb6
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-deprecate-deactivate_image-config-7a282c471937bbcb.yaml
@@ -0,0 +1,6 @@
+---
+deprecations:
+ - |
+ The ``deactivate_image`` configuration switch from the ``config`` module
+ is deprecated. It was added to support the older-than-kilo releases
+ which we don't support anymore.
diff --git a/releasenotes/notes/16/16.0.0-deprecate-dvr_extra_resources-config-8c319d6dab7f7e5c.yaml b/releasenotes/notes/16/16.0.0-deprecate-dvr_extra_resources-config-8c319d6dab7f7e5c.yaml
new file mode 100644
index 0000000..c3e43ee
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-deprecate-dvr_extra_resources-config-8c319d6dab7f7e5c.yaml
@@ -0,0 +1,7 @@
+---
+deprecations:
+ - |
+ The ``dvr_extra_resources`` configuration switch from the ``config`` module
+ is deprecated. It was added to support the Liberty Release which we don't
+ support anymore.
+
diff --git a/releasenotes/notes/16/16.0.0-deprecate-glance-api-version-config-options-8370b63aea8e14cf.yaml b/releasenotes/notes/16/16.0.0-deprecate-glance-api-version-config-options-8370b63aea8e14cf.yaml
new file mode 100644
index 0000000..788bc95
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-deprecate-glance-api-version-config-options-8370b63aea8e14cf.yaml
@@ -0,0 +1,10 @@
+---
+deprecations:
+ - |
+ Glance v1 APIs are deprecated and v2 are current.
+ Tempest should tests only v2 APIs.
+ Below API version selection config options
+ for glance have been deprecated and will be removed in future.
+
+ * CONF.image_feature_enabled.api_v2
+ * CONF.image_feature_enabled.api_v1
diff --git a/releasenotes/notes/16/16.0.0-deprecate-resources-prefix-option-ad490c0a30a0266b.yaml b/releasenotes/notes/16/16.0.0-deprecate-resources-prefix-option-ad490c0a30a0266b.yaml
new file mode 100644
index 0000000..0b45d0d
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-deprecate-resources-prefix-option-ad490c0a30a0266b.yaml
@@ -0,0 +1,10 @@
+---
+upgrade:
+ - The default value of rand_name()'s prefix argument is changed
+ to 'tempest' from None to identify resources are created by
+ Tempest.
+deprecations:
+ - The resources_prefix is marked as deprecated because it is
+ enough to set 'tempest' as the prefix on rand_name() to
+ identify resources which are created by Tempest and no
+ projects set this option on OpenStack dev community.
diff --git a/releasenotes/notes/16/16.0.0-deprecate-skip_unless_attr-decorator-450a1ed727494724.yaml b/releasenotes/notes/16/16.0.0-deprecate-skip_unless_attr-decorator-450a1ed727494724.yaml
new file mode 100644
index 0000000..4d8b941
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-deprecate-skip_unless_attr-decorator-450a1ed727494724.yaml
@@ -0,0 +1,5 @@
+---
+deprecations:
+ - The ``skip_unless_attr`` decorator in lib/decorators.py has been deprecated,
+ please use the standard ``testtools.skipUnless`` and ``testtools.skipIf``
+ decorators.
diff --git a/releasenotes/notes/16/16.0.0-deprecate-skip_unless_config-decorator-64c32d588043ab12.yaml b/releasenotes/notes/16/16.0.0-deprecate-skip_unless_config-decorator-64c32d588043ab12.yaml
new file mode 100644
index 0000000..6285ea6
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-deprecate-skip_unless_config-decorator-64c32d588043ab12.yaml
@@ -0,0 +1,5 @@
+---
+deprecations:
+ - The ``skip_unless_config`` and ``skip_if_config`` decorators in the
+ ``config`` module have been deprecated and will be removed in the Queens
+ dev cycle. Use the ``testtools.skipUnless`` (or a variation of) instead.
diff --git a/releasenotes/notes/16/16.0.0-deprecated-cinder-api-v1-option-df7d5a54d93db5cf.yaml b/releasenotes/notes/16/16.0.0-deprecated-cinder-api-v1-option-df7d5a54d93db5cf.yaml
new file mode 100644
index 0000000..0660d9c
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-deprecated-cinder-api-v1-option-df7d5a54d93db5cf.yaml
@@ -0,0 +1,12 @@
+---
+deprecations:
+ - |
+ Volume v1 API is deprecated and the v3 are CURRENT.
+ Tempest doesn't need to test the v1 API as the default.
+ The volume config option 'api_v1' has been marked as
+ deprecated.
+upgrade:
+ - |
+ The volume config option 'api_v1' default is changed to
+ ``False`` because the volume v1 API has been deprecated
+ since Juno release.
diff --git a/releasenotes/notes/16/16.0.0-dreprecate_client_parameters-cb8d069e62957f7e.yaml b/releasenotes/notes/16/16.0.0-dreprecate_client_parameters-cb8d069e62957f7e.yaml
new file mode 100644
index 0000000..4081f6a
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-dreprecate_client_parameters-cb8d069e62957f7e.yaml
@@ -0,0 +1,6 @@
+---
+deprecations:
+ - |
+ Deprecate the client_parameters argument in
+ `tempest.lib.services.clients.ServiceClients`. The parameter is actually
+ not honoured already - see https://bugs.launchpad.net/tempest/+bug/1680915
diff --git a/releasenotes/notes/16/16.0.0-fix-volume-v2-service-clients-bugfix-1667354-73d2c3c8fedc08bf.yaml b/releasenotes/notes/16/16.0.0-fix-volume-v2-service-clients-bugfix-1667354-73d2c3c8fedc08bf.yaml
new file mode 100644
index 0000000..6d31576
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-fix-volume-v2-service-clients-bugfix-1667354-73d2c3c8fedc08bf.yaml
@@ -0,0 +1,7 @@
+---
+fixes:
+ - |
+ Fix below volume v2 service clients to make v2 API call: Bug#1667354
+
+ - SchedulerStatsClient
+ - CapabilitiesClient
diff --git a/releasenotes/notes/16/16.0.0-mitaka-eol-88ff8355fff81b55.yaml b/releasenotes/notes/16/16.0.0-mitaka-eol-88ff8355fff81b55.yaml
new file mode 100644
index 0000000..24ec512
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-mitaka-eol-88ff8355fff81b55.yaml
@@ -0,0 +1,13 @@
+---
+prelude: >
+ This release indicates end of support for Mitaka in Tempest.
+other:
+ - |
+ OpenStack Releases Supported after this release are **Newton**
+ and **Ocata**
+
+ The release under current development as of this tag is Pike,
+ meaning that every Tempest commit is also tested against master branch
+ during the Pike cycle. However, this does not necessarily mean that
+ using Tempest as of this tag will work against Pike (or future
+ releases) cloud.
diff --git a/releasenotes/notes/16/16.0.0-remove-call_until_true-of-test-de9c13bc8f969921.yaml b/releasenotes/notes/16/16.0.0-remove-call_until_true-of-test-de9c13bc8f969921.yaml
new file mode 100644
index 0000000..5670821
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-remove-call_until_true-of-test-de9c13bc8f969921.yaml
@@ -0,0 +1,6 @@
+---
+upgrade:
+ - The *call_until_true* of *test* module is removed because it was marked
+ as deprecated and Tempest provides it from *test_utils* as a stable
+ interface instead. Please switch to use *test_utils.call_until_true* if
+ necessary.
diff --git a/releasenotes/notes/16/16.0.0-remove-cinder-v1-api-tests-71e266b8d55d475f.yaml b/releasenotes/notes/16/16.0.0-remove-cinder-v1-api-tests-71e266b8d55d475f.yaml
new file mode 100644
index 0000000..710ad9e
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-remove-cinder-v1-api-tests-71e266b8d55d475f.yaml
@@ -0,0 +1,5 @@
+upgrade:
+ - Remove Cinder v1 API tests.
+ Cinder v1 API has been deprecated since Juno release, and Juno is
+ not supported by current Tempest. Then Cinder v1 API tests are
+ removed from Tempest.
diff --git a/releasenotes/notes/16/16.0.0-remove-deprecated-allow_port_security_disabled-option-d0ffaeb2e7817707.yaml b/releasenotes/notes/16/16.0.0-remove-deprecated-allow_port_security_disabled-option-d0ffaeb2e7817707.yaml
new file mode 100644
index 0000000..5b4a96d
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-remove-deprecated-allow_port_security_disabled-option-d0ffaeb2e7817707.yaml
@@ -0,0 +1,5 @@
+---
+upgrade:
+ - |
+ The deprecated config option ``allow_port_security_disabled`` from
+ ``compute_feature_enabled`` group has been removed.
diff --git a/releasenotes/notes/16/16.0.0-remove-deprecated-compute-validation-config-options-part-2-5cd17b6e0e6cb8a3.yaml b/releasenotes/notes/16/16.0.0-remove-deprecated-compute-validation-config-options-part-2-5cd17b6e0e6cb8a3.yaml
new file mode 100644
index 0000000..c8b0ca8
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-remove-deprecated-compute-validation-config-options-part-2-5cd17b6e0e6cb8a3.yaml
@@ -0,0 +1,11 @@
+---
+upgrade:
+ - |
+ Below deprecated config options from compute group have been removed.
+ Corresponding config options already been available in validation group.
+
+ - ``compute.image_ssh_user`` (available as ``validation.image_ssh_user``)
+ - ``compute.ssh_user`` (available as ``validation.image_ssh_user``)
+ - ``scenario.ssh_user`` (available as ``validation.image_ssh_user``)
+ - ``compute.network_for_ssh`` (available as ``validation.network_for_ssh``)
+ - ``compute.ping_timeout`` (available as ``validation.ping_timeout``)
diff --git a/releasenotes/notes/16/16.0.0-remove-deprecated-dvr_extra_resources-option-e8c441c38eab7ddd.yaml b/releasenotes/notes/16/16.0.0-remove-deprecated-dvr_extra_resources-option-e8c441c38eab7ddd.yaml
new file mode 100644
index 0000000..889e862
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-remove-deprecated-dvr_extra_resources-option-e8c441c38eab7ddd.yaml
@@ -0,0 +1,8 @@
+---
+upgrade:
+ - |
+ The deprecated config option 'dvr_extra_resources' from network group has been removed.
+ This option was for extra resources which were provisioned to bind a router to Neutron
+ L3 agent. The extra resources need to be provisioned in Liberty release or older,
+ and are not required since Mitaka release. Current Tempest doesn't support Liberty, so
+ this option has been removed from Tempest.
diff --git a/releasenotes/notes/16/16.0.0-remove-deprecated-identity-reseller-option-4411c7e3951f1094.yaml b/releasenotes/notes/16/16.0.0-remove-deprecated-identity-reseller-option-4411c7e3951f1094.yaml
new file mode 100644
index 0000000..8085694
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-remove-deprecated-identity-reseller-option-4411c7e3951f1094.yaml
@@ -0,0 +1,4 @@
+---
+upgrade:
+ - |
+ The deprecated config option 'reseller' from identity_feature_enabled group has been removed.
diff --git a/releasenotes/notes/16/16.0.0-remove-sahara-service-available-44a642aa9c634ab4.yaml b/releasenotes/notes/16/16.0.0-remove-sahara-service-available-44a642aa9c634ab4.yaml
new file mode 100644
index 0000000..c0dc7d7
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-remove-sahara-service-available-44a642aa9c634ab4.yaml
@@ -0,0 +1,7 @@
+---
+fixes:
+ - |
+ The 'sahara' config option in the 'service-available' group has been moved to the sahara plugin
+ (openstack/sahara-tests) along with tests and service client during the Ocata timeframe.
+ A 'sahara' config option was left over on Tempest side, and it's removed now.
+ As long as the sahara plugin is installed, this change as no impact on users of sahara tests.
diff --git a/releasenotes/notes/16/16.0.0-remove-volume_feature_enabled.volume_services-c6aa142cc1021297.yaml b/releasenotes/notes/16/16.0.0-remove-volume_feature_enabled.volume_services-c6aa142cc1021297.yaml
new file mode 100644
index 0000000..fc15995
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-remove-volume_feature_enabled.volume_services-c6aa142cc1021297.yaml
@@ -0,0 +1,5 @@
+---
+upgrade:
+ - |
+ The deprecated ``volume_services`` option in the ``volume_feature_enabled``
+ section has now been removed.
diff --git a/releasenotes/notes/16/16.0.0-use-keystone-v3-api-935860d30ddbb8e9.yaml b/releasenotes/notes/16/16.0.0-use-keystone-v3-api-935860d30ddbb8e9.yaml
new file mode 100644
index 0000000..dd6e924
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-use-keystone-v3-api-935860d30ddbb8e9.yaml
@@ -0,0 +1,5 @@
+---
+upgrade:
+ - Tempest now defaults to using Keystone v3 API for the
+ authentication, because Keystone v3 API is CURRENT and
+ the v2 API is deprecated.
diff --git a/releasenotes/notes/16/16.0.0-volume-transfers-client-e5ed3f5464c0cdc0.yaml b/releasenotes/notes/16/16.0.0-volume-transfers-client-e5ed3f5464c0cdc0.yaml
new file mode 100644
index 0000000..e5e479b
--- /dev/null
+++ b/releasenotes/notes/16/16.0.0-volume-transfers-client-e5ed3f5464c0cdc0.yaml
@@ -0,0 +1,18 @@
+---
+features:
+ - |
+ Define volume transfers service clients as libraries.
+ The following volume transfers service clients are defined as library interface.
+
+ * transfers_client(v2)
+deprecations:
+ - |
+ Deprecate volume v2 transfers resource methods from volumes_client(v2) libraries.
+ Same methods are available in new transfers service client: transfers_client(v2)
+ The following methods of volume v2 volumes_clients have been deprecated:
+
+ * create_volume_transfer (v2.volumes_client)
+ * show_volume_transfer (v2.volumes_client)
+ * list_volume_transfers (v2.volumes_client)
+ * delete_volume_transfer (v2.volumes_client)
+ * accept_volume_transfer (v2.volumes_client)
diff --git a/releasenotes/notes/add-OAUTH-Token-Client-tempest-tests-6351eda451b95a86.yaml b/releasenotes/notes/add-OAUTH-Token-Client-tempest-tests-6351eda451b95a86.yaml
new file mode 100644
index 0000000..9115f03
--- /dev/null
+++ b/releasenotes/notes/add-OAUTH-Token-Client-tempest-tests-6351eda451b95a86.yaml
@@ -0,0 +1,3 @@
+---
+features:
+ - Add a new client to handle the OAUTH token feature from the identity API.
diff --git a/releasenotes/notes/add-additional-methods-to-policy-client-library-b8279c18335588c9.yaml b/releasenotes/notes/add-additional-methods-to-policy-client-library-b8279c18335588c9.yaml
new file mode 100644
index 0000000..cd5284d
--- /dev/null
+++ b/releasenotes/notes/add-additional-methods-to-policy-client-library-b8279c18335588c9.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Add ``v3-ext/OS-ENDPOINT-POLICY`` API calls to support creation, deletion and
+ retrieval of associations between service endpoints and policies. Such associations
+ enable an endpoint to request its policy.
diff --git a/releasenotes/notes/add-compute-feature-serial-console-45583c4341e34fc9.yaml b/releasenotes/notes/add-compute-feature-serial-console-45583c4341e34fc9.yaml
new file mode 100644
index 0000000..18fd5ad
--- /dev/null
+++ b/releasenotes/notes/add-compute-feature-serial-console-45583c4341e34fc9.yaml
@@ -0,0 +1,7 @@
+---
+features:
+ - |
+ A new boolean config option ``serial_console`` is added to the section
+ ``compute-feature-enabled``. If enabled, tests, which validate the
+ behavior of Nova's *serial console* feature (an alternative to VNC,
+ RDP, SPICE) can be executed.
diff --git a/releasenotes/notes/add-create-group-from-src-tempest-tests-9eb8b0b4b5c52055.yaml b/releasenotes/notes/add-create-group-from-src-tempest-tests-9eb8b0b4b5c52055.yaml
new file mode 100644
index 0000000..dec4a27
--- /dev/null
+++ b/releasenotes/notes/add-create-group-from-src-tempest-tests-9eb8b0b4b5c52055.yaml
@@ -0,0 +1,4 @@
+---
+features:
+ - |
+ Add create_group_from_source to groups_client in the volume service library.
diff --git a/releasenotes/notes/add-domain-configuration-client-tempest-tests-e383efabdbb9ad03.yaml b/releasenotes/notes/add-domain-configuration-client-tempest-tests-e383efabdbb9ad03.yaml
new file mode 100644
index 0000000..5653681
--- /dev/null
+++ b/releasenotes/notes/add-domain-configuration-client-tempest-tests-e383efabdbb9ad03.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - |
+ Add a new client to handle the domain configuration feature from the
+ identity v3 API.
diff --git a/releasenotes/notes/add-domain-param-in-cliclient-a270fcf35c8f09e6.yaml b/releasenotes/notes/add-domain-param-in-cliclient-a270fcf35c8f09e6.yaml
new file mode 100644
index 0000000..87a6af9
--- /dev/null
+++ b/releasenotes/notes/add-domain-param-in-cliclient-a270fcf35c8f09e6.yaml
@@ -0,0 +1,17 @@
+---
+fixes:
+ - |
+ Allow to specify new domain parameters:
+
+ * `user_domain_name`
+ * `user_domain_id`
+ * `project_domain_name`
+ * `project_domain_id`
+
+ for CLIClient class, whose values will be substituted to
+ ``--os-user-domain-name``, ``--os-user-domain-id``,
+ ``--os-project-domain-name`` and ``--os-project-domain-id`` respectively
+ during command execution.
+
+ This allows to prevent possible test failures with authentication in
+ Keystone v3. Bug: #1719687
diff --git a/releasenotes/notes/add-extra-apis-to-volume-v3-services-client-bf9b235cf5a611fe.yaml b/releasenotes/notes/add-extra-apis-to-volume-v3-services-client-bf9b235cf5a611fe.yaml
new file mode 100644
index 0000000..03d0ae8
--- /dev/null
+++ b/releasenotes/notes/add-extra-apis-to-volume-v3-services-client-bf9b235cf5a611fe.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Add ``enable_service``, ``disable_service`` , ``disable_log_reason``,
+ ``freeze_host`` and ``thaw_host`` API endpoints to volume v3
+ ``services_client``.
diff --git a/releasenotes/notes/add-floating-ip-config-option-e5774bf77702ce9f.yaml b/releasenotes/notes/add-floating-ip-config-option-e5774bf77702ce9f.yaml
new file mode 100644
index 0000000..8221d78
--- /dev/null
+++ b/releasenotes/notes/add-floating-ip-config-option-e5774bf77702ce9f.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - A new config option in the network-feature-enabled section, floating_ips,
+ to specify whether floating ips are available in the cloud under test. By
+ default this is set to True.
diff --git a/releasenotes/notes/add-force-detach-volume-to-volumes-client-library-b2071f2954f8e8b1.yaml b/releasenotes/notes/add-force-detach-volume-to-volumes-client-library-b2071f2954f8e8b1.yaml
new file mode 100644
index 0000000..a0156a0
--- /dev/null
+++ b/releasenotes/notes/add-force-detach-volume-to-volumes-client-library-b2071f2954f8e8b1.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Add force detach volume feature API to v2 volumes_client library.
+ This feature enables the possibility to force a volume to detach, and
+ roll back an unsuccessful detach operation after you disconnect the volume.
diff --git a/releasenotes/notes/add-group-type-specs-apis-to-v3-group-types-client-10390b52dedede54.yaml b/releasenotes/notes/add-group-type-specs-apis-to-v3-group-types-client-10390b52dedede54.yaml
new file mode 100644
index 0000000..404319d
--- /dev/null
+++ b/releasenotes/notes/add-group-type-specs-apis-to-v3-group-types-client-10390b52dedede54.yaml
@@ -0,0 +1,10 @@
+---
+features:
+ - |
+ Add group type specs APIs to v3 group_types_client library.
+
+ * create_or_update_group_type_specs
+ * list_group_type_specs
+ * show_group_type_specs_item
+ * update_group_type_specs_item
+ * delete_group_type_specs_item
diff --git a/releasenotes/notes/add-identity-v3-clients-for-os-ep-filter-api-endpoint-groups-3518a90bbb731d0f.yaml b/releasenotes/notes/add-identity-v3-clients-for-os-ep-filter-api-endpoint-groups-3518a90bbb731d0f.yaml
new file mode 100644
index 0000000..1dc33aa
--- /dev/null
+++ b/releasenotes/notes/add-identity-v3-clients-for-os-ep-filter-api-endpoint-groups-3518a90bbb731d0f.yaml
@@ -0,0 +1,7 @@
+---
+features:
+ - |
+ Defines the identity v3 OS-EP-FILTER EndPoint Groups API client.
+ This client manages Create, Get, Update, Check, List, and Delete
+ of EndPoint Group.
+
diff --git a/releasenotes/notes/add-identity-v3-clients-for-os-ep-filter-api-extensions-9cfd217fd2c6a61f.yaml b/releasenotes/notes/add-identity-v3-clients-for-os-ep-filter-api-extensions-9cfd217fd2c6a61f.yaml
new file mode 100644
index 0000000..69320fb
--- /dev/null
+++ b/releasenotes/notes/add-identity-v3-clients-for-os-ep-filter-api-extensions-9cfd217fd2c6a61f.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Defines the identity v3 OS-EP-FILTER extension API client.
+ This client manages associations between endpoints, projects
+ along with groups.
diff --git a/releasenotes/notes/add-ip-version-check-in-addresses-x491ac6d9abaxa12.yaml b/releasenotes/notes/add-ip-version-check-in-addresses-x491ac6d9abaxa12.yaml
new file mode 100644
index 0000000..957e903
--- /dev/null
+++ b/releasenotes/notes/add-ip-version-check-in-addresses-x491ac6d9abaxa12.yaml
@@ -0,0 +1,4 @@
+---
+fixes:
+ Add more accurate ip version check in addresses schema which will
+ limit the ip version value in [4, 6].
diff --git a/releasenotes/notes/add-is-resource-deleted-sg-client-f4a7a7a54ff024d7.yaml b/releasenotes/notes/add-is-resource-deleted-sg-client-f4a7a7a54ff024d7.yaml
new file mode 100644
index 0000000..e046326
--- /dev/null
+++ b/releasenotes/notes/add-is-resource-deleted-sg-client-f4a7a7a54ff024d7.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - |
+ Implement the `rest_client` method `is_resource_deleted` in the network
+ security group client.
diff --git a/releasenotes/notes/add-kwargs-to-delete-vol-of-vol-client-1ecde75beb62933c.yaml b/releasenotes/notes/add-kwargs-to-delete-vol-of-vol-client-1ecde75beb62933c.yaml
new file mode 100644
index 0000000..b8c9dfc
--- /dev/null
+++ b/releasenotes/notes/add-kwargs-to-delete-vol-of-vol-client-1ecde75beb62933c.yaml
@@ -0,0 +1,7 @@
+---
+features:
+ - |
+ The ``delete_volume`` method of the ``VolumesClient`` class
+ now has an additional ``**params`` argument that enables passing
+ additional information in the query string of the HTTP request.
+
diff --git a/releasenotes/notes/add-list-volume-transfers-with-detail-to-transfers-client-80169bf78cf4fa66.yaml b/releasenotes/notes/add-list-volume-transfers-with-detail-to-transfers-client-80169bf78cf4fa66.yaml
new file mode 100644
index 0000000..8e85d3a
--- /dev/null
+++ b/releasenotes/notes/add-list-volume-transfers-with-detail-to-transfers-client-80169bf78cf4fa66.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - |
+ Add list volume transfers with details API to v2 transfers_client library.
+ This feature enables the possibility to list volume transfers with details.
diff --git a/releasenotes/notes/add-load-list-cmd-35a4a2e6ea0a36fd.yaml b/releasenotes/notes/add-load-list-cmd-35a4a2e6ea0a36fd.yaml
new file mode 100644
index 0000000..145e7dd
--- /dev/null
+++ b/releasenotes/notes/add-load-list-cmd-35a4a2e6ea0a36fd.yaml
@@ -0,0 +1,7 @@
+---
+features:
+ - |
+ Adds a new cli option to tempest run, ``--load-list <list-file>``
+ to specify target tests to run from a list-file. The list-file
+ supports the output format of the tempest run ``--list-tests``
+ command.
diff --git a/releasenotes/notes/add-manage-snapshot-ref-config-option-67efd04897335b67.yaml b/releasenotes/notes/add-manage-snapshot-ref-config-option-67efd04897335b67.yaml
new file mode 100644
index 0000000..bc7bcc8
--- /dev/null
+++ b/releasenotes/notes/add-manage-snapshot-ref-config-option-67efd04897335b67.yaml
@@ -0,0 +1,7 @@
+---
+features:
+ - |
+ A new config option 'manage_snapshot_ref' is added in the volume section,
+ to specify snapshot ref parameter for different storage backend drivers
+ when managing an existing snapshot. By default it is set to fit the LVM
+ driver.
diff --git a/releasenotes/notes/add-params-to-identity-v3-list-endpoints-958a155be4e17e5b.yaml b/releasenotes/notes/add-params-to-identity-v3-list-endpoints-958a155be4e17e5b.yaml
new file mode 100644
index 0000000..46f3b49
--- /dev/null
+++ b/releasenotes/notes/add-params-to-identity-v3-list-endpoints-958a155be4e17e5b.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ The ``list_endpoints`` method of the v3 ``EndPointsClient`` class now has
+ an additional ``**params`` argument that enables passing additional
+ information in the query string of the HTTP request.
diff --git a/releasenotes/notes/add-params-to-v2-list-backups-api-c088d2b4bfe90247.yaml b/releasenotes/notes/add-params-to-v2-list-backups-api-c088d2b4bfe90247.yaml
new file mode 100644
index 0000000..cee2d76
--- /dev/null
+++ b/releasenotes/notes/add-params-to-v2-list-backups-api-c088d2b4bfe90247.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ The ``list_backups`` method of the v2 ``BackupsClient`` class now has
+ an additional ``**params`` argument that enables passing additional
+ information in the query string of the HTTP request.
diff --git a/releasenotes/notes/add-port-profile-config-option-2610b2fa67027960.yaml b/releasenotes/notes/add-port-profile-config-option-2610b2fa67027960.yaml
new file mode 100644
index 0000000..19d47d1
--- /dev/null
+++ b/releasenotes/notes/add-port-profile-config-option-2610b2fa67027960.yaml
@@ -0,0 +1,9 @@
+---
+features:
+ - A new config option 'port_profile' is added to the section
+ 'network' to specify capabilities of the port.
+ By default this is set to {}. When using OVS HW offload
+ feature we need to create Neutron port with a certain
+ capability. This is done by creating Neutron port with
+ binding profile. To be able to test this we need profile
+ capability support in Tempest as well.
diff --git a/releasenotes/notes/add-reset-group-snapshot-status-api-to-v3-group-snapshots-client-248d41827daf2a0c.yaml b/releasenotes/notes/add-reset-group-snapshot-status-api-to-v3-group-snapshots-client-248d41827daf2a0c.yaml
new file mode 100644
index 0000000..76b395d
--- /dev/null
+++ b/releasenotes/notes/add-reset-group-snapshot-status-api-to-v3-group-snapshots-client-248d41827daf2a0c.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Add reset group snapshot status API to v3 group_snapshots_client library,
+ min_microversion of this API is 3.19. This feature enables the possibility
+ to reset group snapshot status.
diff --git a/releasenotes/notes/add-reset-group-status-api-to-v3-groups-client-9aa048617c66756a.yaml b/releasenotes/notes/add-reset-group-status-api-to-v3-groups-client-9aa048617c66756a.yaml
new file mode 100644
index 0000000..a39c23b
--- /dev/null
+++ b/releasenotes/notes/add-reset-group-status-api-to-v3-groups-client-9aa048617c66756a.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Add reset group status API to v3 groups_client library, min_microversion
+ of this API is 3.20. This feature enables the possibility to reset group
+ status.
diff --git a/releasenotes/notes/add-return-value-to-retype-volume-a401aa619aaa2457.yaml b/releasenotes/notes/add-return-value-to-retype-volume-a401aa619aaa2457.yaml
new file mode 100644
index 0000000..ca42014
--- /dev/null
+++ b/releasenotes/notes/add-return-value-to-retype-volume-a401aa619aaa2457.yaml
@@ -0,0 +1,7 @@
+---
+fixes:
+ - |
+ Add a missing return statement to the retype_volume API in the v2
+ volumes_client library: Bug#1703997
+
+ This changes the response body from None to an empty dictionary.
diff --git a/releasenotes/notes/add-save-state-option-5ea67858cbaca969.yaml b/releasenotes/notes/add-save-state-option-5ea67858cbaca969.yaml
new file mode 100644
index 0000000..abd2610
--- /dev/null
+++ b/releasenotes/notes/add-save-state-option-5ea67858cbaca969.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - |
+ Add ``--save-state`` option to allow saving state of cloud before
+ tempest run.
diff --git a/releasenotes/notes/add-server-diagnostics-validation-schema-b5a3c55b45aa718a.yaml b/releasenotes/notes/add-server-diagnostics-validation-schema-b5a3c55b45aa718a.yaml
new file mode 100644
index 0000000..e0ac87c
--- /dev/null
+++ b/releasenotes/notes/add-server-diagnostics-validation-schema-b5a3c55b45aa718a.yaml
@@ -0,0 +1,4 @@
+---
+features:
+ - |
+ Add validation schema for Nova server diagnostics API
\ No newline at end of file
diff --git a/releasenotes/notes/add-show-default-quotas-api-to-network-quotas-client-3a7c1159af9e56ff.yaml b/releasenotes/notes/add-show-default-quotas-api-to-network-quotas-client-3a7c1159af9e56ff.yaml
new file mode 100644
index 0000000..6efe7e6
--- /dev/null
+++ b/releasenotes/notes/add-show-default-quotas-api-to-network-quotas-client-3a7c1159af9e56ff.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Add show default quotas API to network quotas_client library.
+ This feature enables the possibility to show default network quotas for
+ a specified project.
diff --git a/releasenotes/notes/add-show-encryption-specs-item-api-to-v2-encryption-types-client-290b421cd4bc0c0e.yaml b/releasenotes/notes/add-show-encryption-specs-item-api-to-v2-encryption-types-client-290b421cd4bc0c0e.yaml
new file mode 100644
index 0000000..9e13afc
--- /dev/null
+++ b/releasenotes/notes/add-show-encryption-specs-item-api-to-v2-encryption-types-client-290b421cd4bc0c0e.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Add show encryption specs item API to v2 encryption_types_client library.
+ This feature enables the possibility to show specific encryption specs for
+ a volume type.
diff --git a/releasenotes/notes/add-show-host-to-hosts-client-library-c60c4eb49d139480.yaml b/releasenotes/notes/add-show-host-to-hosts-client-library-c60c4eb49d139480.yaml
new file mode 100644
index 0000000..0de1803
--- /dev/null
+++ b/releasenotes/notes/add-show-host-to-hosts-client-library-c60c4eb49d139480.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - |
+ Add show host API to the volume v2 hosts_client library.
+ This feature enables the possibility to show details for a host.
diff --git a/releasenotes/notes/add-show-quota-details-api-to-network-quotas-client-3fffd302cc5d335f.yaml b/releasenotes/notes/add-show-quota-details-api-to-network-quotas-client-3fffd302cc5d335f.yaml
new file mode 100644
index 0000000..6c44ba0
--- /dev/null
+++ b/releasenotes/notes/add-show-quota-details-api-to-network-quotas-client-3fffd302cc5d335f.yaml
@@ -0,0 +1,7 @@
+---
+features:
+ - |
+ Add extension API show quota details to network quotas_client library.
+ This feature enables the possibility to show a quota set for a specified
+ project that includes the quota's used, limit and reserved counts per
+ resource.
diff --git a/releasenotes/notes/add-show-snapshot-metadata-item-api-to-v2-snapshots-client-bd3cbab3c7f0e0b3.yaml b/releasenotes/notes/add-show-snapshot-metadata-item-api-to-v2-snapshots-client-bd3cbab3c7f0e0b3.yaml
new file mode 100644
index 0000000..140df60
--- /dev/null
+++ b/releasenotes/notes/add-show-snapshot-metadata-item-api-to-v2-snapshots-client-bd3cbab3c7f0e0b3.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Add show snapshot metadata item API to v2 snapshots_client library.
+ This feature enables the possibility to show a snapshot's metadata for
+ a specific key.
diff --git a/releasenotes/notes/add-show-volume-image-metadata-api-to-v2-volumes-client-ee3c027f35276561.yaml b/releasenotes/notes/add-show-volume-image-metadata-api-to-v2-volumes-client-ee3c027f35276561.yaml
new file mode 100644
index 0000000..ac7c74e
--- /dev/null
+++ b/releasenotes/notes/add-show-volume-image-metadata-api-to-v2-volumes-client-ee3c027f35276561.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - |
+ Add show volume image metadata API to v2 volumes_client library.
+ This feature enables the possibility to show volume's image metadata.
diff --git a/releasenotes/notes/add-show-volume-metadata-item-api-to-v2-volumes-client-47d59ecd999ca9df.yaml b/releasenotes/notes/add-show-volume-metadata-item-api-to-v2-volumes-client-47d59ecd999ca9df.yaml
new file mode 100644
index 0000000..49a935c
--- /dev/null
+++ b/releasenotes/notes/add-show-volume-metadata-item-api-to-v2-volumes-client-47d59ecd999ca9df.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Add show volume metadata item API to v2 volumes_client library.
+ This feature enables the possibility to show a volume's metadata for
+ a specific key.
diff --git a/releasenotes/notes/add-show-volume-summary-api-to-v3-volumes-client-96e7b01abdb5c9c3.yaml b/releasenotes/notes/add-show-volume-summary-api-to-v3-volumes-client-96e7b01abdb5c9c3.yaml
new file mode 100644
index 0000000..361e387
--- /dev/null
+++ b/releasenotes/notes/add-show-volume-summary-api-to-v3-volumes-client-96e7b01abdb5c9c3.yaml
@@ -0,0 +1,10 @@
+---
+features:
+ - |
+ Define v3 volumes_client for the volume service as a library interface,
+ allowing other projects to use this module as a stable library without
+ maintenance changes.
+ Add show volume summary API to v3 volumes_client library, min_microversion
+ of this API is 3.12.
+
+ * volumes_client(v3)
diff --git a/releasenotes/notes/add-support-args-kwargs-in-call-until-true-a91k592h5a64exf7.yaml b/releasenotes/notes/add-support-args-kwargs-in-call-until-true-a91k592h5a64exf7.yaml
new file mode 100644
index 0000000..9c30a0c
--- /dev/null
+++ b/releasenotes/notes/add-support-args-kwargs-in-call-until-true-a91k592h5a64exf7.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - Add support of args and kwargs when calling func in call_until_true,
+ also to log the cost time when call_until_true returns True or False
+ for debugging.
diff --git a/releasenotes/notes/add-update-api-to-group-types-client-09c06ccdf80d5003.yaml b/releasenotes/notes/add-update-api-to-group-types-client-09c06ccdf80d5003.yaml
new file mode 100644
index 0000000..14458d6
--- /dev/null
+++ b/releasenotes/notes/add-update-api-to-group-types-client-09c06ccdf80d5003.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - |
+ Add update group types API to v3 ``group_types_client`` library;
+ min_microversion of this API is 3.11.
diff --git a/releasenotes/notes/add-update-backup-api-to-v3-backups-client-e8465b2b66617dc0.yaml b/releasenotes/notes/add-update-backup-api-to-v3-backups-client-e8465b2b66617dc0.yaml
new file mode 100644
index 0000000..7cd6887
--- /dev/null
+++ b/releasenotes/notes/add-update-backup-api-to-v3-backups-client-e8465b2b66617dc0.yaml
@@ -0,0 +1,10 @@
+---
+features:
+ - |
+ Define v3 backups_client for the volume service as a library interface,
+ allowing other projects to use this module as a stable library without
+ maintenance changes.
+ Add update backup API to v3 backups_client library, min_microversion
+ of this API is 3.9.
+
+ * backups_client(v3)
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/add-update-group-tempest-tests-72f8ec19b2809849.yaml b/releasenotes/notes/add-update-group-tempest-tests-72f8ec19b2809849.yaml
new file mode 100644
index 0000000..23c30af
--- /dev/null
+++ b/releasenotes/notes/add-update-group-tempest-tests-72f8ec19b2809849.yaml
@@ -0,0 +1,4 @@
+---
+features:
+ - |
+ Add update_group to groups_client in the volume service library.
diff --git a/releasenotes/notes/add-validation-resources-to-lib-dc2600c4324ca4d7.yaml b/releasenotes/notes/add-validation-resources-to-lib-dc2600c4324ca4d7.yaml
new file mode 100644
index 0000000..7814f4e
--- /dev/null
+++ b/releasenotes/notes/add-validation-resources-to-lib-dc2600c4324ca4d7.yaml
@@ -0,0 +1,7 @@
+---
+features:
+ - |
+ Add the `validation_resources` module to tempest.lib. The module provides
+ a set of helpers that can be used to provision and cleanup all the
+ resources required to perform ping / ssh tests against a virtual machine:
+ a keypair, a security group with targeted rules and a floating IP.
diff --git a/releasenotes/notes/add-volume-group-snapshots-tempest-tests-840df3da26590f5e.yaml b/releasenotes/notes/add-volume-group-snapshots-tempest-tests-840df3da26590f5e.yaml
new file mode 100644
index 0000000..2ca6e5a
--- /dev/null
+++ b/releasenotes/notes/add-volume-group-snapshots-tempest-tests-840df3da26590f5e.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Add group_snapshots client for the volume service as library.
+ Add tempest tests for create group snapshot, delete group snapshot, show
+ group snapshot, and list group snapshots volume APIs.
diff --git a/releasenotes/notes/add-volume-group-types-tempest-tests-1298ab8cb4fe8b7b.yaml b/releasenotes/notes/add-volume-group-types-tempest-tests-1298ab8cb4fe8b7b.yaml
new file mode 100644
index 0000000..4fd3bee
--- /dev/null
+++ b/releasenotes/notes/add-volume-group-types-tempest-tests-1298ab8cb4fe8b7b.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - |
+ Add list_group_type and show_group_type in the group_types client for
+ the volume service. Add tests for create/delete/show/list group types.
diff --git a/releasenotes/notes/add-volume-groups-tempest-tests-dd7b2abfe2b48427.yaml b/releasenotes/notes/add-volume-groups-tempest-tests-dd7b2abfe2b48427.yaml
new file mode 100644
index 0000000..898d366
--- /dev/null
+++ b/releasenotes/notes/add-volume-groups-tempest-tests-dd7b2abfe2b48427.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Add groups and group_types clients for the volume service as library.
+ Add tempest tests for create group, delete group, show group, and
+ list group volume APIs.
diff --git a/releasenotes/notes/add-volume-quota-class-client-as-library-c4c2b22c36ff807e.yaml b/releasenotes/notes/add-volume-quota-class-client-as-library-c4c2b22c36ff807e.yaml
new file mode 100644
index 0000000..e6847eb
--- /dev/null
+++ b/releasenotes/notes/add-volume-quota-class-client-as-library-c4c2b22c36ff807e.yaml
@@ -0,0 +1,8 @@
+---
+features:
+ - |
+ Define v2 quota_classes_client for the volume service as library
+ interfaces, allowing other projects to use this module as stable libraries
+ without maintenance changes.
+
+ * quota_classes_client(v2)
diff --git a/releasenotes/notes/add_proxy_url_get_credentials-aef66b085450513f.yaml b/releasenotes/notes/add_proxy_url_get_credentials-aef66b085450513f.yaml
new file mode 100644
index 0000000..94ab462
--- /dev/null
+++ b/releasenotes/notes/add_proxy_url_get_credentials-aef66b085450513f.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Add the proxy_url optional parameter to the get_credentials method in
+ tempest/lib/auth.py so that that helper can be used when going through
+ and HTTP proxy.
diff --git a/releasenotes/notes/api_v2_admin_flag-dea5ca9bc2ce63bc.yaml b/releasenotes/notes/api_v2_admin_flag-dea5ca9bc2ce63bc.yaml
new file mode 100644
index 0000000..0c33b69
--- /dev/null
+++ b/releasenotes/notes/api_v2_admin_flag-dea5ca9bc2ce63bc.yaml
@@ -0,0 +1,7 @@
+---
+features:
+ - |
+ A new configuration flag api_v2_admin is introduced in the identity
+ feature flag group to allow for enabling/disabling all identity v2
+ admin tests. The new flag only applies when the existing api_v2 flag
+ is set to True
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/cli-tests-v3fixes-fb38189cefd64213.yaml b/releasenotes/notes/cli-tests-v3fixes-fb38189cefd64213.yaml
new file mode 100644
index 0000000..2a0a86c
--- /dev/null
+++ b/releasenotes/notes/cli-tests-v3fixes-fb38189cefd64213.yaml
@@ -0,0 +1,9 @@
+---
+other:
+ - |
+ The CLIClient class, when it calls a command line client, uses
+ ``--os-project-name`` instead of ``--os-tenant-name`` for the
+ project, and passes ``--os-identity-api-version`` (default empty).
+ All CLI clients still available in supported releases of OpenStack
+ which are wrapped by the ``cmd_with_auth()`` method support those
+ switches.
diff --git a/releasenotes/notes/compare-header-version-func-de5139b2161b3627.yaml b/releasenotes/notes/compare-header-version-func-de5139b2161b3627.yaml
new file mode 100644
index 0000000..cc6c51b
--- /dev/null
+++ b/releasenotes/notes/compare-header-version-func-de5139b2161b3627.yaml
@@ -0,0 +1,15 @@
+---
+features:
+ - |
+ Add a new function called ``compare_version_header_to_response`` to
+ ``tempest.lib.common.api_version_utils``, which compares the API
+ microversion in the response header to another microversion using the
+ comparators defined in
+ ``tempest.lib.common.api_version_request.APIVersionRequest``.
+
+ It is now possible to determine how to retrieve an attribute from a
+ response body of an API call, depending on the returned microversion.
+
+ Add a new exception type called ``InvalidParam`` to
+ ``tempest.lib.exceptions``, allowing the possibility of raising an
+ exception if an invalid parameter is passed to a library function.
diff --git a/releasenotes/notes/config-volume-multiattach-ea8138dfa4fd308c.yaml b/releasenotes/notes/config-volume-multiattach-ea8138dfa4fd308c.yaml
new file mode 100644
index 0000000..8d53dda
--- /dev/null
+++ b/releasenotes/notes/config-volume-multiattach-ea8138dfa4fd308c.yaml
@@ -0,0 +1,12 @@
+---
+other:
+ - |
+ A new configuration option ``[compute-feature-enabled]/volume_multiattach``
+ has been added which defaults to False. Set this to True to enable volume
+ multiattach testing. These tests require that compute API version 2.60 is
+ available and block storage API version 3.44 is available.
+
+ .. note:: In the Queens release, the only compute driver that supports
+ volume multiattach is the libvirt driver, and only then when qemu<2.10
+ or libvirt>=3.10. The only volume backend in Queens that supports volume
+ multiattach is lvm.
diff --git a/releasenotes/notes/create-mount-config-drive-to-lib-1a6e912b8afbcc7e.yaml b/releasenotes/notes/create-mount-config-drive-to-lib-1a6e912b8afbcc7e.yaml
new file mode 100644
index 0000000..f92cd78
--- /dev/null
+++ b/releasenotes/notes/create-mount-config-drive-to-lib-1a6e912b8afbcc7e.yaml
@@ -0,0 +1,4 @@
+---
+features:
+ - A function has been added to the common library to allow mounting and
+ unmounting of the config drive consistently.
diff --git a/releasenotes/notes/credentials-factory-stable-c8037bd9ae642482.yaml b/releasenotes/notes/credentials-factory-stable-c8037bd9ae642482.yaml
new file mode 100644
index 0000000..6faa536
--- /dev/null
+++ b/releasenotes/notes/credentials-factory-stable-c8037bd9ae642482.yaml
@@ -0,0 +1,10 @@
+---
+features:
+ - |
+ The credentials_factory.py module is now marked as stable for Tempest
+ plugins. It provides helpers that can be used by Tempest plugins to
+ obtain test credentials for their test cases in a format that honors the
+ Tempest configuration in use.
+ Credentials may be provisioned on the fly during the test run, or they
+ can be setup in advance and fed to test via a YAML file; they can be
+ setup for identity v2 or identity v3.
diff --git a/releasenotes/notes/deprecate-compute-images-client-in-volume-tests-92b6dd55fcaba620.yaml b/releasenotes/notes/deprecate-compute-images-client-in-volume-tests-92b6dd55fcaba620.yaml
new file mode 100644
index 0000000..1ae251c
--- /dev/null
+++ b/releasenotes/notes/deprecate-compute-images-client-in-volume-tests-92b6dd55fcaba620.yaml
@@ -0,0 +1,10 @@
+---
+deprecations:
+ - |
+ Image APIs in compute are deprecated, Image native APIs are recommended.
+ And Glance v1 APIs are deprecated and v2 APIs are current. Image client
+ compute_images_client and Glance v1 APIs are removed in volume tests.
+upgrade:
+ - |
+ Switch to use Glance v2 APIs in volume tests, by adding the Glance v2
+ client images_client.
diff --git a/releasenotes/notes/deprecate-config-forbid_global_implied_dsr-e64cfa66e6e3ded5.yaml b/releasenotes/notes/deprecate-config-forbid_global_implied_dsr-e64cfa66e6e3ded5.yaml
new file mode 100644
index 0000000..2b63402
--- /dev/null
+++ b/releasenotes/notes/deprecate-config-forbid_global_implied_dsr-e64cfa66e6e3ded5.yaml
@@ -0,0 +1,5 @@
+---
+deprecations:
+ - The config option ``forbid_global_implied_dsr`` from the ``IdentityFeature``
+ group is now deprecated. This feature flag was introduced to support
+ testing of old OpenStack versions which are not supported anymore.
diff --git a/releasenotes/notes/deprecate-default-value-for-v3_endpoint_type-fb9e47c5ba1c719d.yaml b/releasenotes/notes/deprecate-default-value-for-v3_endpoint_type-fb9e47c5ba1c719d.yaml
new file mode 100644
index 0000000..c88522e
--- /dev/null
+++ b/releasenotes/notes/deprecate-default-value-for-v3_endpoint_type-fb9e47c5ba1c719d.yaml
@@ -0,0 +1,6 @@
+---
+deprecations:
+ - |
+ Deprecate default value for configuration parameter v3_endpoint_type
+ of identity section in OpenStack Pike and modify the default value to
+ publicURL in OpenStack Q release.
diff --git a/releasenotes/notes/disable-identity-v2-testing-4ef1565d1a5aedcf.yaml b/releasenotes/notes/disable-identity-v2-testing-4ef1565d1a5aedcf.yaml
new file mode 100644
index 0000000..e5d4ab7
--- /dev/null
+++ b/releasenotes/notes/disable-identity-v2-testing-4ef1565d1a5aedcf.yaml
@@ -0,0 +1,7 @@
+---
+upgrade:
+ - |
+ As of the Queens release, tempest no longer tests the identity v2.0 API
+ because the majority of the v2.0 API have been removed from the identity
+ project. Once the Queens release reaches end-of-life, we can remove the
+ v2.0 tempest tests and clean up v2.0 testing cruft.
diff --git a/releasenotes/notes/drop-DEFAULT_PARAMS-bfcc2e7b74ef880b.yaml b/releasenotes/notes/drop-DEFAULT_PARAMS-bfcc2e7b74ef880b.yaml
new file mode 100644
index 0000000..c9a49a7
--- /dev/null
+++ b/releasenotes/notes/drop-DEFAULT_PARAMS-bfcc2e7b74ef880b.yaml
@@ -0,0 +1,13 @@
+---
+upgrade:
+ - |
+ Replace any call in your code to credentials_factory.DEFAULT_PARAMS with
+ a call to config.service_client_config().
+fixes:
+ - |
+ The credentials_factory module used to load configuration at import time
+ which caused configuration being loaded at test discovery time.
+ This was fixed by removing the DEFAULT_PARAMS variable. This variable
+ was redundant (and outdated), the same dictionary (but up to date) can
+ be obtained via invoking config.service_client_config() with no service
+ parameter.
diff --git a/releasenotes/notes/extra-compute-services-tests-92b6c0618972e02f.yaml b/releasenotes/notes/extra-compute-services-tests-92b6c0618972e02f.yaml
new file mode 100644
index 0000000..414adf1
--- /dev/null
+++ b/releasenotes/notes/extra-compute-services-tests-92b6c0618972e02f.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Add the ``disable_log_reason`` and the ``update_forced_down`` API endpoints
+ to the compute ``services_client``.
+ Add '2.11' compute validation schema for compute services API.
diff --git a/releasenotes/notes/fix-list-group-snapshots-api-969d9321002c566c.yaml b/releasenotes/notes/fix-list-group-snapshots-api-969d9321002c566c.yaml
new file mode 100644
index 0000000..a002fb8
--- /dev/null
+++ b/releasenotes/notes/fix-list-group-snapshots-api-969d9321002c566c.yaml
@@ -0,0 +1,6 @@
+---
+fixes:
+ - |
+ Fix list_group_snapshots API in v3 group_snapshots_client: Bug#1715786.
+ The url path for list group snapshots with details API is changed from
+ ``?detail=True`` to ``/detail``.
diff --git a/releasenotes/notes/fix-remoteclient-default-ssh-shell-prologue-33e99343d086f601.yaml b/releasenotes/notes/fix-remoteclient-default-ssh-shell-prologue-33e99343d086f601.yaml
new file mode 100644
index 0000000..5063fd5
--- /dev/null
+++ b/releasenotes/notes/fix-remoteclient-default-ssh-shell-prologue-33e99343d086f601.yaml
@@ -0,0 +1,7 @@
+---
+fixes:
+ - |
+ Fix RemoteClient default ssh_shell_prologue: Bug#1707478
+
+ The default ssh_shell_proloque has been modified from
+ specifying erroneous PATH=$$PATH:/sbin to PATH=$PATH:/sbin.
diff --git a/releasenotes/notes/fix-show-image-file-expected-code-92d97342d0f6d60e.yaml b/releasenotes/notes/fix-show-image-file-expected-code-92d97342d0f6d60e.yaml
new file mode 100644
index 0000000..a4050a5
--- /dev/null
+++ b/releasenotes/notes/fix-show-image-file-expected-code-92d97342d0f6d60e.yaml
@@ -0,0 +1,6 @@
+---
+fixes:
+ - |
+ Fix show_image_file interface in v2 ImagesClient: Bug#1756264.
+ The expected success code of show_image_file is changed from
+ ``200`` to ``[200, 204, 206]``.
diff --git a/releasenotes/notes/http_proxy_config-cb39b55520e84db5.yaml b/releasenotes/notes/http_proxy_config-cb39b55520e84db5.yaml
new file mode 100644
index 0000000..56969de
--- /dev/null
+++ b/releasenotes/notes/http_proxy_config-cb39b55520e84db5.yaml
@@ -0,0 +1,9 @@
+---
+features:
+ - Adds a new config options, ``proxy_url``. This options is used to configure
+ running tempest through a proxy server.
+ - The RestClient class in tempest.lib.rest_client has a new kwarg parameters,
+ ``proxy_url``, that is used to set a proxy server.
+ - A new class was added to tempest.lib.http, ClosingProxyHttp. This behaves
+ identically to ClosingHttp except that it requires a proxy url and will
+ establish a connection through a proxy
diff --git a/releasenotes/notes/identity-tests-domain-drivers-76235f6672221e45.yaml b/releasenotes/notes/identity-tests-domain-drivers-76235f6672221e45.yaml
new file mode 100644
index 0000000..7ed3081
--- /dev/null
+++ b/releasenotes/notes/identity-tests-domain-drivers-76235f6672221e45.yaml
@@ -0,0 +1,7 @@
+---
+features:
+ - |
+ A new boolean config option ``domain_specific_drivers``
+ is added to the section ``identity-feature-enabled``.
+ This option must be enabled when testing an environment that
+ is configured to use domain-specific identity drivers.
diff --git a/releasenotes/notes/identity-token-client-8aaef74b1d61090a.yaml b/releasenotes/notes/identity-token-client-8aaef74b1d61090a.yaml
new file mode 100644
index 0000000..d94de3e
--- /dev/null
+++ b/releasenotes/notes/identity-token-client-8aaef74b1d61090a.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Add additional API endpoints to the identity v2 client token API:
+ - list_endpoints_for_token
+ - check_token_existence
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/identity_client-635275d43abbb807.yaml b/releasenotes/notes/identity_client-635275d43abbb807.yaml
new file mode 100644
index 0000000..6f984b7
--- /dev/null
+++ b/releasenotes/notes/identity_client-635275d43abbb807.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - |
+ Enhances the v3 identity client with the ``check_token_existence``
+ endpoint, allowing users to check the existence of tokens
diff --git a/releasenotes/notes/intermediate-pike-release-2ce492432ff8f012.yaml b/releasenotes/notes/intermediate-pike-release-2ce492432ff8f012.yaml
new file mode 100644
index 0000000..bfebcd9
--- /dev/null
+++ b/releasenotes/notes/intermediate-pike-release-2ce492432ff8f012.yaml
@@ -0,0 +1,4 @@
+---
+prelude: >
+ This is an intermediate release during the Pike development cycle to
+ make new functionality available to plugins and other consumers.
diff --git a/releasenotes/notes/intermediate-queens-release-2f9f305775fca454.yaml b/releasenotes/notes/intermediate-queens-release-2f9f305775fca454.yaml
new file mode 100644
index 0000000..1493b0b
--- /dev/null
+++ b/releasenotes/notes/intermediate-queens-release-2f9f305775fca454.yaml
@@ -0,0 +1,4 @@
+---
+prelude: >
+ This is an intermediate release during the Queens development cycle to
+ make new functionality available to plugins and other consumers.
diff --git a/releasenotes/notes/list-auth-domains-v3-endpoint-9ec60c7d3011c397.yaml b/releasenotes/notes/list-auth-domains-v3-endpoint-9ec60c7d3011c397.yaml
new file mode 100644
index 0000000..0f104cf
--- /dev/null
+++ b/releasenotes/notes/list-auth-domains-v3-endpoint-9ec60c7d3011c397.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Add ``list_auth_domains`` API endpoint to the identity v3 client. This
+ allows the possibility of listing all domains a user has access to
+ via role assignments.
diff --git a/releasenotes/notes/make-object-storage-client-as-stable-interface-d1b07c7e8f17bef6.yaml b/releasenotes/notes/make-object-storage-client-as-stable-interface-d1b07c7e8f17bef6.yaml
new file mode 100644
index 0000000..2bba952
--- /dev/null
+++ b/releasenotes/notes/make-object-storage-client-as-stable-interface-d1b07c7e8f17bef6.yaml
@@ -0,0 +1,11 @@
+---
+features:
+ - |
+ Define below object storage service clients as libraries.
+ Add new service clients to the library interface so the
+ other projects can use these modules as stable libraries
+ without any maintenance changes.
+
+ * account_client
+ * container_client
+ * object_client
diff --git a/releasenotes/notes/migrate-dynamic-creds-ecebb47528080761.yaml b/releasenotes/notes/migrate-dynamic-creds-ecebb47528080761.yaml
new file mode 100644
index 0000000..c20cbc6
--- /dev/null
+++ b/releasenotes/notes/migrate-dynamic-creds-ecebb47528080761.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - |
+ The tempest module tempest.common.dynamic creds which is used for
+ dynamically allocating credentials has been migrated into tempest lib.
diff --git a/releasenotes/notes/migrate-object-storage-as-stable-interface-42014c7b43ecb254.yaml b/releasenotes/notes/migrate-object-storage-as-stable-interface-42014c7b43ecb254.yaml
new file mode 100644
index 0000000..72b8e26
--- /dev/null
+++ b/releasenotes/notes/migrate-object-storage-as-stable-interface-42014c7b43ecb254.yaml
@@ -0,0 +1,10 @@
+---
+features:
+ - |
+ Define below object storage service clients as libraries.
+ Add new service clients to the library interface so the
+ other projects can use these modules as stable libraries
+ without any maintenance changes.
+
+ * bulk_middleware_client
+ * capabilities_client
diff --git a/releasenotes/notes/migrate-preprov-creds-ef61a046ee1ec604.yaml b/releasenotes/notes/migrate-preprov-creds-ef61a046ee1ec604.yaml
new file mode 100644
index 0000000..aa5f71a
--- /dev/null
+++ b/releasenotes/notes/migrate-preprov-creds-ef61a046ee1ec604.yaml
@@ -0,0 +1,11 @@
+---
+features:
+ - The tempest module tempest.common.preprov_creds which is used to provide
+ credentials from a list of preprovisioned resources has been migrated into
+ tempest lib at tempest.lib.common.preprov_creds.
+ - The InvalidTestResource exception class from tempest.exceptions has been
+ migrated into tempest.lib.exceptions
+ - The tempest module tempest.common.fixed_network which provided utilities for
+ finding fixed networks by and helpers for picking the network to use when
+ multiple tenant networks are available has been migrated into tempest lib
+ at tempest.lib.common.fixed_network.
diff --git a/releasenotes/notes/move-attr-decorator-to-lib-a1e80c42ba9c5392.yaml b/releasenotes/notes/move-attr-decorator-to-lib-a1e80c42ba9c5392.yaml
new file mode 100644
index 0000000..58d45cc
--- /dev/null
+++ b/releasenotes/notes/move-attr-decorator-to-lib-a1e80c42ba9c5392.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ A new ``attr`` decorator has been added in the ``tempest.lib.decorators``
+ module. For example, use it to tag specific tests, which could be leveraged
+ by test runners to run only a subset of Tempest tests.
diff --git a/releasenotes/notes/move-related_bug-decorator-to-lib-dbfd5c543bbb2805.yaml b/releasenotes/notes/move-related_bug-decorator-to-lib-dbfd5c543bbb2805.yaml
new file mode 100644
index 0000000..8c420c8
--- /dev/null
+++ b/releasenotes/notes/move-related_bug-decorator-to-lib-dbfd5c543bbb2805.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ A new ``related_bug`` decorator has been added to
+ ``tempest.lib.decorators``. Use it to decorate and tag a test that was
+ added in relation to a launchpad bug report.
diff --git a/releasenotes/notes/move-volume-v3-base_client-to-volume-1edbz0f207c3b283.yaml b/releasenotes/notes/move-volume-v3-base_client-to-volume-1edbz0f207c3b283.yaml
new file mode 100644
index 0000000..ec81dc5
--- /dev/null
+++ b/releasenotes/notes/move-volume-v3-base_client-to-volume-1edbz0f207c3b283.yaml
@@ -0,0 +1,15 @@
+features:
+ - |
+ Move base_client from tempest.lib.services.volume.v3 to
+ tempest.lib.services.volume, so if we want to add new
+ interfaces based on a v2 client, we can make that v2
+ client inherit from volume.base_client.BaseClient to
+ get microversion support, and then to make the new v3
+ client inherit from the v2 client, thus to avoid the
+ multiple inheritance.
+deprecations:
+ - |
+ Deprecate class BaseClient from volume.v3.base_client
+ and move it to volume.base_client.
+ ``tempest.lib.services.volume.v3.base_client.BaseClient``
+ (new ``tempest.lib.services.volume.base_client.BaseClient``)
diff --git a/releasenotes/notes/network-tag-client-f4614029af7927f0.yaml b/releasenotes/notes/network-tag-client-f4614029af7927f0.yaml
new file mode 100644
index 0000000..9af57b1
--- /dev/null
+++ b/releasenotes/notes/network-tag-client-f4614029af7927f0.yaml
@@ -0,0 +1,8 @@
+---
+features:
+ - |
+ Define v2.0 ``tags_client`` for the network service as a library
+ interface, allowing other projects to use this module as a stable
+ library without maintenance changes.
+
+ * tags_client(v2.0)
diff --git a/releasenotes/notes/pause_teardown-45c9d60ffa889f7f.yaml b/releasenotes/notes/pause_teardown-45c9d60ffa889f7f.yaml
new file mode 100644
index 0000000..a540c7d
--- /dev/null
+++ b/releasenotes/notes/pause_teardown-45c9d60ffa889f7f.yaml
@@ -0,0 +1,9 @@
+---
+features:
+ - |
+ Pause teardown
+ When pause_teardown flag in tempest.conf is set to True a pdb breakpoint
+ is added to tearDown and tearDownClass methods in test.py.
+ This allows to pause cleaning resources process, so that used resources
+ can be examined. Closer examination of used resources may lead to faster
+ debugging.
diff --git a/releasenotes/notes/plugin-client-registration-enhancements-e09131742391225b.yaml b/releasenotes/notes/plugin-client-registration-enhancements-e09131742391225b.yaml
new file mode 100644
index 0000000..b6391b6
--- /dev/null
+++ b/releasenotes/notes/plugin-client-registration-enhancements-e09131742391225b.yaml
@@ -0,0 +1,12 @@
+---
+features:
+ - |
+ When registering service clients from installed plugins, all registrations
+ are now processed, even if one or more fails. All exceptions encountered
+ during the registration process are recorded. If at least one exception
+ was encountered, the registration process fails and all interim errors are
+ reported.
+ - |
+ The __repr__ method is now implemented for the base `tempest.Exception`
+ class, its implementation is identical to __str__: it reports the error
+ message merged with input parameters.
diff --git a/releasenotes/notes/prevent-error-in-parse-resp-when-nullable-list-9898cd0f22180986.yaml b/releasenotes/notes/prevent-error-in-parse-resp-when-nullable-list-9898cd0f22180986.yaml
new file mode 100644
index 0000000..afb6006
--- /dev/null
+++ b/releasenotes/notes/prevent-error-in-parse-resp-when-nullable-list-9898cd0f22180986.yaml
@@ -0,0 +1,6 @@
+---
+fixes:
+ - |
+ When receiving nullable list as a response body, tempest.lib
+ rest_client module raised an exception without valid json
+ deserialization. A new release fixes this bug.
diff --git a/releasenotes/notes/raise-exception-when-error-deleting-on-volume-18d0d0c5886212dd.yaml b/releasenotes/notes/raise-exception-when-error-deleting-on-volume-18d0d0c5886212dd.yaml
new file mode 100644
index 0000000..194dbc1
--- /dev/null
+++ b/releasenotes/notes/raise-exception-when-error-deleting-on-volume-18d0d0c5886212dd.yaml
@@ -0,0 +1,8 @@
+---
+upgrade:
+ - |
+ Tempest checks a volume delete by waiting for NotFound(404) on
+ show_volume(). Sometime a volume delete fails and the volume status
+ becomes error_deleting which means the delete is failed.
+ So Tempest doesn't need to wait anymore. A new release of Tempest
+ raises an exception DeleteErrorException instead of waiting.
diff --git a/releasenotes/notes/removal-deprecated-config-options-3db535b979fe3509.yaml b/releasenotes/notes/removal-deprecated-config-options-3db535b979fe3509.yaml
new file mode 100644
index 0000000..e15d387
--- /dev/null
+++ b/releasenotes/notes/removal-deprecated-config-options-3db535b979fe3509.yaml
@@ -0,0 +1,12 @@
+---
+upgrade:
+ - |
+ Below config options or feature flags were deprecated for removal.
+ It's time to remove them as all supported stable branches are
+ good to handle them.
+
+ * ``[identity-feature-enabled].forbid_global_implied_dsr``
+ * ``[image-feature-enabled].deactivate_image``
+ * ``[default].resources_prefix``
+ * config group ``orchestration``
+ * ``[service_available].heat``
diff --git a/releasenotes/notes/remove-deprecated-apis-from-v2-volumes-client-3ca4a5db5fea518f.yaml b/releasenotes/notes/remove-deprecated-apis-from-v2-volumes-client-3ca4a5db5fea518f.yaml
new file mode 100644
index 0000000..c75da2e
--- /dev/null
+++ b/releasenotes/notes/remove-deprecated-apis-from-v2-volumes-client-3ca4a5db5fea518f.yaml
@@ -0,0 +1,11 @@
+---
+upgrade:
+ - |
+ Remove deprecated APIs from volume v2 volumes_client, and the deprecated
+ APIs are re-realized in volume v2 transfers_client.
+
+ * create_volume_transfer
+ * show_volume_transfer
+ * list_volume_transfers
+ * delete_volume_transfer
+ * accept_volume_transfer
diff --git a/releasenotes/notes/remove-deprecated-skip-decorators-f8b42d812d20b537.yaml b/releasenotes/notes/remove-deprecated-skip-decorators-f8b42d812d20b537.yaml
new file mode 100644
index 0000000..920bc5d
--- /dev/null
+++ b/releasenotes/notes/remove-deprecated-skip-decorators-f8b42d812d20b537.yaml
@@ -0,0 +1,5 @@
+---
+upgrade:
+ - |
+ Remove two deprecated skip decorators in ``config`` module:
+ ``skip_unless_config`` and ``skip_if_config``.
diff --git a/releasenotes/notes/remove-deprecated-skip_unless_attr-decorator-02bde59a00328f5c.yaml b/releasenotes/notes/remove-deprecated-skip_unless_attr-decorator-02bde59a00328f5c.yaml
new file mode 100644
index 0000000..621731d
--- /dev/null
+++ b/releasenotes/notes/remove-deprecated-skip_unless_attr-decorator-02bde59a00328f5c.yaml
@@ -0,0 +1,4 @@
+---
+upgrade:
+ - |
+ Remove the deprecated decorator ``skip_unless_attr`` in lib/decorators.py.
diff --git a/releasenotes/notes/remove-deprecated-volume-apis-from-v2-volumes-client-cf35e5b4cca89860.yaml b/releasenotes/notes/remove-deprecated-volume-apis-from-v2-volumes-client-cf35e5b4cca89860.yaml
new file mode 100644
index 0000000..12ac5b5
--- /dev/null
+++ b/releasenotes/notes/remove-deprecated-volume-apis-from-v2-volumes-client-cf35e5b4cca89860.yaml
@@ -0,0 +1,7 @@
+---
+upgrade:
+ - |
+ Remove deprecated APIs (``show_pools`` and ``show_backend_capabilities``)
+ from volume v2 volumes_client, and the deprecated APIs are re-realized in
+ volume v2 scheduler_stats_client (``list_pools``) and capabilities_client
+ (``show_backend_capabilities``) accordingly.
diff --git a/releasenotes/notes/remove-get-ipv6-addr-by-EUI64-c79972d799c7a430.yaml b/releasenotes/notes/remove-get-ipv6-addr-by-EUI64-c79972d799c7a430.yaml
new file mode 100644
index 0000000..609000c
--- /dev/null
+++ b/releasenotes/notes/remove-get-ipv6-addr-by-EUI64-c79972d799c7a430.yaml
@@ -0,0 +1,5 @@
+---
+upgrade:
+ - |
+ Remove deprecated get_ipv6_addr_by_EUI64 method from data_utils.
+ Use the same method from oslo_utils.netutils.
diff --git a/releasenotes/notes/remove-heat-tests-9efb42cac3e0b306.yaml b/releasenotes/notes/remove-heat-tests-9efb42cac3e0b306.yaml
new file mode 100644
index 0000000..4d0f3ce
--- /dev/null
+++ b/releasenotes/notes/remove-heat-tests-9efb42cac3e0b306.yaml
@@ -0,0 +1,4 @@
+---
+upgrade:
+ - The Heat API tests have been removed from tempest, they were unmaintained.
+ The future direction of api test for heat is their in-tree Gabbi tests
diff --git a/releasenotes/notes/remove-support-of-py34-7d59fdb431fefe24.yaml b/releasenotes/notes/remove-support-of-py34-7d59fdb431fefe24.yaml
new file mode 100644
index 0000000..093228a
--- /dev/null
+++ b/releasenotes/notes/remove-support-of-py34-7d59fdb431fefe24.yaml
@@ -0,0 +1,5 @@
+---
+deprecations:
+ - |
+ Remove the support of python3.4, because in Ubuntu Xenial only
+ python3.5 is available (python3.4 is restricted to <= Mitaka).
diff --git a/releasenotes/notes/removed-tox-ostestr-8997a93d199c44f3.yaml b/releasenotes/notes/removed-tox-ostestr-8997a93d199c44f3.yaml
new file mode 100644
index 0000000..17866e5
--- /dev/null
+++ b/releasenotes/notes/removed-tox-ostestr-8997a93d199c44f3.yaml
@@ -0,0 +1,9 @@
+---
+upgrade:
+ - |
+ The tox ostestr job (normally invoked with ``tox -eostestr``) has been
+ removed. This was lightly used, and in the near future ostestr will be
+ removed from the tempest requirements file. If you were relying on this
+ functionality you can replicate it by using the venv-tempest tox job. For
+ example, simply running ``tox -evenv-tempest -- ostestr`` will do the same
+ thing the old ostestr job did.
diff --git a/releasenotes/notes/set-cinder-api-v3-option-true-1b3e61e3129b7c00.yaml b/releasenotes/notes/set-cinder-api-v3-option-true-1b3e61e3129b7c00.yaml
new file mode 100644
index 0000000..6959ca7
--- /dev/null
+++ b/releasenotes/notes/set-cinder-api-v3-option-true-1b3e61e3129b7c00.yaml
@@ -0,0 +1,5 @@
+---
+upgrade:
+ - |
+ The volume config option 'api_v3' default is changed to
+ ``True`` because the volume v3 API is CURRENT.
diff --git a/releasenotes/notes/start-of-pike-support-f2a1b7ea8e8b0311.yaml b/releasenotes/notes/start-of-pike-support-f2a1b7ea8e8b0311.yaml
new file mode 100644
index 0000000..0787821
--- /dev/null
+++ b/releasenotes/notes/start-of-pike-support-f2a1b7ea8e8b0311.yaml
@@ -0,0 +1,11 @@
+---
+prelude: >
+ This release marks the start of support for the Pike release in Tempest.
+other:
+ - OpenStack Releases supported after this release are **Pike**, **Ocata**,
+ and **Newton**.
+
+ The release under current development of this tag is Queens, meaning
+ that every Tempest commit is also tested against master during the Queens
+ cycle. However, this does not necessarily mean that using Tempest as of
+ this tag will work against a Queens (or future release) cloud.
diff --git a/releasenotes/notes/start-of-queens-support-fea9051ba1d85fc7.yaml b/releasenotes/notes/start-of-queens-support-fea9051ba1d85fc7.yaml
new file mode 100644
index 0000000..10b2300
--- /dev/null
+++ b/releasenotes/notes/start-of-queens-support-fea9051ba1d85fc7.yaml
@@ -0,0 +1,12 @@
+---
+prelude: >
+ This release marks the start of Queens release support in Tempest.
+ This release also marks the end of support for Newton in Tempest.
+other:
+ - OpenStack Releases supported after this release are **Queens**, **Pike**,
+ and **Ocata**.
+
+ The release under current development of this tag is Rocky, meaning
+ that every Tempest commit is also tested against master during the Rocky
+ cycle. However, this does not necessarily mean that using Tempest as of
+ this tag will work against a Rocky (or future release) cloud.
diff --git a/releasenotes/notes/switch-to-stestr-8c9f834b3f5a55d8.yaml b/releasenotes/notes/switch-to-stestr-8c9f834b3f5a55d8.yaml
new file mode 100644
index 0000000..9e2f1ba
--- /dev/null
+++ b/releasenotes/notes/switch-to-stestr-8c9f834b3f5a55d8.yaml
@@ -0,0 +1,13 @@
+---
+features:
+- The Tempest CLI commands have switched from calling testrepository internally
+ to use stestr instead. This means that all of the features and bug fixes from
+ moving to stestr are available to the tempest commands.
+
+upgrade:
+- Tempest CLI commands will no long rely on anything from testr. This means any
+ data in existing testr internals that were being exposed are no longer
+ present. For example things like the .testr directories will be silently
+ ignored. There is a potential incompatibility for existing users who are
+ relying on test results being stored by testr. Anything relying on previous
+ testr behavior will need to be updated to handle stestr.
diff --git a/releasenotes/notes/tempest-identity-catalog-client-f5c8589a9d7c1eb5.yaml b/releasenotes/notes/tempest-identity-catalog-client-f5c8589a9d7c1eb5.yaml
new file mode 100644
index 0000000..dcaaceb
--- /dev/null
+++ b/releasenotes/notes/tempest-identity-catalog-client-f5c8589a9d7c1eb5.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - Add a new identity catalog client. At this point, the new client
+ contains a single functionality, "show_catalog", which returns a
+ catalog object.
diff --git a/releasenotes/notes/tempest-run-fix-updates-564b41706decbba1.yaml b/releasenotes/notes/tempest-run-fix-updates-564b41706decbba1.yaml
new file mode 100644
index 0000000..0f9a0f6
--- /dev/null
+++ b/releasenotes/notes/tempest-run-fix-updates-564b41706decbba1.yaml
@@ -0,0 +1,9 @@
+---
+features:
+ - |
+ Adds a new CLI arg in tempest run, ``--black-regex``, which is a
+ regex to exclude the tests that match it.
+fixes:
+ - |
+ Fixes tempest run CLI args mutually exclusive behavior which should not
+ be the case anymore (Bug#1751201).
diff --git a/releasenotes/notes/tempest-workspace-delete-directory-feature-74d6d157a5a05561.yaml b/releasenotes/notes/tempest-workspace-delete-directory-feature-74d6d157a5a05561.yaml
new file mode 100644
index 0000000..c69ed50
--- /dev/null
+++ b/releasenotes/notes/tempest-workspace-delete-directory-feature-74d6d157a5a05561.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - |
+ Added tempest workspace remove ``--name <workspace_name> --rmdir``
+ feature to delete the workspace directory as well as entry.
diff --git a/releasenotes/notes/test-clients-stable-for-plugin-90b1e7dc83f28ccd.yaml b/releasenotes/notes/test-clients-stable-for-plugin-90b1e7dc83f28ccd.yaml
new file mode 100644
index 0000000..e27ee33
--- /dev/null
+++ b/releasenotes/notes/test-clients-stable-for-plugin-90b1e7dc83f28ccd.yaml
@@ -0,0 +1,8 @@
+---
+features:
+ - |
+ Two extra modules are now marked as stable for plugins, test.py and clients.py.
+ The former includes the test base class with its automatic credentials
+ provisioning and test resource managing fixtures.
+ The latter is built on top of ServiceClients and it adds aliases and a few custom
+ configurations to it.
diff --git a/releasenotes/notes/use-cinder-v3-client-for-verify_tempest_config-2bf3d817b0070064.yaml b/releasenotes/notes/use-cinder-v3-client-for-verify_tempest_config-2bf3d817b0070064.yaml
new file mode 100644
index 0000000..1c8fa77
--- /dev/null
+++ b/releasenotes/notes/use-cinder-v3-client-for-verify_tempest_config-2bf3d817b0070064.yaml
@@ -0,0 +1,6 @@
+---
+upgrade:
+ - verify_tempest_config command starts using extension_client of
+ cinder v2 API only, because cinder v3 API is current and v2 and
+ v1 are deprecated and v3 extension API is the same as v2. Then
+ we can reuse the v2 client for v3 API also.
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-backed-live-mig-5a38b496ba1ec093.yaml b/releasenotes/notes/volume-backed-live-mig-5a38b496ba1ec093.yaml
new file mode 100644
index 0000000..ddd1704
--- /dev/null
+++ b/releasenotes/notes/volume-backed-live-mig-5a38b496ba1ec093.yaml
@@ -0,0 +1,7 @@
+---
+features:
+ - |
+ A new boolean configuration option
+ ``[compute-feature-enabled]/volume_backed_live_migration`` has been added.
+ If enabled, tests which validate the behavior of Nova's *volume-backed live
+ migration* feature will be executed. The option defaults to ``False``.
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/tempest/api/baremetal/__init__.py b/releasenotes/source/_static/.placeholder
similarity index 100%
copy from tempest/api/baremetal/__init__.py
copy to releasenotes/source/_static/.placeholder
diff --git a/tempest/tests/services/__init__.py b/releasenotes/source/_templates/.placeholder
similarity index 100%
copy from tempest/tests/services/__init__.py
copy to releasenotes/source/_templates/.placeholder
diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py
new file mode 100644
index 0000000..57ec7e1
--- /dev/null
+++ b/releasenotes/source/conf.py
@@ -0,0 +1,278 @@
+# 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.
+
+# Tempest Release Notes documentation build configuration file, created by
+# sphinx-quickstart on Tue Nov 3 17:40:50 2015.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+# sys.path.insert(0, os.path.abspath('.'))
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+# needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ 'openstackdocstheme',
+ 'reno.sphinxext',
+]
+
+# openstackdocstheme options
+repository_name = 'openstack/tempest'
+bug_project = 'tempest'
+bug_tag = ''
+
+# Must set this variable to include year, month, day, hours, and minutes.
+html_last_updated_fmt = '%Y-%m-%d %H:%M'
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+# source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'tempest Release Notes'
+copyright = u'2016, tempest Developers'
+
+# Release do not need a version number in the title, they
+# cover multiple versions.
+# The full version, including alpha/beta/rc tags.
+release = ''
+# The short X.Y version.
+version = ''
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+# language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+# today = ''
+# Else, today_fmt is used as the format for a strftime call.
+# today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = []
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+# default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+# add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+# add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+# show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+# modindex_common_prefix = []
+
+# If true, keep warnings as "system message" paragraphs in the built documents.
+# keep_warnings = False
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+html_theme = 'openstackdocs'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+# html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# "<project> v<release> documentation".
+# html_title = None
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+# html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+# html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+# html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# Add any extra paths that contain custom files (such as robots.txt or
+# .htaccess) here, relative to this directory. These files are copied
+# directly to the root of the documentation.
+# html_extra_path = []
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+# html_last_updated_fmt = '%b %d, %Y'
+
+# Custom sidebar templates, maps document names to template names.
+# html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+# html_additional_pages = {}
+
+# If false, no module index is generated.
+# html_domain_indices = True
+
+# If false, no index is generated.
+# html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+# html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+# html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+# html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+# html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+# html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+# html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'tempestReleaseNotesdoc'
+
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+ # The paper size ('letterpaper' or 'a4paper').
+ # 'papersize': 'letterpaper',
+
+ # The font size ('10pt', '11pt' or '12pt').
+ # 'pointsize': '10pt',
+
+ # Additional stuff for the LaTeX preamble.
+ # 'preamble': '',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_documents = [
+ ('index', 'olso.configReleaseNotes.tex',
+ u'olso.config Release Notes Documentation',
+ u'tempest Developers', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page
+# latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+# latex_use_parts = False
+
+# If true, show page references after internal links.
+# latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+# latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+# latex_appendices = []
+
+# If false, no module index is generated.
+# latex_domain_indices = True
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ ('index', 'olso.configreleasenotes',
+ u'tempest Release Notes Documentation',
+ [u'tempest Developers'], 1)
+]
+
+# If true, show URL addresses after external links.
+# man_show_urls = False
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ ('index', 'tempestReleaseNotes',
+ u'tempest Release Notes Documentation',
+ u'tempest Developers', 'olso.configReleaseNotes',
+ 'An OpenStack library for parsing configuration options from the command'
+ ' line and configuration files.',
+ 'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+# texinfo_appendices = []
+
+# If false, no module index is generated.
+# texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+# texinfo_show_urls = 'footnote'
+
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+# texinfo_no_detailmenu = False
+
+# -- Options for Internationalization output ------------------------------
+locale_dirs = ['locale/']
diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst
new file mode 100644
index 0000000..2518703
--- /dev/null
+++ b/releasenotes/source/index.rst
@@ -0,0 +1,26 @@
+===========================
+ Tempest Release Notes
+===========================
+
+.. toctree::
+ :maxdepth: 1
+
+ unreleased
+ v18.0.0
+ v17.2.0
+ v17.1.0
+ v17.0.0
+ v16.1.0
+ v16.0.0
+ v15.0.0
+ v14.0.0
+ v13.0.0
+ v12.0.0
+ v11.0.0
+ v10.0.0
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`search`
diff --git a/releasenotes/source/unreleased.rst b/releasenotes/source/unreleased.rst
new file mode 100644
index 0000000..875030f
--- /dev/null
+++ b/releasenotes/source/unreleased.rst
@@ -0,0 +1,5 @@
+============================
+Current Series Release Notes
+============================
+
+.. release-notes::
diff --git a/releasenotes/source/v10.0.0.rst b/releasenotes/source/v10.0.0.rst
new file mode 100644
index 0000000..38ed2ef
--- /dev/null
+++ b/releasenotes/source/v10.0.0.rst
@@ -0,0 +1,6 @@
+=====================
+v10.0.0 Release Notes
+=====================
+
+.. release-notes:: 10.0.0 Release Notes
+ :version: 10.0.0
diff --git a/releasenotes/source/v11.0.0.rst b/releasenotes/source/v11.0.0.rst
new file mode 100644
index 0000000..84b145d
--- /dev/null
+++ b/releasenotes/source/v11.0.0.rst
@@ -0,0 +1,6 @@
+=====================
+v11.0.0 Release Notes
+=====================
+
+.. release-notes:: 11.0.0 Release Notes
+ :version: 11.0.0
diff --git a/releasenotes/source/v12.0.0.rst b/releasenotes/source/v12.0.0.rst
new file mode 100644
index 0000000..0bc8343
--- /dev/null
+++ b/releasenotes/source/v12.0.0.rst
@@ -0,0 +1,6 @@
+=====================
+v12.0.0 Release Notes
+=====================
+
+.. release-notes:: 12.0.0 Release Notes
+ :version: 12.0.0
diff --git a/releasenotes/source/v13.0.0.rst b/releasenotes/source/v13.0.0.rst
new file mode 100644
index 0000000..39816e4
--- /dev/null
+++ b/releasenotes/source/v13.0.0.rst
@@ -0,0 +1,6 @@
+=====================
+v13.0.0 Release Notes
+=====================
+
+.. release-notes:: 13.0.0 Release Notes
+ :version: 13.0.0
diff --git a/releasenotes/source/v14.0.0.rst b/releasenotes/source/v14.0.0.rst
new file mode 100644
index 0000000..440c85b
--- /dev/null
+++ b/releasenotes/source/v14.0.0.rst
@@ -0,0 +1,6 @@
+=====================
+v14.0.0 Release Notes
+=====================
+
+.. release-notes:: 14.0.0 Release Notes
+ :version: 14.0.0
diff --git a/releasenotes/source/v15.0.0.rst b/releasenotes/source/v15.0.0.rst
new file mode 100644
index 0000000..2ee1894
--- /dev/null
+++ b/releasenotes/source/v15.0.0.rst
@@ -0,0 +1,6 @@
+=====================
+v15.0.0 Release Notes
+=====================
+
+.. release-notes:: 15.0.0 Release Notes
+ :version: 15.0.0
diff --git a/releasenotes/source/v16.0.0.rst b/releasenotes/source/v16.0.0.rst
new file mode 100644
index 0000000..ae29599
--- /dev/null
+++ b/releasenotes/source/v16.0.0.rst
@@ -0,0 +1,6 @@
+=====================
+v16.0.0 Release Notes
+=====================
+
+.. release-notes:: 16.0.0 Release Notes
+ :version: 16.0.0
\ No newline at end of file
diff --git a/releasenotes/source/v16.1.0.rst b/releasenotes/source/v16.1.0.rst
new file mode 100644
index 0000000..e24a70f
--- /dev/null
+++ b/releasenotes/source/v16.1.0.rst
@@ -0,0 +1,6 @@
+=====================
+v16.1.0 Release Notes
+=====================
+
+.. release-notes:: 16.1.0 Release Notes
+ :version: 16.1.0
diff --git a/releasenotes/source/v17.0.0.rst b/releasenotes/source/v17.0.0.rst
new file mode 100644
index 0000000..3f50f11
--- /dev/null
+++ b/releasenotes/source/v17.0.0.rst
@@ -0,0 +1,6 @@
+=====================
+v17.0.0 Release Notes
+=====================
+
+.. release-notes:: 17.0.0 Release Notes
+ :version: 17.0.0
diff --git a/releasenotes/source/v17.1.0.rst b/releasenotes/source/v17.1.0.rst
new file mode 100644
index 0000000..b8fd570
--- /dev/null
+++ b/releasenotes/source/v17.1.0.rst
@@ -0,0 +1,6 @@
+=====================
+v17.1.0 Release Notes
+=====================
+
+.. release-notes:: 17.1.0 Release Notes
+ :version: 17.1.0
diff --git a/releasenotes/source/v17.2.0.rst b/releasenotes/source/v17.2.0.rst
new file mode 100644
index 0000000..8566ae4
--- /dev/null
+++ b/releasenotes/source/v17.2.0.rst
@@ -0,0 +1,6 @@
+=====================
+v17.2.0 Release Notes
+=====================
+
+.. release-notes:: 17.2.0 Release Notes
+ :version: 17.2.0
diff --git a/releasenotes/source/v18.0.0.rst b/releasenotes/source/v18.0.0.rst
new file mode 100644
index 0000000..aa05db3
--- /dev/null
+++ b/releasenotes/source/v18.0.0.rst
@@ -0,0 +1,6 @@
+=====================
+v18.0.0 Release Notes
+=====================
+
+.. release-notes:: 18.0.0 Release Notes
+ :version: 18.0.0
diff --git a/requirements.txt b/requirements.txt
index 469b294..7520d42 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,26 +1,24 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
-pbr>=1.6
-cliff>=1.15.0 # Apache-2.0
-anyjson>=0.3.3
-httplib2>=0.7.5
-jsonschema!=2.5.0,<3.0.0,>=2.0.0
-testtools>=1.4.0
-paramiko>=1.13.0
-netaddr!=0.7.16,>=0.7.12
-testrepository>=0.0.18
-pyOpenSSL>=0.14
-oslo.concurrency>=2.3.0 # Apache-2.0
-oslo.config>=2.7.0 # Apache-2.0
-oslo.i18n>=1.5.0 # Apache-2.0
-oslo.log>=1.12.0 # Apache-2.0
-oslo.serialization>=1.10.0 # Apache-2.0
-oslo.utils>=3.2.0 # Apache-2.0
-six>=1.9.0
-iso8601>=0.1.9
-fixtures>=1.3.1
-testscenarios>=0.4
-tempest-lib>=0.12.0
-PyYAML>=3.1.0
-stevedore>=1.5.0 # Apache-2.0
+pbr!=2.1.0,>=2.0.0 # Apache-2.0
+cliff!=2.9.0,>=2.8.0 # Apache-2.0
+jsonschema<3.0.0,>=2.6.0 # MIT
+testtools>=2.2.0 # MIT
+paramiko>=2.0.0 # LGPLv2.1+
+netaddr>=0.7.18 # BSD
+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
+oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0
+oslo.utils>=3.33.0 # Apache-2.0
+six>=1.10.0 # MIT
+fixtures>=3.0.0 # Apache-2.0/BSD
+PyYAML>=3.12 # MIT
+python-subunit>=1.0.0 # Apache-2.0/BSD
+stevedore>=1.20.0 # Apache-2.0
+PrettyTable<0.8,>=0.7.1 # BSD
+urllib3>=1.21.1 # MIT
+debtcollector>=1.2.0 # Apache-2.0
+unittest2>=1.1.0 # BSD
diff --git a/roles/acl-devstack-files/README.rst b/roles/acl-devstack-files/README.rst
new file mode 100644
index 0000000..76e7e58
--- /dev/null
+++ b/roles/acl-devstack-files/README.rst
@@ -0,0 +1,10 @@
+Grant global read access to devstack `files` folder.
+
+This is handy to grant the `tempest` user access to VM images for testing.
+
+**Role Variables**
+
+.. zuul:rolevar:: devstack_data_dir
+ :default: /opt/stack/data
+
+ The devstack data directory.
diff --git a/roles/acl-devstack-files/defaults/main.yaml b/roles/acl-devstack-files/defaults/main.yaml
new file mode 100644
index 0000000..14265f0
--- /dev/null
+++ b/roles/acl-devstack-files/defaults/main.yaml
@@ -0,0 +1 @@
+devstack_data_dir: /opt/stack/data
diff --git a/roles/acl-devstack-files/tasks/main.yaml b/roles/acl-devstack-files/tasks/main.yaml
new file mode 100644
index 0000000..b3eeec7
--- /dev/null
+++ b/roles/acl-devstack-files/tasks/main.yaml
@@ -0,0 +1,6 @@
+- name: Grant global read access to devstack files
+ file:
+ path: "{{devstack_data_dir}}/files"
+ mode: "o+rx"
+ recurse: yes
+ become: yes
diff --git a/roles/process-stackviz/README.rst b/roles/process-stackviz/README.rst
new file mode 100644
index 0000000..a8447d2
--- /dev/null
+++ b/roles/process-stackviz/README.rst
@@ -0,0 +1,22 @@
+Generate stackviz report.
+
+Generate stackviz report using subunit and dstat data, using
+the stackviz archive embedded in test images.
+
+**Role Variables**
+
+.. zuul:rolevar:: devstack_base_dir
+ :default: /opt/stack
+
+ The devstack base directory.
+
+.. zuul:rolevar:: stage_dir
+ :default: "{{ ansible_user_dir }}"
+
+ The stage directory where the input data can be found and
+ the output will be produced.
+
+.. zuul:rolevar:: zuul_work_dir
+ :default: {{ devstack_base_dir }}/tempest
+
+ Directory to work in. It has to be a fully qualified path.
diff --git a/roles/process-stackviz/defaults/main.yaml b/roles/process-stackviz/defaults/main.yaml
new file mode 100644
index 0000000..f3bc32b
--- /dev/null
+++ b/roles/process-stackviz/defaults/main.yaml
@@ -0,0 +1,3 @@
+devstack_base_dir: /opt/stack
+stage_dir: "{{ ansible_user_dir }}"
+zuul_work_dir: "{{ devstack_base_dir }}/tempest"
diff --git a/roles/process-stackviz/tasks/main.yaml b/roles/process-stackviz/tasks/main.yaml
new file mode 100644
index 0000000..3724e0e
--- /dev/null
+++ b/roles/process-stackviz/tasks/main.yaml
@@ -0,0 +1,65 @@
+- name: Check if stackviz archive exists
+ stat:
+ path: "/opt/cache/files/stackviz-latest.tar.gz"
+ register: stackviz_archive
+
+- debug:
+ msg: "Stackviz archive could not be found in /opt/cache/files/stackviz-latest.tar.gz"
+ when: not stackviz_archive.stat.exists
+
+- name: Check if subunit data exists
+ stat:
+ path: "{{ zuul_work_dir }}/testrepository.subunit"
+ register: subunit_input
+
+- debug:
+ msg: "Subunit file could not be found at {{ zuul_work_dir }}/testrepository.subunit"
+ when: not subunit_input.stat.exists
+
+- name: Install stackviz
+ pip:
+ name: "file://{{ stackviz_archive.stat.path }}"
+ virtualenv: /tmp/stackviz
+ extra_args: -U
+ when:
+ - stackviz_archive.stat.exists
+ - subunit_input.stat.exists
+
+- name: Deploy stackviz static html+js
+ command: cp -pR /tmp/stackviz/share/stackviz-html {{ stage_dir }}/stackviz
+ when:
+ - stackviz_archive.stat.exists
+ - subunit_input.stat.exists
+
+- name: Check if dstat data exists
+ stat:
+ path: "{{ devstack_base_dir }}/logs/dstat-csv.log"
+ register: dstat_input
+ when:
+ - stackviz_archive.stat.exists
+ - subunit_input.stat.exists
+
+- name: Run stackviz with dstat
+ shell: |
+ cat {{ subunit_input.stat.path }} | \
+ /tmp/stackviz/bin/stackviz-export \
+ --dstat "{{ devstack_base_dir }}/logs/dstat-csv.log" \
+ --env --stdin \
+ {{ stage_dir }}/stackviz/data
+ when:
+ - stackviz_archive.stat.exists
+ - subunit_input.stat.exists
+ - dstat_input.stat.exists
+ failed_when: False
+
+- name: Run stackviz without dstat
+ shell: |
+ cat {{ subunit_input.stat.path }} | \
+ /tmp/stackviz/bin/stackviz-export \
+ --env --stdin \
+ {{ stage_dir }}/stackviz/data
+ when:
+ - stackviz_archive.stat.exists
+ - subunit_input.stat.exists
+ - not dstat_input.stat.exists
+ failed_when: False
diff --git a/roles/run-tempest/README.rst b/roles/run-tempest/README.rst
new file mode 100644
index 0000000..384ca38
--- /dev/null
+++ b/roles/run-tempest/README.rst
@@ -0,0 +1,41 @@
+Run Tempest
+
+**Role Variables**
+
+.. zuul:rolevar:: devstack_base_dir
+ :default: /opt/stack
+
+ The devstack base directory.
+
+.. zuul:rolevar:: tempest_concurrency
+ :default: 0
+
+ The number of parallel test processes.
+
+.. zuul:rolevar:: tempest_test_regex
+ :default: ''
+
+ A regular expression used to select the tests.
+
+ It works only when used with some specific tox environments
+ ('all', 'all-plugin'.)
+
+ Multi-line and commented regexs can be achieved by doing this:
+
+ ::
+ vars:
+ tempest_test_regex: |
+ (?x) # Ignore comments and whitespaces
+ # Line with only a comment.
+ (tempest\.(api|scenario|thirdparty)).*$ # Run only api scenario and third party
+
+.. zuul:rolevar:: tempest_test_blacklist
+
+ Specifies a blacklist file to skip tests that are not needed.
+
+ Pass a full path to the file.
+
+.. zuul:rolevar:: tox_envlist
+ :default: smoke
+
+ The Tempest tox environment to run.
diff --git a/roles/run-tempest/defaults/main.yaml b/roles/run-tempest/defaults/main.yaml
new file mode 100644
index 0000000..85e94f2
--- /dev/null
+++ b/roles/run-tempest/defaults/main.yaml
@@ -0,0 +1,3 @@
+devstack_base_dir: /opt/stack
+tempest_test_regex: ''
+tox_envlist: smoke
diff --git a/roles/run-tempest/tasks/main.yaml b/roles/run-tempest/tasks/main.yaml
new file mode 100644
index 0000000..b68507a
--- /dev/null
+++ b/roles/run-tempest/tasks/main.yaml
@@ -0,0 +1,42 @@
+# NOTE(andreaf) The number of vcpus is not available on all systems.
+# See https://github.com/ansible/ansible/issues/30688
+# When not available, we fall back to ansible_processor_cores
+- name: Get hw.logicalcpu from sysctl
+ shell: sysctl hw.logicalcpu | cut -d' ' -f2
+ register: sysctl_hw_logicalcpu
+ when: ansible_processor_vcpus is not defined
+
+- name: Number of cores
+ set_fact:
+ num_cores: "{{ansible_processor_vcpus|default(sysctl_hw_logicalcpu.stdout)}}"
+
+- name: Set concurrency for cores == 3 or less
+ set_fact:
+ default_concurrency: "{{ num_cores }}"
+ when: num_cores|int <= 3
+
+- name: Limit max concurrency when more than 3 vcpus are available
+ set_fact:
+ default_concurrency: "{{ num_cores|int // 2 }}"
+ when: num_cores|int > 3
+
+- when:
+ - tempest_test_blacklist is defined
+ block:
+ - name: Check for test blacklist file
+ stat:
+ path: "{{ tempest_test_blacklist }}"
+ register:
+ blacklist_stat
+
+ - name: Build blacklist option
+ set_fact:
+ blacklist_option: "--blacklist-file={{ tempest_test_blacklist|quote }}"
+ when: blacklist_stat.stat.exists
+
+- name: Run Tempest
+ command: tox -e {{tox_envlist}} -- {{tempest_test_regex|quote}} {{blacklist_option|default('')}} --concurrency={{tempest_concurrency|default(default_concurrency)}}
+ args:
+ chdir: "{{devstack_base_dir}}/tempest"
+ become: true
+ become_user: tempest
diff --git a/roles/setup-tempest-data-dir/README.rst b/roles/setup-tempest-data-dir/README.rst
new file mode 100644
index 0000000..db0b083
--- /dev/null
+++ b/roles/setup-tempest-data-dir/README.rst
@@ -0,0 +1,12 @@
+Setup the `tempest` user as owner of Tempest's data folder.
+
+Tempest's devstack plugin creates the data folder, but it has no knowledge
+of the `tempest` user, so we need a role to fix ownership on the data folder.
+
+
+**Role Variables**
+
+.. zuul:rolevar:: devstack_data_dir
+ :default: /opt/stack/data
+
+ The devstack data directory.
diff --git a/roles/setup-tempest-data-dir/defaults/main.yaml b/roles/setup-tempest-data-dir/defaults/main.yaml
new file mode 100644
index 0000000..14265f0
--- /dev/null
+++ b/roles/setup-tempest-data-dir/defaults/main.yaml
@@ -0,0 +1 @@
+devstack_data_dir: /opt/stack/data
diff --git a/roles/setup-tempest-data-dir/tasks/main.yaml b/roles/setup-tempest-data-dir/tasks/main.yaml
new file mode 100644
index 0000000..9dd6309
--- /dev/null
+++ b/roles/setup-tempest-data-dir/tasks/main.yaml
@@ -0,0 +1,7 @@
+- name: Set tempest as owner of Tempest data folder
+ file:
+ path: "{{devstack_data_dir}}/tempest"
+ owner: tempest
+ group: stack
+ recurse: yes
+ become: yes
diff --git a/roles/setup-tempest-run-dir/README.rst b/roles/setup-tempest-run-dir/README.rst
new file mode 100644
index 0000000..c8e2339
--- /dev/null
+++ b/roles/setup-tempest-run-dir/README.rst
@@ -0,0 +1,14 @@
+Setup Tempest run folder.
+
+To support isolation between multiple runs, separate run folders are required.
+Set `tempest` as owner of Tempest's current run folder.
+There is an implicit assumption here of a one to one relationship between
+devstack versions and Tempest runs.
+
+
+**Role Variables**
+
+.. zuul:rolevar:: devstack_base_dir
+ :default: /opt/stack
+
+ The devstack base directory.
diff --git a/roles/setup-tempest-run-dir/defaults/main.yaml b/roles/setup-tempest-run-dir/defaults/main.yaml
new file mode 100644
index 0000000..fea05c8
--- /dev/null
+++ b/roles/setup-tempest-run-dir/defaults/main.yaml
@@ -0,0 +1 @@
+devstack_base_dir: /opt/stack
diff --git a/roles/setup-tempest-run-dir/tasks/main.yaml b/roles/setup-tempest-run-dir/tasks/main.yaml
new file mode 100644
index 0000000..a012d72
--- /dev/null
+++ b/roles/setup-tempest-run-dir/tasks/main.yaml
@@ -0,0 +1,7 @@
+- name: Set tempest as owner of Tempest run folder
+ file:
+ path: "{{devstack_base_dir}}/tempest"
+ owner: tempest
+ group: stack
+ recurse: yes
+ become: yes
diff --git a/run_tempest.sh b/run_tempest.sh
deleted file mode 100755
index 8c8f25f..0000000
--- a/run_tempest.sh
+++ /dev/null
@@ -1,130 +0,0 @@
-#!/usr/bin/env bash
-
-function usage {
- echo "Usage: $0 [OPTION]..."
- echo "Run Tempest test suite"
- echo ""
- echo " -V, --virtual-env Always use virtualenv. Install automatically if not present"
- echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local environment"
- echo " -n, --no-site-packages Isolate the virtualenv from the global Python environment"
- echo " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added."
- echo " -u, --update Update the virtual environment with any newer package versions"
- echo " -s, --smoke Only run smoke tests"
- echo " -t, --serial Run testr serially"
- echo " -C, --config Config file location"
- echo " -h, --help Print this usage message"
- echo " -d, --debug Run tests with testtools instead of testr. This allows you to use PDB"
- echo " -- [TESTROPTIONS] After the first '--' you can pass arbitrary arguments to testr "
-}
-
-testrargs=""
-venv=${VENV:-.venv}
-with_venv=tools/with_venv.sh
-serial=0
-always_venv=0
-never_venv=0
-no_site_packages=0
-debug=0
-force=0
-wrapper=""
-config_file=""
-update=0
-
-if ! options=$(getopt -o VNnfusthdC:lL: -l virtual-env,no-virtual-env,no-site-packages,force,update,smoke,serial,help,debug,config: -- "$@")
-then
- # parse error
- usage
- exit 1
-fi
-
-eval set -- $options
-first_uu=yes
-while [ $# -gt 0 ]; do
- case "$1" in
- -h|--help) usage; exit;;
- -V|--virtual-env) always_venv=1; never_venv=0;;
- -N|--no-virtual-env) always_venv=0; never_venv=1;;
- -n|--no-site-packages) no_site_packages=1;;
- -f|--force) force=1;;
- -u|--update) update=1;;
- -d|--debug) debug=1;;
- -C|--config) config_file=$2; shift;;
- -s|--smoke) testrargs+="smoke";;
- -t|--serial) serial=1;;
- --) [ "yes" == "$first_uu" ] || testrargs="$testrargs $1"; first_uu=no ;;
- *) testrargs="$testrargs $1";;
- esac
- shift
-done
-
-if [ -n "$config_file" ]; then
- config_file=`readlink -f "$config_file"`
- export TEMPEST_CONFIG_DIR=`dirname "$config_file"`
- export TEMPEST_CONFIG=`basename "$config_file"`
-fi
-
-cd `dirname "$0"`
-
-if [ $no_site_packages -eq 1 ]; then
- installvenvopts="--no-site-packages"
-fi
-
-function testr_init {
- if [ ! -d .testrepository ]; then
- ${wrapper} testr init
- fi
-}
-
-function run_tests {
- testr_init
- ${wrapper} find . -type f -name "*.pyc" -delete
- export OS_TEST_PATH=./tempest/test_discover
- if [ $debug -eq 1 ]; then
- if [ "$testrargs" = "" ]; then
- testrargs="discover ./tempest/test_discover"
- fi
- ${wrapper} python -m testtools.run $testrargs
- return $?
- fi
-
- if [ $serial -eq 1 ]; then
- ${wrapper} testr run --subunit $testrargs | ${wrapper} subunit-trace -n -f
- else
- ${wrapper} testr run --parallel --subunit $testrargs | ${wrapper} subunit-trace -n -f
- fi
-}
-
-if [ $never_venv -eq 0 ]
-then
- # Remove the virtual environment if --force used
- if [ $force -eq 1 ]; then
- echo "Cleaning virtualenv..."
- rm -rf ${venv}
- fi
- if [ $update -eq 1 ]; then
- echo "Updating virtualenv..."
- python tools/install_venv.py $installvenvopts
- fi
- if [ -e ${venv} ]; then
- wrapper="${with_venv}"
- else
- if [ $always_venv -eq 1 ]; then
- # Automatically install the virtualenv
- python tools/install_venv.py $installvenvopts
- wrapper="${with_venv}"
- else
- echo -e "No virtual environment found...create one? (Y/n) \c"
- read use_ve
- if [ "x$use_ve" = "xY" -o "x$use_ve" = "x" -o "x$use_ve" = "xy" ]; then
- # Install the virtualenv and run the test suite in it
- python tools/install_venv.py $installvenvopts
- wrapper=${with_venv}
- fi
- fi
- fi
-fi
-
-run_tests
-retval=$?
-
-exit $retval
diff --git a/run_tests.sh b/run_tests.sh
deleted file mode 100755
index 908056f..0000000
--- a/run_tests.sh
+++ /dev/null
@@ -1,150 +0,0 @@
-#!/usr/bin/env bash
-
-function usage {
- echo "Usage: $0 [OPTION]..."
- echo "Run Tempest unit tests"
- echo ""
- echo " -V, --virtual-env Always use virtualenv. Install automatically if not present"
- echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local environment"
- echo " -n, --no-site-packages Isolate the virtualenv from the global Python environment"
- echo " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added."
- echo " -u, --update Update the virtual environment with any newer package versions"
- echo " -t, --serial Run testr serially"
- echo " -p, --pep8 Just run pep8"
- echo " -c, --coverage Generate coverage report"
- echo " -h, --help Print this usage message"
- echo " -d, --debug Run tests with testtools instead of testr. This allows you to use PDB"
- echo " -- [TESTROPTIONS] After the first '--' you can pass arbitrary arguments to testr "
-}
-
-testrargs=""
-just_pep8=0
-venv=${VENV:-.venv}
-with_venv=tools/with_venv.sh
-serial=0
-always_venv=0
-never_venv=0
-no_site_packages=0
-debug=0
-force=0
-coverage=0
-wrapper=""
-config_file=""
-update=0
-
-if ! options=$(getopt -o VNnfuctphd -l virtual-env,no-virtual-env,no-site-packages,force,update,serial,coverage,pep8,help,debug -- "$@")
-then
- # parse error
- usage
- exit 1
-fi
-
-eval set -- $options
-first_uu=yes
-while [ $# -gt 0 ]; do
- case "$1" in
- -h|--help) usage; exit;;
- -V|--virtual-env) always_venv=1; never_venv=0;;
- -N|--no-virtual-env) always_venv=0; never_venv=1;;
- -n|--no-site-packages) no_site_packages=1;;
- -f|--force) force=1;;
- -u|--update) update=1;;
- -d|--debug) debug=1;;
- -p|--pep8) let just_pep8=1;;
- -c|--coverage) coverage=1;;
- -t|--serial) serial=1;;
- --) [ "yes" == "$first_uu" ] || testrargs="$testrargs $1"; first_uu=no ;;
- *) testrargs="$testrargs $1";;
- esac
- shift
-done
-
-
-cd `dirname "$0"`
-
-if [ $no_site_packages -eq 1 ]; then
- installvenvopts="--no-site-packages"
-fi
-
-function testr_init {
- if [ ! -d .testrepository ]; then
- ${wrapper} testr init
- fi
-}
-
-function run_tests {
- testr_init
- ${wrapper} find . -type f -name "*.pyc" -delete
- export OS_TEST_PATH=./tempest/tests
- if [ $debug -eq 1 ]; then
- if [ "$testrargs" = "" ]; then
- testrargs="discover ./tempest/tests"
- fi
- ${wrapper} python -m testtools.run $testrargs
- return $?
- fi
-
- if [ $coverage -eq 1 ]; then
- ${wrapper} python setup.py test --coverage
- return $?
- fi
-
- if [ $serial -eq 1 ]; then
- ${wrapper} testr run --subunit $testrargs | ${wrapper} subunit-trace -n -f
- else
- ${wrapper} testr run --parallel --subunit $testrargs | ${wrapper} subunit-trace -n -f
- fi
-}
-
-function run_pep8 {
- echo "Running flake8 ..."
- if [ $never_venv -eq 1 ]; then
- echo "**WARNING**:" >&2
- echo "Running flake8 without virtual env may miss OpenStack HACKING detection" >&2
- fi
- ${wrapper} flake8
-}
-
-if [ $never_venv -eq 0 ]
-then
- # Remove the virtual environment if --force used
- if [ $force -eq 1 ]; then
- echo "Cleaning virtualenv..."
- rm -rf ${venv}
- fi
- if [ $update -eq 1 ]; then
- echo "Updating virtualenv..."
- python tools/install_venv.py $installvenvopts
- fi
- if [ -e ${venv} ]; then
- wrapper="${with_venv}"
- else
- if [ $always_venv -eq 1 ]; then
- # Automatically install the virtualenv
- python tools/install_venv.py $installvenvopts
- wrapper="${with_venv}"
- else
- echo -e "No virtual environment found...create one? (Y/n) \c"
- read use_ve
- if [ "x$use_ve" = "xY" -o "x$use_ve" = "x" -o "x$use_ve" = "xy" ]; then
- # Install the virtualenv and run the test suite in it
- python tools/install_venv.py $installvenvopts
- wrapper=${with_venv}
- fi
- fi
- fi
-fi
-
-if [ $just_pep8 -eq 1 ]; then
- run_pep8
- exit
-fi
-
-run_tests
-retval=$?
-
-if [ -z "$testrargs" ]; then
- run_pep8
-fi
-
-exit $retval
diff --git a/setup.cfg b/setup.cfg
index 4415063..96ee7ea 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -5,7 +5,7 @@
README.rst
author = OpenStack
author-email = openstack-dev@lists.openstack.org
-home-page = http://www.openstack.org/
+home-page = https://docs.openstack.org/tempest/latest/
classifier =
Intended Audience :: Information Technology
Intended Audience :: System Administrators
@@ -16,7 +16,7 @@
Programming Language :: Python :: 2
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3
- Programming Language :: Python :: 3.4
+ Programming Language :: Python :: 3.5
[files]
packages =
@@ -27,25 +27,25 @@
[entry_points]
console_scripts =
verify-tempest-config = tempest.cmd.verify_tempest_config:main
- javelin2 = tempest.cmd.javelin:main
- run-tempest-stress = tempest.cmd.run_stress:main
- tempest-cleanup = tempest.cmd.cleanup:main
tempest-account-generator = tempest.cmd.account_generator:main
tempest = tempest.cmd.main:main
+ skip-tracker = tempest.lib.cmd.skip_tracker:main
+ check-uuid = tempest.lib.cmd.check_uuid:run
+ subunit-describe-calls = tempest.cmd.subunit_describe_calls:entry_point
tempest.cm =
+ account-generator = tempest.cmd.account_generator:TempestAccountGenerator
init = tempest.cmd.init:TempestInit
cleanup = tempest.cmd.cleanup:TempestCleanup
- run-stress = tempest.cmd.run_stress:TempestRunStress
+ list-plugins = tempest.cmd.list_plugins:TempestListPlugins
+ verify-config = tempest.cmd.verify_tempest_config:TempestVerifyConfig
+ workspace_register = tempest.cmd.workspace:TempestWorkspaceRegister
+ workspace_rename = tempest.cmd.workspace:TempestWorkspaceRename
+ workspace_move = tempest.cmd.workspace:TempestWorkspaceMove
+ workspace_remove = tempest.cmd.workspace:TempestWorkspaceRemove
+ workspace_list = tempest.cmd.workspace:TempestWorkspaceList
+ run = tempest.cmd.run:TempestRun
oslo.config.opts =
tempest.config = tempest.config:list_opts
-[build_sphinx]
-all_files = 1
-build-dir = doc/build
-source-dir = doc/source
-
-[pbr]
-warnerrors = True
-
[wheel]
universal = 1
diff --git a/setup.py b/setup.py
index 782bb21..566d844 100644
--- a/setup.py
+++ b/setup.py
@@ -25,5 +25,5 @@
pass
setuptools.setup(
- setup_requires=['pbr>=1.8'],
+ setup_requires=['pbr>=2.0.0'],
pbr=True)
diff --git a/tempest/README.rst b/tempest/README.rst
index 113b191..a5f4a92 100644
--- a/tempest/README.rst
+++ b/tempest/README.rst
@@ -9,13 +9,15 @@
OpenStack clouds.
As such Tempest tests come in many flavors, each with their own rules
-and guidelines. Below is the proposed Havana restructuring for Tempest
+and guidelines. Below is the overview of the Tempest respository structure
to make this clear.
-| tempest/
-| api/ - API tests
-| scenario/ - complex scenario tests
-| stress/ - stress tests
+.. code-block:: console
+
+ 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
@@ -25,11 +27,10 @@
----------------------
API tests are validation tests for the OpenStack API. They should not
-use the existing python clients for OpenStack, but should instead use
-the tempest implementations of clients. This allows us to test both
-XML and JSON. Having raw clients also lets us pass invalid JSON and
-XML to the APIs and see the results, something we could not get with
-the native clients.
+use the existing Python clients for OpenStack, but should instead use
+the Tempest implementations of clients. Having raw clients let us
+pass invalid JSON to the APIs and see the results, something we could
+not get with the native clients.
When it makes sense, API testing should be moved closer to the
projects themselves, possibly as functional tests in their unit test
@@ -43,22 +44,14 @@
functionality. They are typically a series of steps where complicated
state requiring multiple services is set up exercised, and torn down.
-Scenario tests should not use the existing python clients for OpenStack,
-but should instead use the tempest implementations of clients.
+Scenario tests should not use the existing Python clients for OpenStack,
+but should instead use the Tempest implementations of clients.
-:ref:`stress_field_guide`
--------------------------
-
-Stress tests are designed to stress an OpenStack environment by running a high
-workload against it and seeing what breaks. The stress test framework runs
-several test jobs in parallel and can run any existing test in Tempest as a
-stress job.
-
:ref:`unit_tests_field_guide`
-----------------------------
Unit tests are the self checks for Tempest. They provide functional
-verification and regression checking for the internal components of tempest.
-They should be used to just verify that the individual pieces of tempest are
+verification and regression checking for the internal components of Tempest.
+They should be used to just verify that the individual pieces of Tempest are
working as expected.
diff --git a/tempest/api/README.rst b/tempest/api/README.rst
index 91e6ad6..a796922 100644
--- a/tempest/api/README.rst
+++ b/tempest/api/README.rst
@@ -13,7 +13,8 @@
It's also important to test not only the expected positive path on
APIs, but also to provide them with invalid data to ensure they fail
-in expected and documented ways. Over the course of the OpenStack
+in expected and documented ways. The latter type of tests is called
+``negative tests`` in Tempest source code. Over the course of the OpenStack
project Tempest has discovered many fundamental bugs by doing just
this.
@@ -22,7 +23,7 @@
spinning up a server, image, etc, then operating on it.
-Why are these tests in tempest?
+Why are these tests in Tempest?
-------------------------------
This is one of the core missions for the Tempest project, and where it
diff --git a/tempest/api/baremetal/README.rst b/tempest/api/baremetal/README.rst
deleted file mode 100644
index 759c937..0000000
--- a/tempest/api/baremetal/README.rst
+++ /dev/null
@@ -1,25 +0,0 @@
-Tempest Field Guide to Baremetal API tests
-==========================================
-
-
-What are these tests?
----------------------
-
-These tests stress the OpenStack baremetal provisioning API provided by
-Ironic.
-
-
-Why are these tests in tempest?
-------------------------------
-
-The purpose of these tests is to exercise the various APIs provided by Ironic
-for managing baremetal nodes.
-
-
-Scope of these tests
---------------------
-
-The baremetal API test perform basic CRUD operations on the Ironic node
-inventory. They do not actually perform hardware provisioning. It is important
-to note that all Ironic API actions are admin operations meant to be used
-either by cloud operators or other OpenStack services (i.e., Nova).
diff --git a/tempest/api/baremetal/admin/__init__.py b/tempest/api/baremetal/admin/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/api/baremetal/admin/__init__.py
+++ /dev/null
diff --git a/tempest/api/baremetal/admin/base.py b/tempest/api/baremetal/admin/base.py
deleted file mode 100644
index 80b69b9..0000000
--- a/tempest/api/baremetal/admin/base.py
+++ /dev/null
@@ -1,205 +0,0 @@
-# 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 functools
-
-from tempest_lib import exceptions as lib_exc
-
-from tempest.common.utils import data_utils
-from tempest import config
-from tempest import test
-
-CONF = config.CONF
-
-
-# NOTE(adam_g): The baremetal API tests exercise operations such as enroll
-# node, power on, power off, etc. Testing against real drivers (ie, IPMI)
-# will require passing driver-specific data to Tempest (addresses,
-# credentials, etc). Until then, only support testing against the fake driver,
-# which has no external dependencies.
-SUPPORTED_DRIVERS = ['fake']
-
-# NOTE(jroll): resources must be deleted in a specific order, this list
-# defines the resource types to clean up, and the correct order.
-RESOURCE_TYPES = ['port', 'node', 'chassis']
-
-
-def creates(resource):
- """Decorator that adds resources to the appropriate cleanup list."""
-
- def decorator(f):
- @functools.wraps(f)
- def wrapper(cls, *args, **kwargs):
- resp, body = f(cls, *args, **kwargs)
-
- if 'uuid' in body:
- cls.created_objects[resource].add(body['uuid'])
-
- return resp, body
- return wrapper
- return decorator
-
-
-class BaseBaremetalTest(test.BaseTestCase):
- """Base class for Baremetal API tests."""
-
- credentials = ['admin']
-
- @classmethod
- def skip_checks(cls):
- super(BaseBaremetalTest, cls).skip_checks()
- if not CONF.service_available.ironic:
- skip_msg = ('%s skipped as Ironic is not available' % cls.__name__)
- raise cls.skipException(skip_msg)
-
- if CONF.baremetal.driver not in SUPPORTED_DRIVERS:
- skip_msg = ('%s skipped as Ironic driver %s is not supported for '
- 'testing.' %
- (cls.__name__, CONF.baremetal.driver))
- raise cls.skipException(skip_msg)
-
- @classmethod
- def setup_clients(cls):
- super(BaseBaremetalTest, cls).setup_clients()
- cls.client = cls.os_admin.baremetal_client
-
- @classmethod
- def resource_setup(cls):
- super(BaseBaremetalTest, cls).resource_setup()
-
- cls.driver = CONF.baremetal.driver
- cls.power_timeout = CONF.baremetal.power_timeout
- cls.created_objects = {}
- for resource in RESOURCE_TYPES:
- cls.created_objects[resource] = set()
-
- @classmethod
- def resource_cleanup(cls):
- """Ensure that all created objects get destroyed."""
-
- try:
- for resource in RESOURCE_TYPES:
- uuids = cls.created_objects[resource]
- delete_method = getattr(cls.client, 'delete_%s' % resource)
- for u in uuids:
- delete_method(u, ignore_errors=lib_exc.NotFound)
- finally:
- super(BaseBaremetalTest, cls).resource_cleanup()
-
- @classmethod
- @creates('chassis')
- def create_chassis(cls, description=None, expect_errors=False):
- """Wrapper utility for creating test chassis.
-
- :param description: A description of the chassis. if not supplied,
- a random value will be generated.
- :return: Created chassis.
-
- """
- description = description or data_utils.rand_name('test-chassis')
- resp, body = cls.client.create_chassis(description=description)
- return resp, body
-
- @classmethod
- @creates('node')
- def create_node(cls, chassis_id, cpu_arch='x86', cpus=8, local_gb=10,
- memory_mb=4096):
- """Wrapper utility for creating test baremetal nodes.
-
- :param cpu_arch: CPU architecture of the node. Default: x86.
- :param cpus: Number of CPUs. Default: 8.
- :param local_gb: Disk size. Default: 10.
- :param memory_mb: Available RAM. Default: 4096.
- :return: Created node.
-
- """
- resp, body = cls.client.create_node(chassis_id, cpu_arch=cpu_arch,
- cpus=cpus, local_gb=local_gb,
- memory_mb=memory_mb,
- driver=cls.driver)
-
- return resp, body
-
- @classmethod
- @creates('port')
- def create_port(cls, node_id, address, extra=None, uuid=None):
- """Wrapper utility for creating test ports.
-
- :param address: MAC address of the port.
- :param extra: Meta data of the port. If not supplied, an empty
- dictionary will be created.
- :param uuid: UUID of the port.
- :return: Created port.
-
- """
- extra = extra or {}
- resp, body = cls.client.create_port(address=address, node_id=node_id,
- extra=extra, uuid=uuid)
-
- return resp, body
-
- @classmethod
- def delete_chassis(cls, chassis_id):
- """Deletes a chassis having the specified UUID.
-
- :param uuid: The unique identifier of the chassis.
- :return: Server response.
-
- """
-
- resp, body = cls.client.delete_chassis(chassis_id)
-
- if chassis_id in cls.created_objects['chassis']:
- cls.created_objects['chassis'].remove(chassis_id)
-
- return resp
-
- @classmethod
- def delete_node(cls, node_id):
- """Deletes a node having the specified UUID.
-
- :param uuid: The unique identifier of the node.
- :return: Server response.
-
- """
-
- resp, body = cls.client.delete_node(node_id)
-
- if node_id in cls.created_objects['node']:
- cls.created_objects['node'].remove(node_id)
-
- return resp
-
- @classmethod
- def delete_port(cls, port_id):
- """Deletes a port having the specified UUID.
-
- :param uuid: The unique identifier of the port.
- :return: Server response.
-
- """
-
- resp, body = cls.client.delete_port(port_id)
-
- if port_id in cls.created_objects['port']:
- cls.created_objects['port'].remove(port_id)
-
- return resp
-
- def validate_self_link(self, resource, uuid, link):
- """Check whether the given self link formatted correctly."""
- expected_link = "{base}/{pref}/{res}/{uuid}".format(
- base=self.client.base_url,
- pref=self.client.uri_prefix,
- res=resource,
- uuid=uuid)
- self.assertEqual(expected_link, link)
diff --git a/tempest/api/baremetal/admin/test_api_discovery.py b/tempest/api/baremetal/admin/test_api_discovery.py
deleted file mode 100644
index 41388ad..0000000
--- a/tempest/api/baremetal/admin/test_api_discovery.py
+++ /dev/null
@@ -1,42 +0,0 @@
-# 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.baremetal.admin import base
-from tempest import test
-
-
-class TestApiDiscovery(base.BaseBaremetalTest):
- """Tests for API discovery features."""
-
- @test.idempotent_id('a3c27e94-f56c-42c4-8600-d6790650b9c5')
- def test_api_versions(self):
- _, descr = self.client.get_api_description()
- expected_versions = ('v1',)
- versions = [version['id'] for version in descr['versions']]
-
- for v in expected_versions:
- self.assertIn(v, versions)
-
- @test.idempotent_id('896283a6-488e-4f31-af78-6614286cbe0d')
- def test_default_version(self):
- _, descr = self.client.get_api_description()
- default_version = descr['default_version']
- self.assertEqual(default_version['id'], 'v1')
-
- @test.idempotent_id('abc0b34d-e684-4546-9728-ab7a9ad9f174')
- def test_version_1_resources(self):
- _, descr = self.client.get_version_description(version='v1')
- expected_resources = ('nodes', 'chassis',
- 'ports', 'links', 'media_types')
-
- for res in expected_resources:
- self.assertIn(res, descr)
diff --git a/tempest/api/baremetal/admin/test_chassis.py b/tempest/api/baremetal/admin/test_chassis.py
deleted file mode 100644
index 29fc64c..0000000
--- a/tempest/api/baremetal/admin/test_chassis.py
+++ /dev/null
@@ -1,85 +0,0 @@
-# -*- coding: utf-8 -*-
-# 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 six
-from tempest_lib import exceptions as lib_exc
-
-from tempest.api.baremetal.admin import base
-from tempest.common.utils import data_utils
-from tempest import test
-
-
-class TestChassis(base.BaseBaremetalTest):
- """Tests for chassis."""
-
- @classmethod
- def resource_setup(cls):
- super(TestChassis, cls).resource_setup()
- _, cls.chassis = cls.create_chassis()
-
- def _assertExpected(self, expected, actual):
- # Check if not expected keys/values exists in actual response body
- for key, value in six.iteritems(expected):
- if key not in ('created_at', 'updated_at'):
- self.assertIn(key, actual)
- self.assertEqual(value, actual[key])
-
- @test.idempotent_id('7c5a2e09-699c-44be-89ed-2bc189992d42')
- def test_create_chassis(self):
- descr = data_utils.rand_name('test-chassis')
- _, chassis = self.create_chassis(description=descr)
- self.assertEqual(chassis['description'], descr)
-
- @test.idempotent_id('cabe9c6f-dc16-41a7-b6b9-0a90c212edd5')
- def test_create_chassis_unicode_description(self):
- # Use a unicode string for testing:
- # 'We ♡ OpenStack in Ukraine'
- descr = u'В Україні ♡ OpenStack!'
- _, chassis = self.create_chassis(description=descr)
- self.assertEqual(chassis['description'], descr)
-
- @test.idempotent_id('c84644df-31c4-49db-a307-8942881f41c0')
- def test_show_chassis(self):
- _, chassis = self.client.show_chassis(self.chassis['uuid'])
- self._assertExpected(self.chassis, chassis)
-
- @test.idempotent_id('29c9cd3f-19b5-417b-9864-99512c3b33b3')
- def test_list_chassis(self):
- _, body = self.client.list_chassis()
- self.assertIn(self.chassis['uuid'],
- [i['uuid'] for i in body['chassis']])
-
- @test.idempotent_id('5ae649ad-22d1-4fe1-bbc6-97227d199fb3')
- def test_delete_chassis(self):
- _, body = self.create_chassis()
- uuid = body['uuid']
-
- self.delete_chassis(uuid)
- self.assertRaises(lib_exc.NotFound, self.client.show_chassis, uuid)
-
- @test.idempotent_id('cda8a41f-6be2-4cbf-840c-994b00a89b44')
- def test_update_chassis(self):
- _, body = self.create_chassis()
- uuid = body['uuid']
-
- new_description = data_utils.rand_name('new-description')
- _, body = (self.client.update_chassis(uuid,
- description=new_description))
- _, chassis = self.client.show_chassis(uuid)
- self.assertEqual(chassis['description'], new_description)
-
- @test.idempotent_id('76305e22-a4e2-4ab3-855c-f4e2368b9335')
- def test_chassis_node_list(self):
- _, node = self.create_node(self.chassis['uuid'])
- _, body = self.client.list_chassis_nodes(self.chassis['uuid'])
- self.assertIn(node['uuid'], [n['uuid'] for n in body['nodes']])
diff --git a/tempest/api/baremetal/admin/test_drivers.py b/tempest/api/baremetal/admin/test_drivers.py
deleted file mode 100644
index f08d7ab..0000000
--- a/tempest/api/baremetal/admin/test_drivers.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# Copyright 2014 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.baremetal.admin import base
-from tempest import config
-from tempest import test
-
-CONF = config.CONF
-
-
-class TestDrivers(base.BaseBaremetalTest):
- """Tests for drivers."""
- @classmethod
- def resource_setup(cls):
- super(TestDrivers, cls).resource_setup()
- cls.driver_name = CONF.baremetal.driver
-
- @test.idempotent_id('5aed2790-7592-4655-9b16-99abcc2e6ec5')
- def test_list_drivers(self):
- _, drivers = self.client.list_drivers()
- self.assertIn(self.driver_name,
- [d['name'] for d in drivers['drivers']])
-
- @test.idempotent_id('fb3287a3-c4d7-44bf-ae9d-1eef906d78ce')
- def test_show_driver(self):
- _, driver = self.client.show_driver(self.driver_name)
- self.assertEqual(self.driver_name, driver['name'])
diff --git a/tempest/api/baremetal/admin/test_nodes.py b/tempest/api/baremetal/admin/test_nodes.py
deleted file mode 100644
index b6dee18..0000000
--- a/tempest/api/baremetal/admin/test_nodes.py
+++ /dev/null
@@ -1,169 +0,0 @@
-# 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 six
-from tempest_lib import exceptions as lib_exc
-
-from tempest.api.baremetal.admin import base
-from tempest.common.utils import data_utils
-from tempest.common import waiters
-from tempest import test
-
-
-class TestNodes(base.BaseBaremetalTest):
- """Tests for baremetal nodes."""
-
- def setUp(self):
- super(TestNodes, self).setUp()
-
- _, self.chassis = self.create_chassis()
- _, self.node = self.create_node(self.chassis['uuid'])
-
- def _assertExpected(self, expected, actual):
- # Check if not expected keys/values exists in actual response body
- for key, value in six.iteritems(expected):
- if key not in ('created_at', 'updated_at'):
- self.assertIn(key, actual)
- self.assertEqual(value, actual[key])
-
- def _associate_node_with_instance(self):
- self.client.set_node_power_state(self.node['uuid'], 'power off')
- waiters.wait_for_bm_node_status(self.client, self.node['uuid'],
- 'power_state', 'power off')
- instance_uuid = data_utils.rand_uuid()
- self.client.update_node(self.node['uuid'],
- instance_uuid=instance_uuid)
- self.addCleanup(self.client.update_node,
- uuid=self.node['uuid'], instance_uuid=None)
- return instance_uuid
-
- @test.idempotent_id('4e939eb2-8a69-4e84-8652-6fffcbc9db8f')
- def test_create_node(self):
- params = {'cpu_arch': 'x86_64',
- 'cpus': '12',
- 'local_gb': '10',
- 'memory_mb': '1024'}
-
- _, body = self.create_node(self.chassis['uuid'], **params)
- self._assertExpected(params, body['properties'])
-
- @test.idempotent_id('9ade60a4-505e-4259-9ec4-71352cbbaf47')
- def test_delete_node(self):
- _, node = self.create_node(self.chassis['uuid'])
-
- self.delete_node(node['uuid'])
-
- self.assertRaises(lib_exc.NotFound, self.client.show_node,
- node['uuid'])
-
- @test.idempotent_id('55451300-057c-4ecf-8255-ba42a83d3a03')
- def test_show_node(self):
- _, loaded_node = self.client.show_node(self.node['uuid'])
- self._assertExpected(self.node, loaded_node)
-
- @test.idempotent_id('4ca123c4-160d-4d8d-a3f7-15feda812263')
- def test_list_nodes(self):
- _, body = self.client.list_nodes()
- self.assertIn(self.node['uuid'],
- [i['uuid'] for i in body['nodes']])
-
- @test.idempotent_id('85b1f6e0-57fd-424c-aeff-c3422920556f')
- def test_list_nodes_association(self):
- _, body = self.client.list_nodes(associated=True)
- self.assertNotIn(self.node['uuid'],
- [n['uuid'] for n in body['nodes']])
-
- self._associate_node_with_instance()
-
- _, body = self.client.list_nodes(associated=True)
- self.assertIn(self.node['uuid'], [n['uuid'] for n in body['nodes']])
-
- _, body = self.client.list_nodes(associated=False)
- self.assertNotIn(self.node['uuid'], [n['uuid'] for n in body['nodes']])
-
- @test.idempotent_id('18c4ebd8-f83a-4df7-9653-9fb33a329730')
- def test_node_port_list(self):
- _, port = self.create_port(self.node['uuid'],
- data_utils.rand_mac_address())
- _, body = self.client.list_node_ports(self.node['uuid'])
- self.assertIn(port['uuid'],
- [p['uuid'] for p in body['ports']])
-
- @test.idempotent_id('72591acb-f215-49db-8395-710d14eb86ab')
- def test_node_port_list_no_ports(self):
- _, node = self.create_node(self.chassis['uuid'])
- _, body = self.client.list_node_ports(node['uuid'])
- self.assertEmpty(body['ports'])
-
- @test.idempotent_id('4fed270a-677a-4d19-be87-fd38ae490320')
- def test_update_node(self):
- props = {'cpu_arch': 'x86_64',
- 'cpus': '12',
- 'local_gb': '10',
- 'memory_mb': '128'}
-
- _, node = self.create_node(self.chassis['uuid'], **props)
-
- new_p = {'cpu_arch': 'x86',
- 'cpus': '1',
- 'local_gb': '10000',
- 'memory_mb': '12300'}
-
- _, body = self.client.update_node(node['uuid'], properties=new_p)
- _, node = self.client.show_node(node['uuid'])
- self._assertExpected(new_p, node['properties'])
-
- @test.idempotent_id('cbf1f515-5f4b-4e49-945c-86bcaccfeb1d')
- def test_validate_driver_interface(self):
- _, body = self.client.validate_driver_interface(self.node['uuid'])
- core_interfaces = ['power', 'deploy']
- for interface in core_interfaces:
- self.assertIn(interface, body)
-
- @test.idempotent_id('5519371c-26a2-46e9-aa1a-f74226e9d71f')
- def test_set_node_boot_device(self):
- self.client.set_node_boot_device(self.node['uuid'], 'pxe')
-
- @test.idempotent_id('9ea73775-f578-40b9-bc34-efc639c4f21f')
- def test_get_node_boot_device(self):
- body = self.client.get_node_boot_device(self.node['uuid'])
- self.assertIn('boot_device', body)
- self.assertIn('persistent', body)
- self.assertTrue(isinstance(body['boot_device'], six.string_types))
- self.assertTrue(isinstance(body['persistent'], bool))
-
- @test.idempotent_id('3622bc6f-3589-4bc2-89f3-50419c66b133')
- def test_get_node_supported_boot_devices(self):
- body = self.client.get_node_supported_boot_devices(self.node['uuid'])
- self.assertIn('supported_boot_devices', body)
- self.assertTrue(isinstance(body['supported_boot_devices'], list))
-
- @test.idempotent_id('f63b6288-1137-4426-8cfe-0d5b7eb87c06')
- def test_get_console(self):
- _, body = self.client.get_console(self.node['uuid'])
- con_info = ['console_enabled', 'console_info']
- for key in con_info:
- self.assertIn(key, body)
-
- @test.idempotent_id('80504575-9b21-4670-92d1-143b948f9437')
- def test_set_console_mode(self):
- self.client.set_console_mode(self.node['uuid'], True)
-
- _, body = self.client.get_console(self.node['uuid'])
- self.assertEqual(True, body['console_enabled'])
-
- @test.idempotent_id('b02a4f38-5e8b-44b2-aed2-a69a36ecfd69')
- def test_get_node_by_instance_uuid(self):
- instance_uuid = self._associate_node_with_instance()
- _, body = self.client.show_node_by_instance_uuid(instance_uuid)
- self.assertEqual(len(body['nodes']), 1)
- self.assertIn(self.node['uuid'], [n['uuid'] for n in body['nodes']])
diff --git a/tempest/api/baremetal/admin/test_nodestates.py b/tempest/api/baremetal/admin/test_nodestates.py
deleted file mode 100644
index 1ffea25..0000000
--- a/tempest/api/baremetal/admin/test_nodestates.py
+++ /dev/null
@@ -1,59 +0,0 @@
-# Copyright 2014 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 oslo_utils import timeutils
-
-from tempest.api.baremetal.admin import base
-from tempest import exceptions
-from tempest import test
-
-
-class TestNodeStates(base.BaseBaremetalTest):
- """Tests for baremetal NodeStates."""
-
- @classmethod
- def resource_setup(cls):
- super(TestNodeStates, cls).resource_setup()
- _, cls.chassis = cls.create_chassis()
- _, cls.node = cls.create_node(cls.chassis['uuid'])
-
- def _validate_power_state(self, node_uuid, power_state):
- # Validate that power state is set within timeout
- if power_state == 'rebooting':
- power_state = 'power on'
- start = timeutils.utcnow()
- while timeutils.delta_seconds(
- start, timeutils.utcnow()) < self.power_timeout:
- _, node = self.client.show_node(node_uuid)
- if node['power_state'] == power_state:
- return
- message = ('Failed to set power state within '
- 'the required time: %s sec.' % self.power_timeout)
- raise exceptions.TimeoutException(message)
-
- @test.idempotent_id('cd8afa5e-3f57-4e43-8185-beb83d3c9015')
- def test_list_nodestates(self):
- _, nodestates = self.client.list_nodestates(self.node['uuid'])
- for key in nodestates:
- self.assertEqual(nodestates[key], self.node[key])
-
- @test.idempotent_id('fc5b9320-0c98-4e5a-8848-877fe5a0322c')
- def test_set_node_power_state(self):
- _, node = self.create_node(self.chassis['uuid'])
- states = ["power on", "rebooting", "power off"]
- for state in states:
- # Set power state
- self.client.set_node_power_state(node['uuid'], state)
- # Check power state after state is set
- self._validate_power_state(node['uuid'], state)
diff --git a/tempest/api/baremetal/admin/test_ports.py b/tempest/api/baremetal/admin/test_ports.py
deleted file mode 100644
index 180b848..0000000
--- a/tempest/api/baremetal/admin/test_ports.py
+++ /dev/null
@@ -1,266 +0,0 @@
-# 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 six
-from tempest_lib import exceptions as lib_exc
-
-from tempest.api.baremetal.admin import base
-from tempest.common.utils import data_utils
-from tempest import test
-
-
-class TestPorts(base.BaseBaremetalTest):
- """Tests for ports."""
-
- def setUp(self):
- super(TestPorts, self).setUp()
-
- _, self.chassis = self.create_chassis()
- _, self.node = self.create_node(self.chassis['uuid'])
- _, self.port = self.create_port(self.node['uuid'],
- data_utils.rand_mac_address())
-
- def _assertExpected(self, expected, actual):
- # Check if not expected keys/values exists in actual response body
- for key, value in six.iteritems(expected):
- if key not in ('created_at', 'updated_at'):
- self.assertIn(key, actual)
- self.assertEqual(value, actual[key])
-
- @test.idempotent_id('83975898-2e50-42ed-b5f0-e510e36a0b56')
- def test_create_port(self):
- node_id = self.node['uuid']
- address = data_utils.rand_mac_address()
-
- _, port = self.create_port(node_id=node_id, address=address)
-
- _, body = self.client.show_port(port['uuid'])
-
- self._assertExpected(port, body)
-
- @test.idempotent_id('d1f6b249-4cf6-4fe6-9ed6-a6e84b1bf67b')
- def test_create_port_specifying_uuid(self):
- node_id = self.node['uuid']
- address = data_utils.rand_mac_address()
- uuid = data_utils.rand_uuid()
-
- _, port = self.create_port(node_id=node_id,
- address=address, uuid=uuid)
-
- _, body = self.client.show_port(uuid)
- self._assertExpected(port, body)
-
- @test.idempotent_id('4a02c4b0-6573-42a4-a513-2e36ad485b62')
- def test_create_port_with_extra(self):
- node_id = self.node['uuid']
- address = data_utils.rand_mac_address()
- extra = {'str': 'value', 'int': 123, 'float': 0.123,
- 'bool': True, 'list': [1, 2, 3], 'dict': {'foo': 'bar'}}
-
- _, port = self.create_port(node_id=node_id, address=address,
- extra=extra)
-
- _, body = self.client.show_port(port['uuid'])
- self._assertExpected(port, body)
-
- @test.idempotent_id('1bf257a9-aea3-494e-89c0-63f657ab4fdd')
- def test_delete_port(self):
- node_id = self.node['uuid']
- address = data_utils.rand_mac_address()
- _, port = self.create_port(node_id=node_id, address=address)
-
- self.delete_port(port['uuid'])
-
- self.assertRaises(lib_exc.NotFound, self.client.show_port,
- port['uuid'])
-
- @test.idempotent_id('9fa77ab5-ce59-4f05-baac-148904ba1597')
- def test_show_port(self):
- _, port = self.client.show_port(self.port['uuid'])
- self._assertExpected(self.port, port)
-
- @test.idempotent_id('7c1114ff-fc3f-47bb-bc2f-68f61620ba8b')
- def test_show_port_by_address(self):
- _, port = self.client.show_port_by_address(self.port['address'])
- self._assertExpected(self.port, port['ports'][0])
-
- @test.idempotent_id('bd773405-aea5-465d-b576-0ab1780069e5')
- def test_show_port_with_links(self):
- _, port = self.client.show_port(self.port['uuid'])
- self.assertIn('links', port.keys())
- self.assertEqual(2, len(port['links']))
- self.assertIn(port['uuid'], port['links'][0]['href'])
-
- @test.idempotent_id('b5e91854-5cd7-4a8e-bb35-3e0a1314606d')
- def test_list_ports(self):
- _, body = self.client.list_ports()
- self.assertIn(self.port['uuid'],
- [i['uuid'] for i in body['ports']])
- # Verify self links.
- for port in body['ports']:
- self.validate_self_link('ports', port['uuid'],
- port['links'][0]['href'])
-
- @test.idempotent_id('324a910e-2f80-4258-9087-062b5ae06240')
- def test_list_with_limit(self):
- _, body = self.client.list_ports(limit=3)
-
- next_marker = body['ports'][-1]['uuid']
- self.assertIn(next_marker, body['next'])
-
- @test.idempotent_id('8a94b50f-9895-4a63-a574-7ecff86e5875')
- def test_list_ports_details(self):
- node_id = self.node['uuid']
-
- uuids = [
- self.create_port(node_id=node_id,
- address=data_utils.rand_mac_address())
- [1]['uuid'] for i in range(0, 5)]
-
- _, body = self.client.list_ports_detail()
-
- ports_dict = dict((port['uuid'], port) for port in body['ports']
- if port['uuid'] in uuids)
-
- for uuid in uuids:
- self.assertIn(uuid, ports_dict)
- port = ports_dict[uuid]
- self.assertIn('extra', port)
- self.assertIn('node_uuid', port)
- # never expose the node_id
- self.assertNotIn('node_id', port)
- # Verify self link.
- self.validate_self_link('ports', port['uuid'],
- port['links'][0]['href'])
-
- @test.idempotent_id('8a03f688-7d75-4ecd-8cbc-e06b8f346738')
- def test_list_ports_details_with_address(self):
- node_id = self.node['uuid']
- address = data_utils.rand_mac_address()
- self.create_port(node_id=node_id, address=address)
- for i in range(0, 5):
- self.create_port(node_id=node_id,
- address=data_utils.rand_mac_address())
-
- _, body = self.client.list_ports_detail(address=address)
- self.assertEqual(1, len(body['ports']))
- self.assertEqual(address, body['ports'][0]['address'])
-
- @test.idempotent_id('9c26298b-1bcb-47b7-9b9e-8bdd6e3c4aba')
- def test_update_port_replace(self):
- node_id = self.node['uuid']
- address = data_utils.rand_mac_address()
- extra = {'key1': 'value1', 'key2': 'value2', 'key3': 'value3'}
-
- _, port = self.create_port(node_id=node_id, address=address,
- extra=extra)
-
- new_address = data_utils.rand_mac_address()
- new_extra = {'key1': 'new-value1', 'key2': 'new-value2',
- 'key3': 'new-value3'}
-
- patch = [{'path': '/address',
- 'op': 'replace',
- 'value': new_address},
- {'path': '/extra/key1',
- 'op': 'replace',
- 'value': new_extra['key1']},
- {'path': '/extra/key2',
- 'op': 'replace',
- 'value': new_extra['key2']},
- {'path': '/extra/key3',
- 'op': 'replace',
- 'value': new_extra['key3']}]
-
- self.client.update_port(port['uuid'], patch)
-
- _, body = self.client.show_port(port['uuid'])
- self.assertEqual(new_address, body['address'])
- self.assertEqual(new_extra, body['extra'])
-
- @test.idempotent_id('d7e7fece-6ed9-460a-9ebe-9267217e8580')
- def test_update_port_remove(self):
- node_id = self.node['uuid']
- address = data_utils.rand_mac_address()
- extra = {'key1': 'value1', 'key2': 'value2', 'key3': 'value3'}
-
- _, port = self.create_port(node_id=node_id, address=address,
- extra=extra)
-
- # Removing one item from the collection
- self.client.update_port(port['uuid'],
- [{'path': '/extra/key2',
- 'op': 'remove'}])
- extra.pop('key2')
- _, body = self.client.show_port(port['uuid'])
- self.assertEqual(extra, body['extra'])
-
- # Removing the collection
- self.client.update_port(port['uuid'], [{'path': '/extra',
- 'op': 'remove'}])
- _, body = self.client.show_port(port['uuid'])
- self.assertEqual({}, body['extra'])
-
- # Assert nothing else was changed
- self.assertEqual(node_id, body['node_uuid'])
- self.assertEqual(address, body['address'])
-
- @test.idempotent_id('241288b3-e98a-400f-a4d7-d1f716146361')
- def test_update_port_add(self):
- node_id = self.node['uuid']
- address = data_utils.rand_mac_address()
-
- _, port = self.create_port(node_id=node_id, address=address)
-
- extra = {'key1': 'value1', 'key2': 'value2'}
-
- patch = [{'path': '/extra/key1',
- 'op': 'add',
- 'value': extra['key1']},
- {'path': '/extra/key2',
- 'op': 'add',
- 'value': extra['key2']}]
-
- self.client.update_port(port['uuid'], patch)
-
- _, body = self.client.show_port(port['uuid'])
- self.assertEqual(extra, body['extra'])
-
- @test.idempotent_id('5309e897-0799-4649-a982-0179b04c3876')
- def test_update_port_mixed_ops(self):
- node_id = self.node['uuid']
- address = data_utils.rand_mac_address()
- extra = {'key1': 'value1', 'key2': 'value2'}
-
- _, port = self.create_port(node_id=node_id, address=address,
- extra=extra)
-
- new_address = data_utils.rand_mac_address()
- new_extra = {'key1': 0.123, 'key3': {'cat': 'meow'}}
-
- patch = [{'path': '/address',
- 'op': 'replace',
- 'value': new_address},
- {'path': '/extra/key1',
- 'op': 'replace',
- 'value': new_extra['key1']},
- {'path': '/extra/key2',
- 'op': 'remove'},
- {'path': '/extra/key3',
- 'op': 'add',
- 'value': new_extra['key3']}]
-
- self.client.update_port(port['uuid'], patch)
-
- _, body = self.client.show_port(port['uuid'])
- self.assertEqual(new_address, body['address'])
- self.assertEqual(new_extra, body['extra'])
diff --git a/tempest/api/baremetal/admin/test_ports_negative.py b/tempest/api/baremetal/admin/test_ports_negative.py
deleted file mode 100644
index 610758a..0000000
--- a/tempest/api/baremetal/admin/test_ports_negative.py
+++ /dev/null
@@ -1,339 +0,0 @@
-# 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 import exceptions as lib_exc
-
-from tempest.api.baremetal.admin import base
-from tempest.common.utils import data_utils
-from tempest import test
-
-
-class TestPortsNegative(base.BaseBaremetalTest):
- """Negative tests for ports."""
-
- def setUp(self):
- super(TestPortsNegative, self).setUp()
-
- _, self.chassis = self.create_chassis()
- _, self.node = self.create_node(self.chassis['uuid'])
-
- @test.attr(type=['negative'])
- @test.idempotent_id('0a6ee1f7-d0d9-4069-8778-37f3aa07303a')
- def test_create_port_malformed_mac(self):
- node_id = self.node['uuid']
- address = 'malformed:mac'
-
- self.assertRaises(lib_exc.BadRequest,
- self.create_port, node_id=node_id, address=address)
-
- @test.attr(type=['negative'])
- @test.idempotent_id('30277ee8-0c60-4f1d-b125-0e51c2f43369')
- def test_create_port_nonexsistent_node_id(self):
- node_id = str(data_utils.rand_uuid())
- address = data_utils.rand_mac_address()
- self.assertRaises(lib_exc.BadRequest, self.create_port,
- node_id=node_id, address=address)
-
- @test.attr(type=['negative'])
- @test.idempotent_id('029190f6-43e1-40a3-b64a-65173ba653a3')
- def test_show_port_malformed_uuid(self):
- self.assertRaises(lib_exc.BadRequest, self.client.show_port,
- 'malformed:uuid')
-
- @test.attr(type=['negative'])
- @test.idempotent_id('0d00e13d-e2e0-45b1-bcbc-55a6d90ca793')
- def test_show_port_nonexistent_uuid(self):
- self.assertRaises(lib_exc.NotFound, self.client.show_port,
- data_utils.rand_uuid())
-
- @test.attr(type=['negative'])
- @test.idempotent_id('4ad85266-31e9-4942-99ac-751897dc9e23')
- def test_show_port_by_mac_not_allowed(self):
- self.assertRaises(lib_exc.BadRequest, self.client.show_port,
- data_utils.rand_mac_address())
-
- @test.attr(type=['negative'])
- @test.idempotent_id('89a34380-3c61-4c32-955c-2cd9ce94da21')
- def test_create_port_duplicated_port_uuid(self):
- node_id = self.node['uuid']
- address = data_utils.rand_mac_address()
- uuid = data_utils.rand_uuid()
-
- self.create_port(node_id=node_id, address=address, uuid=uuid)
- self.assertRaises(lib_exc.Conflict, self.create_port, node_id=node_id,
- address=address, uuid=uuid)
-
- @test.attr(type=['negative'])
- @test.idempotent_id('65e84917-733c-40ae-ae4b-96a4adff931c')
- def test_create_port_no_mandatory_field_node_id(self):
- address = data_utils.rand_mac_address()
-
- self.assertRaises(lib_exc.BadRequest, self.create_port, node_id=None,
- address=address)
-
- @test.attr(type=['negative'])
- @test.idempotent_id('bcea3476-7033-4183-acfe-e56a30809b46')
- def test_create_port_no_mandatory_field_mac(self):
- node_id = self.node['uuid']
-
- self.assertRaises(lib_exc.BadRequest, self.create_port,
- node_id=node_id, address=None)
-
- @test.attr(type=['negative'])
- @test.idempotent_id('2b51cd18-fb95-458b-9780-e6257787b649')
- def test_create_port_malformed_port_uuid(self):
- node_id = self.node['uuid']
- address = data_utils.rand_mac_address()
- uuid = 'malformed:uuid'
-
- self.assertRaises(lib_exc.BadRequest, self.create_port,
- node_id=node_id, address=address, uuid=uuid)
-
- @test.attr(type=['negative'])
- @test.idempotent_id('583a6856-6a30-4ac4-889f-14e2adff8105')
- def test_create_port_malformed_node_id(self):
- address = data_utils.rand_mac_address()
- self.assertRaises(lib_exc.BadRequest, self.create_port,
- node_id='malformed:nodeid', address=address)
-
- @test.attr(type=['negative'])
- @test.idempotent_id('e27f8b2e-42c6-4a43-a3cd-accff716bc5c')
- def test_create_port_duplicated_mac(self):
- node_id = self.node['uuid']
- address = data_utils.rand_mac_address()
- self.create_port(node_id=node_id, address=address)
- self.assertRaises(lib_exc.Conflict,
- self.create_port, node_id=node_id,
- address=address)
-
- @test.attr(type=['negative'])
- @test.idempotent_id('8907082d-ac5e-4be3-b05f-d072ede82020')
- def test_update_port_by_mac_not_allowed(self):
- node_id = self.node['uuid']
- address = data_utils.rand_mac_address()
- extra = {'key': 'value'}
-
- self.create_port(node_id=node_id, address=address, extra=extra)
-
- patch = [{'path': '/extra/key',
- 'op': 'replace',
- 'value': 'new-value'}]
-
- self.assertRaises(lib_exc.BadRequest,
- self.client.update_port, address,
- patch)
-
- @test.attr(type=['negative'])
- @test.idempotent_id('df1ac70c-db9f-41d9-90f1-78cd6b905718')
- def test_update_port_nonexistent(self):
- node_id = self.node['uuid']
- address = data_utils.rand_mac_address()
- extra = {'key': 'value'}
-
- _, port = self.create_port(node_id=node_id, address=address,
- extra=extra)
- port_id = port['uuid']
-
- _, body = self.client.delete_port(port_id)
-
- patch = [{'path': '/extra/key',
- 'op': 'replace',
- 'value': 'new-value'}]
- self.assertRaises(lib_exc.NotFound,
- self.client.update_port, port_id, patch)
-
- @test.attr(type=['negative'])
- @test.idempotent_id('c701e315-aa52-41ea-817c-65c5ca8ca2a8')
- def test_update_port_malformed_port_uuid(self):
- node_id = self.node['uuid']
- address = data_utils.rand_mac_address()
-
- self.create_port(node_id=node_id, address=address)
-
- new_address = data_utils.rand_mac_address()
- self.assertRaises(lib_exc.BadRequest, self.client.update_port,
- uuid='malformed:uuid',
- patch=[{'path': '/address', 'op': 'replace',
- 'value': new_address}])
-
- @test.attr(type=['negative'])
- @test.idempotent_id('f8f15803-34d6-45dc-b06f-e5e04bf1b38b')
- def test_update_port_add_nonexistent_property(self):
- node_id = self.node['uuid']
- address = data_utils.rand_mac_address()
-
- _, port = self.create_port(node_id=node_id, address=address)
- port_id = port['uuid']
-
- self.assertRaises(lib_exc.BadRequest, self.client.update_port, port_id,
- [{'path': '/nonexistent', ' op': 'add',
- 'value': 'value'}])
-
- @test.attr(type=['negative'])
- @test.idempotent_id('898ec904-38b1-4fcb-9584-1187d4263a2a')
- def test_update_port_replace_node_id_with_malformed(self):
- node_id = self.node['uuid']
- address = data_utils.rand_mac_address()
-
- _, port = self.create_port(node_id=node_id, address=address)
- port_id = port['uuid']
-
- patch = [{'path': '/node_uuid',
- 'op': 'replace',
- 'value': 'malformed:node_uuid'}]
- self.assertRaises(lib_exc.BadRequest,
- self.client.update_port, port_id, patch)
-
- @test.attr(type=['negative'])
- @test.idempotent_id('2949f30f-5f59-43fa-a6d9-4eac578afab4')
- def test_update_port_replace_mac_with_duplicated(self):
- node_id = self.node['uuid']
- address1 = data_utils.rand_mac_address()
- address2 = data_utils.rand_mac_address()
-
- _, port1 = self.create_port(node_id=node_id, address=address1)
-
- _, port2 = self.create_port(node_id=node_id, address=address2)
- port_id = port2['uuid']
-
- patch = [{'path': '/address',
- 'op': 'replace',
- 'value': address1}]
- self.assertRaises(lib_exc.Conflict,
- self.client.update_port, port_id, patch)
-
- @test.attr(type=['negative'])
- @test.idempotent_id('97f6e048-6e4f-4eba-a09d-fbbc78b77a77')
- def test_update_port_replace_node_id_with_nonexistent(self):
- node_id = self.node['uuid']
- address = data_utils.rand_mac_address()
-
- _, port = self.create_port(node_id=node_id, address=address)
- port_id = port['uuid']
-
- patch = [{'path': '/node_uuid',
- 'op': 'replace',
- 'value': data_utils.rand_uuid()}]
- self.assertRaises(lib_exc.BadRequest,
- self.client.update_port, port_id, patch)
-
- @test.attr(type=['negative'])
- @test.idempotent_id('375022c5-9e9e-4b11-9ca4-656729c0c9b2')
- def test_update_port_replace_mac_with_malformed(self):
- node_id = self.node['uuid']
- address = data_utils.rand_mac_address()
-
- _, port = self.create_port(node_id=node_id, address=address)
- port_id = port['uuid']
-
- patch = [{'path': '/address',
- 'op': 'replace',
- 'value': 'malformed:mac'}]
-
- self.assertRaises(lib_exc.BadRequest,
- self.client.update_port, port_id, patch)
-
- @test.attr(type=['negative'])
- @test.idempotent_id('5722b853-03fc-4854-8308-2036a1b67d85')
- def test_update_port_replace_nonexistent_property(self):
- node_id = self.node['uuid']
- address = data_utils.rand_mac_address()
-
- _, port = self.create_port(node_id=node_id, address=address)
- port_id = port['uuid']
-
- patch = [{'path': '/nonexistent', ' op': 'replace', 'value': 'value'}]
-
- self.assertRaises(lib_exc.BadRequest,
- self.client.update_port, port_id, patch)
-
- @test.attr(type=['negative'])
- @test.idempotent_id('ae2696ca-930a-4a7f-918f-30ae97c60f56')
- def test_update_port_remove_mandatory_field_mac(self):
- node_id = self.node['uuid']
- address = data_utils.rand_mac_address()
-
- _, port = self.create_port(node_id=node_id, address=address)
- port_id = port['uuid']
-
- self.assertRaises(lib_exc.BadRequest, self.client.update_port, port_id,
- [{'path': '/address', 'op': 'remove'}])
-
- @test.attr(type=['negative'])
- @test.idempotent_id('5392c1f0-2071-4697-9064-ec2d63019018')
- def test_update_port_remove_mandatory_field_port_uuid(self):
- node_id = self.node['uuid']
- address = data_utils.rand_mac_address()
-
- _, port = self.create_port(node_id=node_id, address=address)
- port_id = port['uuid']
-
- self.assertRaises(lib_exc.BadRequest, self.client.update_port, port_id,
- [{'path': '/uuid', 'op': 'remove'}])
-
- @test.attr(type=['negative'])
- @test.idempotent_id('06b50d82-802a-47ef-b079-0a3311cf85a2')
- def test_update_port_remove_nonexistent_property(self):
- node_id = self.node['uuid']
- address = data_utils.rand_mac_address()
-
- _, port = self.create_port(node_id=node_id, address=address)
- port_id = port['uuid']
-
- self.assertRaises(lib_exc.BadRequest, self.client.update_port, port_id,
- [{'path': '/nonexistent', 'op': 'remove'}])
-
- @test.attr(type=['negative'])
- @test.idempotent_id('03d42391-2145-4a6c-95bf-63fe55eb64fd')
- def test_delete_port_by_mac_not_allowed(self):
- node_id = self.node['uuid']
- address = data_utils.rand_mac_address()
-
- self.create_port(node_id=node_id, address=address)
- self.assertRaises(lib_exc.BadRequest, self.client.delete_port, address)
-
- @test.attr(type=['negative'])
- @test.idempotent_id('0629e002-818e-4763-b25b-ae5e07b1cb23')
- def test_update_port_mixed_ops_integrity(self):
- node_id = self.node['uuid']
- address = data_utils.rand_mac_address()
- extra = {'key1': 'value1', 'key2': 'value2'}
-
- _, port = self.create_port(node_id=node_id, address=address,
- extra=extra)
- port_id = port['uuid']
-
- new_address = data_utils.rand_mac_address()
- new_extra = {'key1': 'new-value1', 'key3': 'new-value3'}
-
- patch = [{'path': '/address',
- 'op': 'replace',
- 'value': new_address},
- {'path': '/extra/key1',
- 'op': 'replace',
- 'value': new_extra['key1']},
- {'path': '/extra/key2',
- 'op': 'remove'},
- {'path': '/extra/key3',
- 'op': 'add',
- 'value': new_extra['key3']},
- {'path': '/nonexistent',
- 'op': 'replace',
- 'value': 'value'}]
-
- self.assertRaises(lib_exc.BadRequest, self.client.update_port, port_id,
- patch)
-
- # patch should not be applied
- _, body = self.client.show_port(port_id)
- self.assertEqual(address, body['address'])
- self.assertEqual(extra, body['extra'])
diff --git a/tempest/api/compute/admin/test_agents.py b/tempest/api/compute/admin/test_agents.py
index 9f7bbae..0901374 100644
--- a/tempest/api/compute/admin/test_agents.py
+++ b/tempest/api/compute/admin/test_agents.py
@@ -12,14 +12,9 @@
# License for the specific language governing permissions and limitations
# under the License.
-from oslo_log import log
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.compute import base
-from tempest.common.utils import data_utils
-from tempest import test
-
-LOG = log.getLogger(__name__)
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
class AgentsAdminTestJSON(base.BaseV2ComputeAdminTest):
@@ -28,27 +23,18 @@
@classmethod
def setup_clients(cls):
super(AgentsAdminTestJSON, cls).setup_clients()
- cls.client = cls.os_adm.agents_client
+ cls.client = cls.os_admin.agents_client
- def setUp(self):
- super(AgentsAdminTestJSON, self).setUp()
- params = self._param_helper(
+ @classmethod
+ def resource_setup(cls):
+ super(AgentsAdminTestJSON, cls).resource_setup()
+ cls.params_agent = cls._param_helper(
hypervisor='common', os='linux', architecture='x86_64',
version='7.0', url='xxx://xxxx/xxx/xxx',
md5hash='add6bb58e139be103324d04d82d8f545')
- body = self.client.create_agent(**params)['agent']
- self.agent_id = body['agent_id']
- def tearDown(self):
- try:
- self.client.delete_agent(self.agent_id)
- except lib_exc.NotFound:
- pass
- except Exception:
- LOG.exception('Exception raised deleting agent %s', self.agent_id)
- super(AgentsAdminTestJSON, self).tearDown()
-
- def _param_helper(self, **kwargs):
+ @staticmethod
+ def _param_helper(**kwargs):
rand_key = 'architecture'
if rand_key in kwargs:
# NOTE: The rand_name is for avoiding agent conflicts.
@@ -58,7 +44,7 @@
kwargs[rand_key] = data_utils.rand_name(kwargs[rand_key])
return kwargs
- @test.idempotent_id('1fc6bdc8-0b6d-4cc7-9f30-9b04fabe5b90')
+ @decorators.idempotent_id('1fc6bdc8-0b6d-4cc7-9f30-9b04fabe5b90')
def test_create_agent(self):
# Create an agent.
params = self._param_helper(
@@ -70,35 +56,44 @@
for expected_item, value in params.items():
self.assertEqual(value, body[expected_item])
- @test.idempotent_id('dc9ffd51-1c50-4f0e-a820-ae6d2a568a9e')
+ @decorators.idempotent_id('dc9ffd51-1c50-4f0e-a820-ae6d2a568a9e')
def test_update_agent(self):
- # Update an agent.
+ # Create and update an agent.
+ body = self.client.create_agent(**self.params_agent)['agent']
+ self.addCleanup(self.client.delete_agent, body['agent_id'])
+ agent_id = body['agent_id']
params = self._param_helper(
version='8.0', url='xxx://xxxx/xxx/xxx2',
md5hash='add6bb58e139be103324d04d82d8f547')
- body = self.client.update_agent(self.agent_id, **params)['agent']
+ body = self.client.update_agent(agent_id, **params)['agent']
for expected_item, value in params.items():
self.assertEqual(value, body[expected_item])
- @test.idempotent_id('470e0b89-386f-407b-91fd-819737d0b335')
+ @decorators.idempotent_id('470e0b89-386f-407b-91fd-819737d0b335')
def test_delete_agent(self):
- # Delete an agent.
- self.client.delete_agent(self.agent_id)
+ # Create an agent and delete it.
+ body = self.client.create_agent(**self.params_agent)['agent']
+ self.client.delete_agent(body['agent_id'])
# Verify the list doesn't contain the deleted agent.
agents = self.client.list_agents()['agents']
- self.assertNotIn(self.agent_id, map(lambda x: x['agent_id'], agents))
+ self.assertNotIn(body['agent_id'], map(lambda x: x['agent_id'],
+ agents))
- @test.idempotent_id('6a326c69-654b-438a-80a3-34bcc454e138')
+ @decorators.idempotent_id('6a326c69-654b-438a-80a3-34bcc454e138')
def test_list_agents(self):
- # List all agents.
+ # Create an agent and list all agents.
+ body = self.client.create_agent(**self.params_agent)['agent']
+ self.addCleanup(self.client.delete_agent, body['agent_id'])
agents = self.client.list_agents()['agents']
- self.assertTrue(len(agents) > 0, 'Cannot get any agents.(%s)' % agents)
- self.assertIn(self.agent_id, map(lambda x: x['agent_id'], agents))
+ self.assertNotEmpty(agents, 'Cannot get any agents.')
+ self.assertIn(body['agent_id'], map(lambda x: x['agent_id'], agents))
- @test.idempotent_id('eabadde4-3cd7-4ec4-a4b5-5a936d2d4408')
+ @decorators.idempotent_id('eabadde4-3cd7-4ec4-a4b5-5a936d2d4408')
def test_list_agents_with_filter(self):
- # List the agent builds by the filter.
+ # Create agents and list the agent builds by the filter.
+ body = self.client.create_agent(**self.params_agent)['agent']
+ self.addCleanup(self.client.delete_agent, body['agent_id'])
params = self._param_helper(
hypervisor='xen', os='linux', architecture='x86',
version='7.0', url='xxx://xxxx/xxx/xxx1',
@@ -109,6 +104,7 @@
agent_id_xen = agent_xen['agent_id']
agents = (self.client.list_agents(hypervisor=agent_xen['hypervisor'])
['agents'])
- self.assertTrue(len(agents) > 0, 'Cannot get any agents.(%s)' % agents)
+ self.assertNotEmpty(agents, 'Cannot get any agents.')
self.assertIn(agent_id_xen, map(lambda x: x['agent_id'], agents))
- self.assertNotIn(self.agent_id, map(lambda x: x['agent_id'], agents))
+ self.assertNotIn(body['agent_id'], map(lambda x: x['agent_id'],
+ agents))
diff --git a/tempest/api/compute/admin/test_aggregates.py b/tempest/api/compute/admin/test_aggregates.py
index 1d83fec..57d3983 100644
--- a/tempest/api/compute/admin/test_aggregates.py
+++ b/tempest/api/compute/admin/test_aggregates.py
@@ -13,23 +13,25 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
+import testtools
from tempest.api.compute import base
from tempest.common import tempest_fixtures as fixtures
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+
+CONF = config.CONF
class AggregatesAdminTestJSON(base.BaseV2ComputeAdminTest):
"""Tests Aggregates API that require admin privileges"""
- _host_key = 'OS-EXT-SRV-ATTR:host'
-
@classmethod
def setup_clients(cls):
super(AggregatesAdminTestJSON, cls).setup_clients()
- cls.client = cls.os_adm.aggregates_client
+ cls.client = cls.os_admin.aggregates_client
@classmethod
def resource_setup(cls):
@@ -37,67 +39,68 @@
cls.aggregate_name_prefix = 'test_aggregate'
cls.az_name_prefix = 'test_az'
- hosts_all = cls.os_adm.hosts_client.list_hosts()['hosts']
- hosts = map(lambda x: x['host_name'],
- filter(lambda y: y['service'] == 'compute', hosts_all))
- cls.host = hosts[0]
+ cls.host = None
+ hypers = cls.os_admin.hypervisor_client.list_hypervisors(
+ detail=True)['hypervisors']
- def _try_delete_aggregate(self, aggregate_id):
- # delete aggregate, if it exists
- try:
- self.client.delete_aggregate(aggregate_id)
- # if aggregate not found, it depict it was deleted in the test
- except lib_exc.NotFound:
- pass
+ if CONF.compute.hypervisor_type:
+ hypers = [hyper for hyper in hypers
+ if (hyper['hypervisor_type'] ==
+ CONF.compute.hypervisor_type)]
- @test.idempotent_id('0d148aa3-d54c-4317-aa8d-42040a475e20')
+ hosts_available = [hyper['service']['host'] for hyper in hypers
+ if (hyper['state'] == 'up' and
+ hyper['status'] == 'enabled')]
+ if hosts_available:
+ cls.host = hosts_available[0]
+ else:
+ msg = "no available compute node found"
+ if CONF.compute.hypervisor_type:
+ msg += " for hypervisor_type %s" % CONF.compute.hypervisor_type
+ raise testtools.TestCase.failureException(msg)
+
+ def _create_test_aggregate(self, **kwargs):
+ if 'name' not in kwargs:
+ kwargs['name'] = data_utils.rand_name(self.aggregate_name_prefix)
+ aggregate = self.client.create_aggregate(**kwargs)['aggregate']
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.client.delete_aggregate, aggregate['id'])
+ self.assertEqual(kwargs['name'], aggregate['name'])
+
+ return aggregate
+
+ @decorators.idempotent_id('0d148aa3-d54c-4317-aa8d-42040a475e20')
def test_aggregate_create_delete(self):
# Create and delete an aggregate.
- aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
- aggregate = (self.client.create_aggregate(name=aggregate_name)
- ['aggregate'])
- self.addCleanup(self._try_delete_aggregate, aggregate['id'])
- self.assertEqual(aggregate_name, aggregate['name'])
+ aggregate = self._create_test_aggregate()
self.assertIsNone(aggregate['availability_zone'])
self.client.delete_aggregate(aggregate['id'])
self.client.wait_for_resource_deletion(aggregate['id'])
- @test.idempotent_id('5873a6f8-671a-43ff-8838-7ce430bb6d0b')
+ @decorators.idempotent_id('5873a6f8-671a-43ff-8838-7ce430bb6d0b')
def test_aggregate_create_delete_with_az(self):
# Create and delete an aggregate.
- aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
az_name = data_utils.rand_name(self.az_name_prefix)
- aggregate = self.client.create_aggregate(
- name=aggregate_name, availability_zone=az_name)['aggregate']
- self.addCleanup(self._try_delete_aggregate, aggregate['id'])
- self.assertEqual(aggregate_name, aggregate['name'])
+ aggregate = self._create_test_aggregate(availability_zone=az_name)
self.assertEqual(az_name, aggregate['availability_zone'])
self.client.delete_aggregate(aggregate['id'])
self.client.wait_for_resource_deletion(aggregate['id'])
- @test.idempotent_id('68089c38-04b1-4758-bdf0-cf0daec4defd')
+ @decorators.idempotent_id('68089c38-04b1-4758-bdf0-cf0daec4defd')
def test_aggregate_create_verify_entry_in_list(self):
# Create an aggregate and ensure it is listed.
- aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
- aggregate = (self.client.create_aggregate(name=aggregate_name)
- ['aggregate'])
- self.addCleanup(self.client.delete_aggregate, aggregate['id'])
-
+ aggregate = self._create_test_aggregate()
aggregates = self.client.list_aggregates()['aggregates']
self.assertIn((aggregate['id'], aggregate['availability_zone']),
map(lambda x: (x['id'], x['availability_zone']),
aggregates))
- @test.idempotent_id('36ec92ca-7a73-43bc-b920-7531809e8540')
+ @decorators.idempotent_id('36ec92ca-7a73-43bc-b920-7531809e8540')
def test_aggregate_create_update_metadata_get_details(self):
# Create an aggregate and ensure its details are returned.
- aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
- aggregate = (self.client.create_aggregate(name=aggregate_name)
- ['aggregate'])
- self.addCleanup(self.client.delete_aggregate, aggregate['id'])
-
+ aggregate = self._create_test_aggregate()
body = self.client.show_aggregate(aggregate['id'])['aggregate']
self.assertEqual(aggregate['name'], body['name'])
self.assertEqual(aggregate['availability_zone'],
@@ -113,18 +116,15 @@
body = self.client.show_aggregate(aggregate['id'])['aggregate']
self.assertEqual(meta, body["metadata"])
- @test.idempotent_id('4d2b2004-40fa-40a1-aab2-66f4dab81beb')
+ @decorators.idempotent_id('4d2b2004-40fa-40a1-aab2-66f4dab81beb')
def test_aggregate_create_update_with_az(self):
# Update an aggregate and ensure properties are updated correctly
aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
az_name = data_utils.rand_name(self.az_name_prefix)
- aggregate = self.client.create_aggregate(
- name=aggregate_name, availability_zone=az_name)['aggregate']
- self.addCleanup(self.client.delete_aggregate, aggregate['id'])
+ aggregate = self._create_test_aggregate(
+ name=aggregate_name, availability_zone=az_name)
- self.assertEqual(aggregate_name, aggregate['name'])
self.assertEqual(az_name, aggregate['availability_zone'])
- self.assertIsNotNone(aggregate['id'])
aggregate_id = aggregate['id']
new_aggregate_name = aggregate_name + '_new'
@@ -143,14 +143,12 @@
(x['id'], x['name'], x['availability_zone']),
aggregates))
- @test.idempotent_id('c8e85064-e79b-4906-9931-c11c24294d02')
+ @decorators.idempotent_id('c8e85064-e79b-4906-9931-c11c24294d02')
def test_aggregate_add_remove_host(self):
# Add a host to the given aggregate and remove.
self.useFixture(fixtures.LockFixture('availability_zone'))
aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
- aggregate = (self.client.create_aggregate(name=aggregate_name)
- ['aggregate'])
- self.addCleanup(self.client.delete_aggregate, aggregate['id'])
+ aggregate = self._create_test_aggregate(name=aggregate_name)
body = (self.client.add_host(aggregate['id'], host=self.host)
['aggregate'])
@@ -166,34 +164,32 @@
body['availability_zone'])
self.assertNotIn(self.host, body['hosts'])
- @test.idempotent_id('7f6a1cc5-2446-4cdb-9baa-b6ae0a919b72')
+ @decorators.idempotent_id('7f6a1cc5-2446-4cdb-9baa-b6ae0a919b72')
def test_aggregate_add_host_list(self):
# Add a host to the given aggregate and list.
self.useFixture(fixtures.LockFixture('availability_zone'))
aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
- aggregate = (self.client.create_aggregate(name=aggregate_name)
- ['aggregate'])
- self.addCleanup(self.client.delete_aggregate, aggregate['id'])
+ aggregate = self._create_test_aggregate(name=aggregate_name)
+
self.client.add_host(aggregate['id'], host=self.host)
self.addCleanup(self.client.remove_host, aggregate['id'],
host=self.host)
aggregates = self.client.list_aggregates()['aggregates']
- aggs = filter(lambda x: x['id'] == aggregate['id'], aggregates)
+ aggs = [agg for agg in aggregates if agg['id'] == aggregate['id']]
self.assertEqual(1, len(aggs))
agg = aggs[0]
self.assertEqual(aggregate_name, agg['name'])
self.assertIsNone(agg['availability_zone'])
self.assertIn(self.host, agg['hosts'])
- @test.idempotent_id('eeef473c-7c52-494d-9f09-2ed7fc8fc036')
+ @decorators.idempotent_id('eeef473c-7c52-494d-9f09-2ed7fc8fc036')
def test_aggregate_add_host_get_details(self):
# Add a host to the given aggregate and get details.
self.useFixture(fixtures.LockFixture('availability_zone'))
aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
- aggregate = (self.client.create_aggregate(name=aggregate_name)
- ['aggregate'])
- self.addCleanup(self.client.delete_aggregate, aggregate['id'])
+ aggregate = self._create_test_aggregate(name=aggregate_name)
+
self.client.add_host(aggregate['id'], host=self.host)
self.addCleanup(self.client.remove_host, aggregate['id'],
host=self.host)
@@ -203,22 +199,18 @@
self.assertIsNone(body['availability_zone'])
self.assertIn(self.host, body['hosts'])
- @test.idempotent_id('96be03c7-570d-409c-90f8-e4db3c646996')
+ @decorators.idempotent_id('96be03c7-570d-409c-90f8-e4db3c646996')
def test_aggregate_add_host_create_server_with_az(self):
# Add a host to the given aggregate and create a server.
self.useFixture(fixtures.LockFixture('availability_zone'))
- aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
az_name = data_utils.rand_name(self.az_name_prefix)
- aggregate = self.client.create_aggregate(
- name=aggregate_name, availability_zone=az_name)['aggregate']
- self.addCleanup(self.client.delete_aggregate, aggregate['id'])
+ aggregate = self._create_test_aggregate(availability_zone=az_name)
+
self.client.add_host(aggregate['id'], host=self.host)
self.addCleanup(self.client.remove_host, aggregate['id'],
host=self.host)
- server_name = data_utils.rand_name('test_server')
- admin_servers_client = self.os_adm.servers_client
- server = self.create_test_server(name=server_name,
- availability_zone=az_name,
+ admin_servers_client = self.os_admin.servers_client
+ server = self.create_test_server(availability_zone=az_name,
wait_until='ACTIVE')
body = admin_servers_client.show_server(server['id'])['server']
- self.assertEqual(self.host, body[self._host_key])
+ self.assertEqual(self.host, body['OS-EXT-SRV-ATTR:host'])
diff --git a/tempest/api/compute/admin/test_aggregates_negative.py b/tempest/api/compute/admin/test_aggregates_negative.py
index 181533b..a6e0efa 100644
--- a/tempest/api/compute/admin/test_aggregates_negative.py
+++ b/tempest/api/compute/admin/test_aggregates_negative.py
@@ -13,12 +13,11 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.compute import base
from tempest.common import tempest_fixtures as fixtures
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class AggregatesAdminNegativeTestJSON(base.BaseV2ComputeAdminTest):
@@ -27,39 +26,45 @@
@classmethod
def setup_clients(cls):
super(AggregatesAdminNegativeTestJSON, cls).setup_clients()
- cls.client = cls.os_adm.aggregates_client
- cls.user_client = cls.aggregates_client
+ cls.client = cls.os_admin.aggregates_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'
- cls.az_name_prefix = 'test_az'
- hosts_all = cls.os_adm.hosts_client.list_hosts()['hosts']
- hosts = map(lambda x: x['host_name'],
- filter(lambda y: y['service'] == 'compute', hosts_all))
- cls.host = hosts[0]
+ 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']
- @test.attr(type=['negative'])
- @test.idempotent_id('86a1cb14-da37-4a70-b056-903fd56dfe29')
+ def _create_test_aggregate(self):
+ aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
+ aggregate = (self.client.create_aggregate(name=aggregate_name)
+ ['aggregate'])
+ self.addCleanup(self.client.delete_aggregate, aggregate['id'])
+ return aggregate
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('86a1cb14-da37-4a70-b056-903fd56dfe29')
def test_aggregate_create_as_user(self):
# Regular user is not allowed to create an aggregate.
aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
self.assertRaises(lib_exc.Forbidden,
- self.user_client.create_aggregate,
+ self.aggregates_client.create_aggregate,
name=aggregate_name)
- @test.attr(type=['negative'])
- @test.idempotent_id('3b8a1929-3793-4e92-bcb4-dfa572ee6c1d')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('3b8a1929-3793-4e92-bcb4-dfa572ee6c1d')
def test_aggregate_create_aggregate_name_length_less_than_1(self):
# the length of aggregate name should >= 1 and <=255
self.assertRaises(lib_exc.BadRequest,
self.client.create_aggregate,
name='')
- @test.attr(type=['negative'])
- @test.idempotent_id('4c194563-543b-4e70-a719-557bbe947fac')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('4c194563-543b-4e70-a719-557bbe947fac')
def test_aggregate_create_aggregate_name_length_exceeds_255(self):
# the length of aggregate name should >= 1 and <=255
aggregate_name = 'a' * 256
@@ -67,139 +72,107 @@
self.client.create_aggregate,
name=aggregate_name)
- @test.attr(type=['negative'])
- @test.idempotent_id('9c23a291-b0b1-487b-b464-132e061151b3')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('9c23a291-b0b1-487b-b464-132e061151b3')
def test_aggregate_create_with_existent_aggregate_name(self):
# creating an aggregate with existent aggregate name is forbidden
- aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
- aggregate = self.client.create_aggregate(name=aggregate_name)
- self.addCleanup(self.client.delete_aggregate,
- aggregate['aggregate']['id'])
-
+ aggregate = self._create_test_aggregate()
self.assertRaises(lib_exc.Conflict,
self.client.create_aggregate,
- name=aggregate_name)
+ name=aggregate['name'])
- @test.attr(type=['negative'])
- @test.idempotent_id('cd6de795-c15d-45f1-8d9e-813c6bb72a3d')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('cd6de795-c15d-45f1-8d9e-813c6bb72a3d')
def test_aggregate_delete_as_user(self):
# Regular user is not allowed to delete an aggregate.
- aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
- aggregate = (self.client.create_aggregate(name=aggregate_name)
- ['aggregate'])
- self.addCleanup(self.client.delete_aggregate, aggregate['id'])
-
+ aggregate = self._create_test_aggregate()
self.assertRaises(lib_exc.Forbidden,
- self.user_client.delete_aggregate,
+ self.aggregates_client.delete_aggregate,
aggregate['id'])
- @test.attr(type=['negative'])
- @test.idempotent_id('b7d475a6-5dcd-4ff4-b70a-cd9de66a6672')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('b7d475a6-5dcd-4ff4-b70a-cd9de66a6672')
def test_aggregate_list_as_user(self):
# Regular user is not allowed to list aggregates.
self.assertRaises(lib_exc.Forbidden,
- self.user_client.list_aggregates)
+ self.aggregates_client.list_aggregates)
- @test.attr(type=['negative'])
- @test.idempotent_id('557cad12-34c9-4ff4-95f0-22f0dfbaf7dc')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('557cad12-34c9-4ff4-95f0-22f0dfbaf7dc')
def test_aggregate_get_details_as_user(self):
# Regular user is not allowed to get aggregate details.
- aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
- aggregate = (self.client.create_aggregate(name=aggregate_name)
- ['aggregate'])
- self.addCleanup(self.client.delete_aggregate, aggregate['id'])
-
+ aggregate = self._create_test_aggregate()
self.assertRaises(lib_exc.Forbidden,
- self.user_client.show_aggregate,
+ self.aggregates_client.show_aggregate,
aggregate['id'])
- @test.attr(type=['negative'])
- @test.idempotent_id('c74f4bf1-4708-4ff2-95a0-f49eaca951bd')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('c74f4bf1-4708-4ff2-95a0-f49eaca951bd')
def test_aggregate_delete_with_invalid_id(self):
# Delete an aggregate with invalid id should raise exceptions.
self.assertRaises(lib_exc.NotFound,
self.client.delete_aggregate, -1)
- @test.attr(type=['negative'])
- @test.idempotent_id('3c916244-2c46-49a4-9b55-b20bb0ae512c')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('3c916244-2c46-49a4-9b55-b20bb0ae512c')
def test_aggregate_get_details_with_invalid_id(self):
# Get aggregate details with invalid id should raise exceptions.
self.assertRaises(lib_exc.NotFound,
self.client.show_aggregate, -1)
- @test.attr(type=['negative'])
- @test.idempotent_id('0ef07828-12b4-45ba-87cc-41425faf5711')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('0ef07828-12b4-45ba-87cc-41425faf5711')
def test_aggregate_add_non_exist_host(self):
# Adding a non-exist host to an aggregate should raise exceptions.
- hosts_all = self.os_adm.hosts_client.list_hosts()['hosts']
- hosts = map(lambda x: x['host_name'], hosts_all)
while True:
non_exist_host = data_utils.rand_name('nonexist_host')
- if non_exist_host not in hosts:
+ if non_exist_host not in self.hosts:
break
-
- aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
- aggregate = (self.client.create_aggregate(name=aggregate_name)
- ['aggregate'])
- self.addCleanup(self.client.delete_aggregate, aggregate['id'])
-
+ aggregate = self._create_test_aggregate()
self.assertRaises(lib_exc.NotFound, self.client.add_host,
aggregate['id'], host=non_exist_host)
- @test.attr(type=['negative'])
- @test.idempotent_id('7324c334-bd13-4c93-8521-5877322c3d51')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('7324c334-bd13-4c93-8521-5877322c3d51')
def test_aggregate_add_host_as_user(self):
# Regular user is not allowed to add a host to an aggregate.
- aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
- aggregate = (self.client.create_aggregate(name=aggregate_name)
- ['aggregate'])
- self.addCleanup(self.client.delete_aggregate, aggregate['id'])
-
+ aggregate = self._create_test_aggregate()
self.assertRaises(lib_exc.Forbidden,
- self.user_client.add_host,
- aggregate['id'], host=self.host)
+ self.aggregates_client.add_host,
+ aggregate['id'], host=self.hosts[0])
- @test.attr(type=['negative'])
- @test.idempotent_id('19dd44e1-c435-4ee1-a402-88c4f90b5950')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('19dd44e1-c435-4ee1-a402-88c4f90b5950')
def test_aggregate_add_existent_host(self):
self.useFixture(fixtures.LockFixture('availability_zone'))
- aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
- aggregate = (self.client.create_aggregate(name=aggregate_name)
- ['aggregate'])
- self.addCleanup(self.client.delete_aggregate, aggregate['id'])
+ aggregate = self._create_test_aggregate()
- self.client.add_host(aggregate['id'], host=self.host)
+ self.client.add_host(aggregate['id'], host=self.hosts[0])
self.addCleanup(self.client.remove_host, aggregate['id'],
- host=self.host)
+ host=self.hosts[0])
self.assertRaises(lib_exc.Conflict, self.client.add_host,
- aggregate['id'], host=self.host)
+ aggregate['id'], host=self.hosts[0])
- @test.attr(type=['negative'])
- @test.idempotent_id('7a53af20-137a-4e44-a4ae-e19260e626d9')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('7a53af20-137a-4e44-a4ae-e19260e626d9')
def test_aggregate_remove_host_as_user(self):
# Regular user is not allowed to remove a host from an aggregate.
self.useFixture(fixtures.LockFixture('availability_zone'))
- aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
- aggregate = (self.client.create_aggregate(name=aggregate_name)
- ['aggregate'])
- self.addCleanup(self.client.delete_aggregate, aggregate['id'])
- self.client.add_host(aggregate['id'], host=self.host)
+ aggregate = self._create_test_aggregate()
+
+ self.client.add_host(aggregate['id'], host=self.hosts[0])
self.addCleanup(self.client.remove_host, aggregate['id'],
- host=self.host)
+ host=self.hosts[0])
self.assertRaises(lib_exc.Forbidden,
- self.user_client.remove_host,
- aggregate['id'], host=self.host)
+ self.aggregates_client.remove_host,
+ aggregate['id'], host=self.hosts[0])
- @test.attr(type=['negative'])
- @test.idempotent_id('95d6a6fa-8da9-4426-84d0-eec0329f2e4d')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('95d6a6fa-8da9-4426-84d0-eec0329f2e4d')
def test_aggregate_remove_nonexistent_host(self):
- non_exist_host = data_utils.rand_name('nonexist_host')
- aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
- aggregate = (self.client.create_aggregate(name=aggregate_name)
- ['aggregate'])
- self.addCleanup(self.client.delete_aggregate, aggregate['id'])
+ aggregate = self._create_test_aggregate()
self.assertRaises(lib_exc.NotFound, self.client.remove_host,
- aggregate['id'], host=non_exist_host)
+ aggregate['id'], host='nonexist_host')
diff --git a/tempest/api/compute/admin/test_auto_allocate_network.py b/tempest/api/compute/admin/test_auto_allocate_network.py
new file mode 100644
index 0000000..e8011a6
--- /dev/null
+++ b/tempest/api/compute/admin/test_auto_allocate_network.py
@@ -0,0 +1,204 @@
+# Copyright 2016 IBM Corp.
+#
+# 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_log import log
+
+from tempest.api.compute import base
+from tempest.common import compute
+from tempest.common import utils
+from tempest import config
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_excs
+
+CONF = config.CONF
+LOG = log.getLogger(__name__)
+
+
+# NOTE(mriedem): This is in the admin directory only because it requires
+# force_tenant_isolation=True, but doesn't extend BaseV2ComputeAdminTest
+# because it doesn't actually use any admin credentials in the tests.
+class AutoAllocateNetworkTest(base.BaseV2ComputeTest):
+ """Tests auto-allocating networks with the v2.37 microversion.
+
+ These tests rely on Neutron being enabled. Also, the tenant must not have
+ any network resources available to it so we can make sure that Nova
+ calls to Neutron to automatically allocate the network topology.
+ """
+
+ force_tenant_isolation = True
+
+ min_microversion = '2.37'
+ max_microversion = 'latest'
+
+ @classmethod
+ def skip_checks(cls):
+ super(AutoAllocateNetworkTest, cls).skip_checks()
+ if not CONF.service_available.neutron:
+ raise cls.skipException('Neutron is required')
+ if not utils.is_extension_enabled('auto-allocated-topology',
+ 'network'):
+ raise cls.skipException(
+ 'auto-allocated-topology extension is not available')
+
+ @classmethod
+ def setup_credentials(cls):
+ # Do not create network resources for these tests.
+ cls.set_network_resources()
+ super(AutoAllocateNetworkTest, cls).setup_credentials()
+
+ @classmethod
+ def setup_clients(cls):
+ super(AutoAllocateNetworkTest, cls).setup_clients()
+ cls.networks_client = cls.os_primary.networks_client
+ cls.routers_client = cls.os_primary.routers_client
+ cls.subnets_client = cls.os_primary.subnets_client
+ cls.ports_client = cls.os_primary.ports_client
+
+ @classmethod
+ def resource_setup(cls):
+ super(AutoAllocateNetworkTest, cls).resource_setup()
+ # Sanity check that there are no networks available to the tenant.
+ # This is essentially what Nova does for getting available networks.
+ tenant_id = cls.networks_client.tenant_id
+ # (1) Retrieve non-public network list owned by the tenant.
+ search_opts = {'tenant_id': tenant_id, 'shared': False}
+ nets = cls.networks_client.list_networks(
+ **search_opts).get('networks', [])
+ if nets:
+ raise lib_excs.TempestException(
+ 'Found tenant networks: %s' % nets)
+ # (2) Retrieve shared network list.
+ search_opts = {'shared': True}
+ nets = cls.networks_client.list_networks(
+ **search_opts).get('networks', [])
+ if nets:
+ raise cls.skipException('Found shared networks: %s' % nets)
+
+ @classmethod
+ def resource_cleanup(cls):
+ """Deletes any auto_allocated_network and it's associated resources."""
+
+ # Find the auto-allocated router for the tenant.
+ # This is a bit hacky since we don't have a great way to find the
+ # auto-allocated router given the private tenant network we have.
+ routers = cls.routers_client.list_routers().get('routers', [])
+ if len(routers) > 1:
+ # This indicates a race where nova is concurrently calling the
+ # neutron auto-allocated-topology API for multiple server builds
+ # at the same time (it's called from nova-compute when setting up
+ # networking for a server). Neutron will detect duplicates and
+ # automatically clean them up, but there is a window where the API
+ # can return multiple and we don't have a good way to filter those
+ # out right now, so we'll just handle them.
+ LOG.info('(%s) Found more than one router for tenant.',
+ test_utils.find_test_caller())
+
+ # Remove any networks, duplicate or otherwise, that these tests
+ # created. All such networks will be in the current tenant. Neutron
+ # will cleanup duplicate resources automatically, so ignore 404s.
+ search_opts = {'tenant_id': cls.networks_client.tenant_id}
+ networks = cls.networks_client.list_networks(
+ **search_opts).get('networks', [])
+
+ for router in routers:
+ # Disassociate the subnets from the router. Because of the race
+ # mentioned above the subnets might not be associated with the
+ # router so ignore any 404.
+ for network in networks:
+ for subnet_id in network['subnets']:
+ test_utils.call_and_ignore_notfound_exc(
+ cls.routers_client.remove_router_interface,
+ router['id'], subnet_id=subnet_id)
+
+ # Delete the router.
+ cls.routers_client.delete_router(router['id'])
+
+ for network in networks:
+ # Get and delete the ports for the given network.
+ ports = cls.ports_client.list_ports(
+ network_id=network['id']).get('ports', [])
+ for port in ports:
+ test_utils.call_and_ignore_notfound_exc(
+ cls.ports_client.delete_port, port['id'])
+
+ # Delete the subnets.
+ for subnet_id in network['subnets']:
+ test_utils.call_and_ignore_notfound_exc(
+ cls.subnets_client.delete_subnet, subnet_id)
+
+ # Delete the network.
+ test_utils.call_and_ignore_notfound_exc(
+ cls.networks_client.delete_network, network['id'])
+
+ super(AutoAllocateNetworkTest, cls).resource_cleanup()
+
+ @decorators.idempotent_id('5eb7b8fa-9c23-47a2-9d7d-02ed5809dd34')
+ def test_server_create_no_allocate(self):
+ """Tests that no networking is allocated for the server."""
+ # create the server with no networking
+ server = self.create_test_server(networks='none', wait_until='ACTIVE')
+ # get the server ips
+ addresses = self.servers_client.list_addresses(
+ server['id'])['addresses']
+ # assert that there is no networking
+ self.assertEqual({}, addresses)
+
+ @decorators.idempotent_id('2e6cf129-9e28-4e8a-aaaa-045ea826b2a6')
+ def test_server_multi_create_auto_allocate(self):
+ """Tests that networking is auto-allocated for multiple servers."""
+
+ # Create multiple servers with auto networking to make sure the
+ # automatic network allocation is atomic. Using a minimum of three
+ # servers is essential for this scenario because:
+ #
+ # - First request sees no networks for the tenant so it auto-allocates
+ # one from Neutron, let's call that net1.
+ # - Second request sees no networks for the tenant so it auto-allocates
+ # one from Neutron. Neutron creates net2 but sees it's a duplicate
+ # so it queues net2 for deletion and returns net1 from the API and
+ # Nova uses that for the second server request.
+ # - Third request sees net1 and net2 for the tenant and fails with a
+ # NetworkAmbiguous 400 error.
+ _, servers = compute.create_test_server(
+ self.os_primary, networks='auto', wait_until='ACTIVE',
+ min_count=3)
+ for server in servers:
+ self.addCleanup(self.delete_server, server['id'])
+
+ server_nets = set()
+ for server in servers:
+ # get the server ips
+ addresses = self.servers_client.list_addresses(
+ server['id'])['addresses']
+ # assert that there is networking (should only be one)
+ self.assertEqual(1, len(addresses))
+ server_nets.add(list(addresses.keys())[0])
+ # all servers should be on the same network
+ self.assertEqual(1, len(server_nets))
+
+ # List the networks for the tenant; we filter on admin_state_up=True
+ # because the auto-allocated-topology code in Neutron won't set that
+ # to True until the network is ready and is returned from the API.
+ # Duplicate networks created from a race should have
+ # admin_state_up=False.
+ search_opts = {'tenant_id': self.networks_client.tenant_id,
+ 'shared': False,
+ 'admin_state_up': True}
+ nets = self.networks_client.list_networks(
+ **search_opts).get('networks', [])
+ self.assertEqual(1, len(nets))
+ # verify the single private tenant network is the one that the servers
+ # are using also
+ self.assertIn(nets[0]['name'], server_nets)
diff --git a/tempest/api/compute/admin/test_availability_zone.py b/tempest/api/compute/admin/test_availability_zone.py
index 5befa53..bbd39b6 100644
--- a/tempest/api/compute/admin/test_availability_zone.py
+++ b/tempest/api/compute/admin/test_availability_zone.py
@@ -14,7 +14,7 @@
# under the License.
from tempest.api.compute import base
-from tempest import test
+from tempest.lib import decorators
class AZAdminV2TestJSON(base.BaseV2ComputeAdminTest):
@@ -25,14 +25,14 @@
super(AZAdminV2TestJSON, cls).setup_clients()
cls.client = cls.availability_zone_admin_client
- @test.idempotent_id('d3431479-8a09-4f76-aa2d-26dc580cb27c')
+ @decorators.idempotent_id('d3431479-8a09-4f76-aa2d-26dc580cb27c')
def test_get_availability_zone_list(self):
# List of availability zone
availability_zone = self.client.list_availability_zones()
- self.assertTrue(len(availability_zone['availabilityZoneInfo']) > 0)
+ self.assertNotEmpty(availability_zone['availabilityZoneInfo'])
- @test.idempotent_id('ef726c58-530f-44c2-968c-c7bed22d5b8c')
+ @decorators.idempotent_id('ef726c58-530f-44c2-968c-c7bed22d5b8c')
def test_get_availability_zone_list_detail(self):
# List of availability zones and available services
availability_zone = self.client.list_availability_zones(detail=True)
- self.assertTrue(len(availability_zone['availabilityZoneInfo']) > 0)
+ self.assertNotEmpty(availability_zone['availabilityZoneInfo'])
diff --git a/tempest/api/compute/admin/test_availability_zone_negative.py b/tempest/api/compute/admin/test_availability_zone_negative.py
index fe979d4..a58c22c 100644
--- a/tempest/api/compute/admin/test_availability_zone_negative.py
+++ b/tempest/api/compute/admin/test_availability_zone_negative.py
@@ -12,10 +12,9 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.compute import base
-from tempest import test
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class AZAdminNegativeTestJSON(base.BaseV2ComputeAdminTest):
@@ -26,8 +25,8 @@
super(AZAdminNegativeTestJSON, cls).setup_clients()
cls.non_adm_client = cls.availability_zone_client
- @test.attr(type=['negative'])
- @test.idempotent_id('bf34dca2-fdc3-4073-9c02-7648d9eae0d7')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('bf34dca2-fdc3-4073-9c02-7648d9eae0d7')
def test_get_availability_zone_list_detail_with_non_admin_user(self):
# List of availability zones and available services with
# non-administrator user
diff --git a/tempest/api/compute/admin/test_baremetal_nodes.py b/tempest/api/compute/admin/test_baremetal_nodes.py
deleted file mode 100644
index b764483..0000000
--- a/tempest/api/compute/admin/test_baremetal_nodes.py
+++ /dev/null
@@ -1,54 +0,0 @@
-# Copyright 2015 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 import config
-from tempest import test
-
-CONF = config.CONF
-
-
-class BaremetalNodesAdminTestJSON(base.BaseV2ComputeAdminTest):
- """Tests Baremetal API"""
-
- @classmethod
- def resource_setup(cls):
- super(BaremetalNodesAdminTestJSON, cls).resource_setup()
- if not CONF.service_available.ironic:
- skip_msg = ('%s skipped as Ironic is not available' % cls.__name__)
- raise cls.skipException(skip_msg)
- cls.client = cls.os_adm.baremetal_nodes_client
- cls.ironic_client = cls.os_adm.baremetal_client
-
- @test.attr(type=['baremetal'])
- @test.idempotent_id('e475aa6e-416d-4fa4-b3af-28d5e84250fb')
- def test_list_get_baremetal_nodes(self):
- # Create some test nodes in Ironic directly
- test_nodes = []
- for i in range(0, 3):
- _, node = self.ironic_client.create_node()
- test_nodes.append(node)
- self.addCleanup(self.ironic_client.delete_node, node['uuid'])
-
- # List all baremetal nodes and ensure our created test nodes are
- # listed
- bm_node_ids = set([n['id'] for n in
- self.client.list_baremetal_nodes()['nodes']])
- test_node_ids = set([n['uuid'] for n in test_nodes])
- self.assertTrue(test_node_ids.issubset(bm_node_ids))
-
- # Test getting each individually
- for node in test_nodes:
- baremetal_node = self.client.show_baremetal_node(node['uuid'])
- self.assertEqual(node['uuid'], baremetal_node['node']['id'])
diff --git a/tempest/api/compute/admin/test_create_server.py b/tempest/api/compute/admin/test_create_server.py
new file mode 100644
index 0000000..711b441
--- /dev/null
+++ b/tempest/api/compute/admin/test_create_server.py
@@ -0,0 +1,133 @@
+# 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.
+
+import testtools
+
+from tempest.api.compute import base
+from tempest.common.utils.linux import remote_client
+from tempest.common import waiters
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+
+CONF = config.CONF
+
+
+class ServersWithSpecificFlavorTestJSON(base.BaseV2ComputeAdminTest):
+ @classmethod
+ def setup_credentials(cls):
+ cls.prepare_instance_network()
+ super(ServersWithSpecificFlavorTestJSON, cls).setup_credentials()
+
+ @classmethod
+ def setup_clients(cls):
+ super(ServersWithSpecificFlavorTestJSON, cls).setup_clients()
+ cls.client = cls.servers_client
+
+ @decorators.idempotent_id('b3c7bcfc-bb5b-4e22-b517-c7f686b802ca')
+ @testtools.skipUnless(CONF.validation.run_validation,
+ 'Instance validation tests are disabled.')
+ def test_verify_created_server_ephemeral_disk(self):
+ # Verify that the ephemeral disk is created when creating server
+ flavor_base = self.flavors_client.show_flavor(
+ self.flavor_ref)['flavor']
+
+ def create_flavor_with_ephemeral(ephem_disk):
+ name = 'flavor_with_ephemeral_%s' % ephem_disk
+ flavor_name = data_utils.rand_name(name)
+
+ ram = flavor_base['ram']
+ vcpus = flavor_base['vcpus']
+ disk = flavor_base['disk']
+
+ # Create a flavor with ephemeral disk
+ flavor = self.create_flavor(name=flavor_name, ram=ram, vcpus=vcpus,
+ disk=disk, ephemeral=ephem_disk)
+
+ # Set extra specs same as self.flavor_ref for the created flavor,
+ # because the environment may need some special extra specs to
+ # create server which should have been contained in
+ # self.flavor_ref.
+ extra_spec_keys = \
+ self.admin_flavors_client.list_flavor_extra_specs(
+ self.flavor_ref)['extra_specs']
+ if extra_spec_keys:
+ self.admin_flavors_client.set_flavor_extra_spec(
+ flavor['id'], **extra_spec_keys)
+
+ return flavor['id']
+
+ flavor_with_eph_disk_id = create_flavor_with_ephemeral(ephem_disk=1)
+ flavor_no_eph_disk_id = create_flavor_with_ephemeral(ephem_disk=0)
+
+ admin_pass = self.image_ssh_password
+
+ validation_resources = self.get_test_validation_resources(
+ self.os_primary)
+ server_no_eph_disk = self.create_test_server(
+ validatable=True,
+ validation_resources=validation_resources,
+ wait_until='ACTIVE',
+ adminPass=admin_pass,
+ flavor=flavor_no_eph_disk_id)
+
+ self.addCleanup(waiters.wait_for_server_termination,
+ self.servers_client, server_no_eph_disk['id'])
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.servers_client.delete_server,
+ server_no_eph_disk['id'])
+
+ # Get partition number of server without ephemeral disk.
+ server_no_eph_disk = self.client.show_server(
+ server_no_eph_disk['id'])['server']
+ linux_client = remote_client.RemoteClient(
+ self.get_server_ip(server_no_eph_disk,
+ validation_resources),
+ self.ssh_user,
+ admin_pass,
+ validation_resources['keypair']['private_key'],
+ server=server_no_eph_disk,
+ servers_client=self.client)
+ disks_num = len(linux_client.get_disks().split('\n'))
+
+ # Explicit server deletion necessary for Juno compatibility
+ self.client.delete_server(server_no_eph_disk['id'])
+
+ server_with_eph_disk = self.create_test_server(
+ validatable=True,
+ validation_resources=validation_resources,
+ wait_until='ACTIVE',
+ adminPass=admin_pass,
+ flavor=flavor_with_eph_disk_id)
+
+ self.addCleanup(waiters.wait_for_server_termination,
+ self.servers_client, server_with_eph_disk['id'])
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.servers_client.delete_server,
+ server_with_eph_disk['id'])
+
+ server_with_eph_disk = self.client.show_server(
+ server_with_eph_disk['id'])['server']
+ linux_client = remote_client.RemoteClient(
+ self.get_server_ip(server_with_eph_disk,
+ validation_resources),
+ self.ssh_user,
+ admin_pass,
+ validation_resources['keypair']['private_key'],
+ server=server_with_eph_disk,
+ servers_client=self.client)
+ disks_num_eph = len(linux_client.get_disks().split('\n'))
+ self.assertEqual(disks_num + 1, disks_num_eph)
diff --git a/tempest/api/compute/admin/test_delete_server.py b/tempest/api/compute/admin/test_delete_server.py
new file mode 100644
index 0000000..83444b9
--- /dev/null
+++ b/tempest/api/compute/admin/test_delete_server.py
@@ -0,0 +1,52 @@
+# 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 tempest.api.compute import base
+from tempest.common import waiters
+from tempest import config
+from tempest.lib import decorators
+
+CONF = config.CONF
+
+
+class DeleteServersAdminTestJSON(base.BaseV2ComputeAdminTest):
+ # NOTE: Server creations of each test class should be under 10
+ # for preventing "Quota exceeded for instances".
+
+ @classmethod
+ def setup_clients(cls):
+ super(DeleteServersAdminTestJSON, cls).setup_clients()
+ cls.non_admin_client = cls.servers_client
+ cls.admin_client = cls.os_admin.servers_client
+
+ @decorators.idempotent_id('99774678-e072-49d1-9d2a-49a59bc56063')
+ def test_delete_server_while_in_error_state(self):
+ # Delete a server while it's VM state is error
+ server = self.create_test_server(wait_until='ACTIVE')
+ self.admin_client.reset_state(server['id'], state='error')
+ # Verify server's state
+ server = self.non_admin_client.show_server(server['id'])['server']
+ self.assertEqual(server['status'], 'ERROR')
+ self.non_admin_client.delete_server(server['id'])
+ waiters.wait_for_server_termination(self.servers_client,
+ server['id'],
+ ignore_error=True)
+
+ @decorators.idempotent_id('73177903-6737-4f27-a60c-379e8ae8cf48')
+ def test_admin_delete_servers_of_others(self):
+ # Administrator can delete servers of others
+ server = self.create_test_server(wait_until='ACTIVE')
+ self.admin_client.delete_server(server['id'])
+ waiters.wait_for_server_termination(self.servers_client, server['id'])
diff --git a/tempest/api/compute/admin/test_fixed_ips.py b/tempest/api/compute/admin/test_fixed_ips.py
index 9cda0cd..66c2c2d 100644
--- a/tempest/api/compute/admin/test_fixed_ips.py
+++ b/tempest/api/compute/admin/test_fixed_ips.py
@@ -14,8 +14,9 @@
# under the License.
from tempest.api.compute import base
+from tempest.common import utils
from tempest import config
-from tempest import test
+from tempest.lib import decorators
CONF = config.CONF
@@ -28,17 +29,20 @@
if CONF.service_available.neutron:
msg = ("%s skipped as neutron is available" % cls.__name__)
raise cls.skipException(msg)
+ if not utils.get_service_list()['network']:
+ raise cls.skipException("network service not enabled.")
@classmethod
def setup_clients(cls):
super(FixedIPsTestJson, cls).setup_clients()
- cls.client = cls.os_adm.fixed_ips_client
+ cls.client = cls.os_admin.fixed_ips_client
@classmethod
def resource_setup(cls):
super(FixedIPsTestJson, cls).resource_setup()
server = cls.create_test_server(wait_until='ACTIVE')
server = cls.servers_client.show_server(server['id'])['server']
+ cls.ip = None
for ip_set in server['addresses']:
for ip in server['addresses'][ip_set]:
if ip['OS-EXT-IPS:type'] == 'fixed':
@@ -46,19 +50,19 @@
break
if cls.ip:
break
+ if cls.ip is None:
+ raise cls.skipException("No fixed ip found for server: %s"
+ % server['id'])
- @test.idempotent_id('16b7d848-2f7c-4709-85a3-2dfb4576cc52')
- @test.services('network')
+ @decorators.idempotent_id('16b7d848-2f7c-4709-85a3-2dfb4576cc52')
def test_list_fixed_ip_details(self):
fixed_ip = self.client.show_fixed_ip(self.ip)
self.assertEqual(fixed_ip['fixed_ip']['address'], self.ip)
- @test.idempotent_id('5485077b-7e46-4cec-b402-91dc3173433b')
- @test.services('network')
+ @decorators.idempotent_id('5485077b-7e46-4cec-b402-91dc3173433b')
def test_set_reserve(self):
self.client.reserve_fixed_ip(self.ip, reserve="None")
- @test.idempotent_id('7476e322-b9ff-4710-bf82-49d51bac6e2e')
- @test.services('network')
+ @decorators.idempotent_id('7476e322-b9ff-4710-bf82-49d51bac6e2e')
def test_set_unreserve(self):
self.client.reserve_fixed_ip(self.ip, unreserve="None")
diff --git a/tempest/api/compute/admin/test_fixed_ips_negative.py b/tempest/api/compute/admin/test_fixed_ips_negative.py
index 8d745c9..7d41f46 100644
--- a/tempest/api/compute/admin/test_fixed_ips_negative.py
+++ b/tempest/api/compute/admin/test_fixed_ips_negative.py
@@ -12,11 +12,11 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.compute import base
+from tempest.common import utils
from tempest import config
-from tempest import test
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
CONF = config.CONF
@@ -29,11 +29,13 @@
if CONF.service_available.neutron:
msg = ("%s skipped as neutron is available" % cls.__name__)
raise cls.skipException(msg)
+ if not utils.get_service_list()['network']:
+ raise cls.skipException("network service not enabled.")
@classmethod
def setup_clients(cls):
super(FixedIPsNegativeTestJson, cls).setup_clients()
- cls.client = cls.os_adm.fixed_ips_client
+ cls.client = cls.os_admin.fixed_ips_client
cls.non_admin_client = cls.fixed_ips_client
@classmethod
@@ -41,6 +43,7 @@
super(FixedIPsNegativeTestJson, cls).resource_setup()
server = cls.create_test_server(wait_until='ACTIVE')
server = cls.servers_client.show_server(server['id'])['server']
+ cls.ip = None
for ip_set in server['addresses']:
for ip in server['addresses'][ip_set]:
if ip['OS-EXT-IPS:type'] == 'fixed':
@@ -48,33 +51,32 @@
break
if cls.ip:
break
+ if cls.ip is None:
+ raise cls.skipException("No fixed ip found for server: %s"
+ % server['id'])
- @test.attr(type=['negative'])
- @test.idempotent_id('9f17f47d-daad-4adc-986e-12370c93e407')
- @test.services('network')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('9f17f47d-daad-4adc-986e-12370c93e407')
def test_list_fixed_ip_details_with_non_admin_user(self):
self.assertRaises(lib_exc.Forbidden,
self.non_admin_client.show_fixed_ip, self.ip)
- @test.attr(type=['negative'])
- @test.idempotent_id('ce60042c-fa60-4836-8d43-1c8e3359dc47')
- @test.services('network')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('ce60042c-fa60-4836-8d43-1c8e3359dc47')
def test_set_reserve_with_non_admin_user(self):
self.assertRaises(lib_exc.Forbidden,
self.non_admin_client.reserve_fixed_ip,
self.ip, reserve="None")
- @test.attr(type=['negative'])
- @test.idempotent_id('f1f7a35b-0390-48c5-9803-5f27461439db')
- @test.services('network')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('f1f7a35b-0390-48c5-9803-5f27461439db')
def test_set_unreserve_with_non_admin_user(self):
self.assertRaises(lib_exc.Forbidden,
self.non_admin_client.reserve_fixed_ip,
self.ip, unreserve="None")
- @test.attr(type=['negative'])
- @test.idempotent_id('f51cf464-7fc5-4352-bc3e-e75cfa2cb717')
- @test.services('network')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('f51cf464-7fc5-4352-bc3e-e75cfa2cb717')
def test_set_reserve_with_invalid_ip(self):
# NOTE(maurosr): since this exercises the same code snippet, we do it
# only for reserve action
@@ -85,9 +87,8 @@
self.client.reserve_fixed_ip,
"my.invalid.ip", reserve="None")
- @test.attr(type=['negative'])
- @test.idempotent_id('fd26ef50-f135-4232-9d32-281aab3f9176')
- @test.services('network')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('fd26ef50-f135-4232-9d32-281aab3f9176')
def test_fixed_ip_with_invalid_action(self):
self.assertRaises(lib_exc.BadRequest,
self.client.reserve_fixed_ip,
diff --git a/tempest/api/compute/admin/test_flavors.py b/tempest/api/compute/admin/test_flavors.py
index 1ef8f67..1483c2e 100644
--- a/tempest/api/compute/admin/test_flavors.py
+++ b/tempest/api/compute/admin/test_flavors.py
@@ -15,11 +15,11 @@
import uuid
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.compute import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.common import utils
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class FlavorsAdminTestJSON(base.BaseV2ComputeAdminTest):
@@ -28,17 +28,11 @@
@classmethod
def skip_checks(cls):
super(FlavorsAdminTestJSON, cls).skip_checks()
- if not test.is_extension_enabled('OS-FLV-EXT-DATA', 'compute'):
+ if not utils.is_extension_enabled('OS-FLV-EXT-DATA', 'compute'):
msg = "OS-FLV-EXT-DATA extension not enabled."
raise cls.skipException(msg)
@classmethod
- def setup_clients(cls):
- super(FlavorsAdminTestJSON, cls).setup_clients()
- cls.client = cls.os_adm.flavors_client
- cls.user_client = cls.os.flavors_client
-
- @classmethod
def resource_setup(cls):
super(FlavorsAdminTestJSON, cls).resource_setup()
@@ -50,85 +44,55 @@
cls.swap = 1024
cls.rxtx = 2
- def flavor_clean_up(self, flavor_id):
- self.client.delete_flavor(flavor_id)
- self.client.wait_for_resource_deletion(flavor_id)
-
- def _create_flavor(self, flavor_id):
- # Create a flavor and ensure it is listed
- # This operation requires the user to have 'admin' role
- flavor_name = data_utils.rand_name(self.flavor_name_prefix)
-
- # Create the flavor
- flavor = self.client.create_flavor(name=flavor_name,
- ram=self.ram, vcpus=self.vcpus,
- disk=self.disk,
- id=flavor_id,
- ephemeral=self.ephemeral,
- swap=self.swap,
- rxtx_factor=self.rxtx)['flavor']
- self.addCleanup(self.flavor_clean_up, flavor['id'])
- self.assertEqual(flavor['name'], flavor_name)
- self.assertEqual(flavor['vcpus'], self.vcpus)
- self.assertEqual(flavor['disk'], self.disk)
- self.assertEqual(flavor['ram'], self.ram)
- self.assertEqual(flavor['swap'], self.swap)
- self.assertEqual(flavor['rxtx_factor'], self.rxtx)
- self.assertEqual(flavor['OS-FLV-EXT-DATA:ephemeral'],
- self.ephemeral)
- self.assertEqual(flavor['os-flavor-access:is_public'], True)
-
- # Verify flavor is retrieved
- flavor = self.client.show_flavor(flavor['id'])['flavor']
- self.assertEqual(flavor['name'], flavor_name)
-
- return flavor['id']
-
- @test.idempotent_id('8b4330e1-12c4-4554-9390-e6639971f086')
+ @decorators.idempotent_id('8b4330e1-12c4-4554-9390-e6639971f086')
def test_create_flavor_with_int_id(self):
flavor_id = data_utils.rand_int_id(start=1000)
- new_flavor_id = self._create_flavor(flavor_id)
+ new_flavor_id = self.create_flavor(ram=self.ram,
+ vcpus=self.vcpus,
+ disk=self.disk,
+ id=flavor_id)['id']
self.assertEqual(new_flavor_id, str(flavor_id))
- @test.idempotent_id('94c9bb4e-2c2a-4f3c-bb1f-5f0daf918e6d')
+ @decorators.idempotent_id('94c9bb4e-2c2a-4f3c-bb1f-5f0daf918e6d')
def test_create_flavor_with_uuid_id(self):
- flavor_id = str(uuid.uuid4())
- new_flavor_id = self._create_flavor(flavor_id)
+ flavor_id = data_utils.rand_uuid()
+ new_flavor_id = self.create_flavor(ram=self.ram,
+ vcpus=self.vcpus,
+ disk=self.disk,
+ id=flavor_id)['id']
self.assertEqual(new_flavor_id, flavor_id)
- @test.idempotent_id('f83fe669-6758-448a-a85e-32d351f36fe0')
+ @decorators.idempotent_id('f83fe669-6758-448a-a85e-32d351f36fe0')
def test_create_flavor_with_none_id(self):
# If nova receives a request with None as flavor_id,
# nova generates flavor_id of uuid.
flavor_id = None
- new_flavor_id = self._create_flavor(flavor_id)
+ new_flavor_id = self.create_flavor(ram=self.ram,
+ vcpus=self.vcpus,
+ disk=self.disk,
+ id=flavor_id)['id']
self.assertEqual(new_flavor_id, str(uuid.UUID(new_flavor_id)))
- @test.idempotent_id('8261d7b0-be58-43ec-a2e5-300573c3f6c5')
+ @decorators.idempotent_id('8261d7b0-be58-43ec-a2e5-300573c3f6c5')
def test_create_flavor_verify_entry_in_list_details(self):
# Create a flavor and ensure it's details are listed
# This operation requires the user to have 'admin' role
flavor_name = data_utils.rand_name(self.flavor_name_prefix)
- new_flavor_id = data_utils.rand_int_id(start=1000)
# Create the flavor
- flavor = self.client.create_flavor(name=flavor_name,
- ram=self.ram, vcpus=self.vcpus,
- disk=self.disk,
- id=new_flavor_id,
- ephemeral=self.ephemeral,
- swap=self.swap,
- rxtx_factor=self.rxtx)['flavor']
- self.addCleanup(self.flavor_clean_up, flavor['id'])
- flag = False
- # Verify flavor is retrieved
- flavors = self.client.list_flavors(detail=True)['flavors']
- for flavor in flavors:
- if flavor['name'] == flavor_name:
- flag = True
- self.assertTrue(flag)
+ self.create_flavor(name=flavor_name,
+ ram=self.ram, vcpus=self.vcpus,
+ disk=self.disk,
+ ephemeral=self.ephemeral,
+ swap=self.swap,
+ rxtx_factor=self.rxtx)
- @test.idempotent_id('63dc64e6-2e79-4fdf-868f-85500d308d66')
+ # Check if flavor is present in list
+ flavors_list = self.admin_flavors_client.list_flavors(
+ detail=True)['flavors']
+ self.assertIn(flavor_name, [f['name'] for f in flavors_list])
+
+ @decorators.idempotent_id('63dc64e6-2e79-4fdf-868f-85500d308d66')
def test_create_list_flavor_without_extra_data(self):
# Create a flavor and ensure it is listed
# This operation requires the user to have 'admin' role
@@ -137,18 +101,17 @@
# check some extensions for the flavor create/show/detail response
self.assertEqual(flavor['swap'], '')
self.assertEqual(int(flavor['rxtx_factor']), 1)
- self.assertEqual(int(flavor['OS-FLV-EXT-DATA:ephemeral']), 0)
+ self.assertEqual(flavor['OS-FLV-EXT-DATA:ephemeral'], 0)
self.assertEqual(flavor['os-flavor-access:is_public'], True)
flavor_name = data_utils.rand_name(self.flavor_name_prefix)
new_flavor_id = data_utils.rand_int_id(start=1000)
# Create the flavor
- flavor = self.client.create_flavor(name=flavor_name,
- ram=self.ram, vcpus=self.vcpus,
- disk=self.disk,
- id=new_flavor_id)['flavor']
- self.addCleanup(self.flavor_clean_up, flavor['id'])
+ flavor = self.create_flavor(name=flavor_name,
+ ram=self.ram, vcpus=self.vcpus,
+ disk=self.disk,
+ id=new_flavor_id)
self.assertEqual(flavor['name'], flavor_name)
self.assertEqual(flavor['ram'], self.ram)
self.assertEqual(flavor['vcpus'], self.vcpus)
@@ -157,129 +120,92 @@
verify_flavor_response_extension(flavor)
# Verify flavor is retrieved
- flavor = self.client.show_flavor(new_flavor_id)['flavor']
+ flavor = self.admin_flavors_client.show_flavor(new_flavor_id)['flavor']
self.assertEqual(flavor['name'], flavor_name)
verify_flavor_response_extension(flavor)
# Check if flavor is present in list
- flavors = self.user_client.list_flavors(detail=True)['flavors']
- for flavor in flavors:
- if flavor['name'] == flavor_name:
- verify_flavor_response_extension(flavor)
- flag = True
- self.assertTrue(flag)
+ flavors_list = [
+ f for f in self.flavors_client.list_flavors(detail=True)['flavors']
+ if f['name'] == flavor_name
+ ]
+ self.assertNotEmpty(flavors_list)
+ verify_flavor_response_extension(flavors_list[0])
- @test.idempotent_id('be6cc18c-7c5d-48c0-ac16-17eaf03c54eb')
+ @decorators.idempotent_id('be6cc18c-7c5d-48c0-ac16-17eaf03c54eb')
def test_list_non_public_flavor(self):
# Create a flavor with os-flavor-access:is_public false.
# The flavor should not be present in list_details as the
# tenant is not automatically added access list.
# This operation requires the user to have 'admin' role
flavor_name = data_utils.rand_name(self.flavor_name_prefix)
- new_flavor_id = data_utils.rand_int_id(start=1000)
# Create the flavor
- flavor = self.client.create_flavor(name=flavor_name,
- ram=self.ram, vcpus=self.vcpus,
- disk=self.disk,
- id=new_flavor_id,
- is_public="False")['flavor']
- self.addCleanup(self.flavor_clean_up, flavor['id'])
- # Verify flavor is retrieved
- flag = False
- flavors = self.client.list_flavors(detail=True)['flavors']
- for flavor in flavors:
- if flavor['name'] == flavor_name:
- flag = True
- self.assertFalse(flag)
+ self.create_flavor(name=flavor_name,
+ ram=self.ram, vcpus=self.vcpus,
+ disk=self.disk,
+ is_public="False")
+ # Verify flavor is not retrieved
+ flavors_list = self.admin_flavors_client.list_flavors(
+ detail=True)['flavors']
+ self.assertNotIn(flavor_name, [f['name'] for f in flavors_list])
# Verify flavor is not retrieved with other user
- flag = False
- flavors = self.user_client.list_flavors(detail=True)['flavors']
- for flavor in flavors:
- if flavor['name'] == flavor_name:
- flag = True
- self.assertFalse(flag)
+ flavors_list = self.flavors_client.list_flavors(detail=True)['flavors']
+ self.assertNotIn(flavor_name, [f['name'] for f in flavors_list])
- @test.idempotent_id('bcc418ef-799b-47cc-baa1-ce01368b8987')
+ @decorators.idempotent_id('bcc418ef-799b-47cc-baa1-ce01368b8987')
def test_create_server_with_non_public_flavor(self):
# Create a flavor with os-flavor-access:is_public false
- flavor_name = data_utils.rand_name(self.flavor_name_prefix)
- new_flavor_id = data_utils.rand_int_id(start=1000)
-
- # Create the flavor
- flavor = self.client.create_flavor(name=flavor_name,
- ram=self.ram, vcpus=self.vcpus,
- disk=self.disk,
- id=new_flavor_id,
- is_public="False")['flavor']
- self.addCleanup(self.flavor_clean_up, flavor['id'])
+ flavor = self.create_flavor(ram=self.ram, vcpus=self.vcpus,
+ disk=self.disk,
+ is_public="False")
# Verify flavor is not used by other user
self.assertRaises(lib_exc.BadRequest,
- self.os.servers_client.create_server,
+ self.os_primary.servers_client.create_server,
name='test', imageRef=self.image_ref,
flavorRef=flavor['id'])
- @test.idempotent_id('b345b196-bfbd-4231-8ac1-6d7fe15ff3a3')
+ @decorators.idempotent_id('b345b196-bfbd-4231-8ac1-6d7fe15ff3a3')
def test_list_public_flavor_with_other_user(self):
# Create a Flavor with public access.
# Try to List/Get flavor with another user
flavor_name = data_utils.rand_name(self.flavor_name_prefix)
- new_flavor_id = data_utils.rand_int_id(start=1000)
# Create the flavor
- flavor = self.client.create_flavor(name=flavor_name,
- ram=self.ram, vcpus=self.vcpus,
- disk=self.disk,
- id=new_flavor_id,
- is_public="True")['flavor']
- self.addCleanup(self.flavor_clean_up, flavor['id'])
- flag = False
- self.new_client = self.flavors_client
+ self.create_flavor(name=flavor_name,
+ ram=self.ram, vcpus=self.vcpus,
+ disk=self.disk,
+ is_public="True")
# Verify flavor is retrieved with new user
- flavors = self.new_client.list_flavors(detail=True)['flavors']
- for flavor in flavors:
- if flavor['name'] == flavor_name:
- flag = True
- self.assertTrue(flag)
+ flavors_list = self.flavors_client.list_flavors(detail=True)['flavors']
+ self.assertIn(flavor_name, [f['name'] for f in flavors_list])
- @test.idempotent_id('fb9cbde6-3a0e-41f2-a983-bdb0a823c44e')
+ @decorators.idempotent_id('fb9cbde6-3a0e-41f2-a983-bdb0a823c44e')
def test_is_public_string_variations(self):
- flavor_id_not_public = data_utils.rand_int_id(start=1000)
flavor_name_not_public = data_utils.rand_name(self.flavor_name_prefix)
- flavor_id_public = data_utils.rand_int_id(start=1000)
flavor_name_public = data_utils.rand_name(self.flavor_name_prefix)
# Create a non public flavor
- flavor = self.client.create_flavor(name=flavor_name_not_public,
- ram=self.ram, vcpus=self.vcpus,
- disk=self.disk,
- id=flavor_id_not_public,
- is_public="False")['flavor']
- self.addCleanup(self.flavor_clean_up, flavor['id'])
+ self.create_flavor(name=flavor_name_not_public,
+ ram=self.ram, vcpus=self.vcpus,
+ disk=self.disk,
+ is_public="False")
# Create a public flavor
- flavor = self.client.create_flavor(name=flavor_name_public,
- ram=self.ram, vcpus=self.vcpus,
- disk=self.disk,
- id=flavor_id_public,
- is_public="True")['flavor']
- self.addCleanup(self.flavor_clean_up, flavor['id'])
-
- def _flavor_lookup(flavors, flavor_name):
- for flavor in flavors:
- if flavor['name'] == flavor_name:
- return flavor
- return None
+ self.create_flavor(name=flavor_name_public,
+ ram=self.ram, vcpus=self.vcpus,
+ disk=self.disk,
+ is_public="True")
def _test_string_variations(variations, flavor_name):
for string in variations:
params = {'is_public': string}
- flavors = (self.client.list_flavors(detail=True, **params)
+ flavors = (self.admin_flavors_client.list_flavors(detail=True,
+ **params)
['flavors'])
- flavor = _flavor_lookup(flavors, flavor_name)
- self.assertIsNotNone(flavor)
+ self.assertIn(flavor_name, [f['name'] for f in flavors])
_test_string_variations(['f', 'false', 'no', '0'],
flavor_name_not_public)
@@ -287,19 +213,13 @@
_test_string_variations(['t', 'true', 'yes', '1'],
flavor_name_public)
- @test.idempotent_id('3b541a2e-2ac2-4b42-8b8d-ba6e22fcd4da')
+ @decorators.idempotent_id('3b541a2e-2ac2-4b42-8b8d-ba6e22fcd4da')
def test_create_flavor_using_string_ram(self):
- flavor_name = data_utils.rand_name(self.flavor_name_prefix)
new_flavor_id = data_utils.rand_int_id(start=1000)
ram = "1024"
- flavor = self.client.create_flavor(name=flavor_name,
- ram=ram, vcpus=self.vcpus,
- disk=self.disk,
- id=new_flavor_id)['flavor']
- self.addCleanup(self.flavor_clean_up, flavor['id'])
- self.assertEqual(flavor['name'], flavor_name)
- self.assertEqual(flavor['vcpus'], self.vcpus)
- self.assertEqual(flavor['disk'], self.disk)
+ flavor = self.create_flavor(ram=ram, vcpus=self.vcpus,
+ disk=self.disk,
+ id=new_flavor_id)
self.assertEqual(flavor['ram'], int(ram))
self.assertEqual(int(flavor['id']), new_flavor_id)
diff --git a/tempest/api/compute/admin/test_flavors_access.py b/tempest/api/compute/admin/test_flavors_access.py
index 2063267..b8e2b42 100644
--- a/tempest/api/compute/admin/test_flavors_access.py
+++ b/tempest/api/compute/admin/test_flavors_access.py
@@ -14,8 +14,8 @@
# under the License.
from tempest.api.compute import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.common import utils
+from tempest.lib import decorators
class FlavorsAccessTestJSON(base.BaseV2ComputeAdminTest):
@@ -27,73 +27,55 @@
@classmethod
def skip_checks(cls):
super(FlavorsAccessTestJSON, cls).skip_checks()
- if not test.is_extension_enabled('OS-FLV-EXT-DATA', 'compute'):
+ if not utils.is_extension_enabled('OS-FLV-EXT-DATA', 'compute'):
msg = "OS-FLV-EXT-DATA extension not enabled."
raise cls.skipException(msg)
@classmethod
- def setup_clients(cls):
- super(FlavorsAccessTestJSON, cls).setup_clients()
- cls.client = cls.os_adm.flavors_client
-
- @classmethod
def resource_setup(cls):
super(FlavorsAccessTestJSON, cls).resource_setup()
# Non admin tenant ID
cls.tenant_id = cls.flavors_client.tenant_id
- cls.flavor_name_prefix = 'test_flavor_access_'
cls.ram = 512
cls.vcpus = 1
cls.disk = 10
- @test.idempotent_id('ea2c2211-29fa-4db9-97c3-906d36fad3e0')
+ @decorators.idempotent_id('ea2c2211-29fa-4db9-97c3-906d36fad3e0')
def test_flavor_access_list_with_private_flavor(self):
# Test to make sure that list flavor access on a newly created
# private flavor will return an empty access list
- flavor_name = data_utils.rand_name(self.flavor_name_prefix)
- new_flavor_id = data_utils.rand_int_id(start=1000)
- new_flavor = self.client.create_flavor(name=flavor_name,
- ram=self.ram, vcpus=self.vcpus,
- disk=self.disk,
- id=new_flavor_id,
- is_public='False')['flavor']
- self.addCleanup(self.client.delete_flavor, new_flavor['id'])
- flavor_access = (self.client.list_flavor_access(new_flavor_id)
- ['flavor_access'])
- self.assertEqual(len(flavor_access), 0, str(flavor_access))
+ flavor = self.create_flavor(ram=self.ram, vcpus=self.vcpus,
+ disk=self.disk, is_public='False')
- @test.idempotent_id('59e622f6-bdf6-45e3-8ba8-fedad905a6b4')
+ flavor_access = (self.admin_flavors_client.list_flavor_access(
+ flavor['id'])['flavor_access'])
+ self.assertEmpty(flavor_access)
+
+ @decorators.idempotent_id('59e622f6-bdf6-45e3-8ba8-fedad905a6b4')
def test_flavor_access_add_remove(self):
# Test to add and remove flavor access to a given tenant.
- flavor_name = data_utils.rand_name(self.flavor_name_prefix)
- new_flavor_id = data_utils.rand_int_id(start=1000)
- new_flavor = self.client.create_flavor(name=flavor_name,
- ram=self.ram, vcpus=self.vcpus,
- disk=self.disk,
- id=new_flavor_id,
- is_public='False')['flavor']
- self.addCleanup(self.client.delete_flavor, new_flavor['id'])
+ flavor = self.create_flavor(ram=self.ram, vcpus=self.vcpus,
+ disk=self.disk, is_public='False')
+
# Add flavor access to a tenant.
resp_body = {
"tenant_id": str(self.tenant_id),
- "flavor_id": str(new_flavor['id']),
+ "flavor_id": str(flavor['id']),
}
- add_body = (self.client.add_flavor_access(new_flavor['id'],
- self.tenant_id)
- ['flavor_access'])
+ add_body = (self.admin_flavors_client.add_flavor_access(
+ flavor['id'], self.tenant_id)['flavor_access'])
self.assertIn(resp_body, add_body)
# The flavor is present in list.
flavors = self.flavors_client.list_flavors(detail=True)['flavors']
- self.assertIn(new_flavor['id'], map(lambda x: x['id'], flavors))
+ self.assertIn(flavor['id'], map(lambda x: x['id'], flavors))
# Remove flavor access from a tenant.
- remove_body = (self.client.remove_flavor_access(new_flavor['id'],
- self.tenant_id)
- ['flavor_access'])
+ remove_body = (self.admin_flavors_client.remove_flavor_access(
+ flavor['id'], self.tenant_id)['flavor_access'])
self.assertNotIn(resp_body, remove_body)
# The flavor is not present in list.
flavors = self.flavors_client.list_flavors(detail=True)['flavors']
- self.assertNotIn(new_flavor['id'], map(lambda x: x['id'], flavors))
+ self.assertNotIn(flavor['id'], map(lambda x: x['id'], flavors))
diff --git a/tempest/api/compute/admin/test_flavors_access_negative.py b/tempest/api/compute/admin/test_flavors_access_negative.py
index 5070fd7..45ca10a 100644
--- a/tempest/api/compute/admin/test_flavors_access_negative.py
+++ b/tempest/api/compute/admin/test_flavors_access_negative.py
@@ -13,13 +13,10 @@
# License for the specific language governing permissions and limitations
# under the License.
-import uuid
-
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.compute import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.common import utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class FlavorsAccessNegativeTestJSON(base.BaseV2ComputeAdminTest):
@@ -28,122 +25,91 @@
Add and remove Flavor Access require admin privileges.
"""
+ credentials = ['primary', 'admin', 'alt']
+
@classmethod
def skip_checks(cls):
super(FlavorsAccessNegativeTestJSON, cls).skip_checks()
- if not test.is_extension_enabled('OS-FLV-EXT-DATA', 'compute'):
+ if not utils.is_extension_enabled('OS-FLV-EXT-DATA', 'compute'):
msg = "OS-FLV-EXT-DATA extension not enabled."
raise cls.skipException(msg)
@classmethod
- def setup_clients(cls):
- super(FlavorsAccessNegativeTestJSON, cls).setup_clients()
- cls.client = cls.os_adm.flavors_client
-
- @classmethod
def resource_setup(cls):
super(FlavorsAccessNegativeTestJSON, cls).resource_setup()
cls.tenant_id = cls.flavors_client.tenant_id
- cls.flavor_name_prefix = 'test_flavor_access_'
cls.ram = 512
cls.vcpus = 1
cls.disk = 10
- @test.attr(type=['negative'])
- @test.idempotent_id('0621c53e-d45d-40e7-951d-43e5e257b272')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('0621c53e-d45d-40e7-951d-43e5e257b272')
def test_flavor_access_list_with_public_flavor(self):
# Test to list flavor access with exceptions by querying public flavor
- flavor_name = data_utils.rand_name(self.flavor_name_prefix)
- new_flavor_id = data_utils.rand_int_id(start=1000)
- new_flavor = self.client.create_flavor(name=flavor_name,
- ram=self.ram, vcpus=self.vcpus,
- disk=self.disk,
- id=new_flavor_id,
- is_public='True')['flavor']
- self.addCleanup(self.client.delete_flavor, new_flavor['id'])
+ flavor = self.create_flavor(ram=self.ram, vcpus=self.vcpus,
+ disk=self.disk, is_public='True')
self.assertRaises(lib_exc.NotFound,
- self.client.list_flavor_access,
- new_flavor_id)
+ self.admin_flavors_client.list_flavor_access,
+ flavor['id'])
- @test.attr(type=['negative'])
- @test.idempotent_id('41eaaade-6d37-4f28-9c74-f21b46ca67bd')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('41eaaade-6d37-4f28-9c74-f21b46ca67bd')
def test_flavor_non_admin_add(self):
# Test to add flavor access as a user without admin privileges.
- flavor_name = data_utils.rand_name(self.flavor_name_prefix)
- new_flavor_id = data_utils.rand_int_id(start=1000)
- new_flavor = self.client.create_flavor(name=flavor_name,
- ram=self.ram, vcpus=self.vcpus,
- disk=self.disk,
- id=new_flavor_id,
- is_public='False')['flavor']
- self.addCleanup(self.client.delete_flavor, new_flavor['id'])
+ flavor = self.create_flavor(ram=self.ram, vcpus=self.vcpus,
+ disk=self.disk, is_public='False')
self.assertRaises(lib_exc.Forbidden,
self.flavors_client.add_flavor_access,
- new_flavor['id'],
+ flavor['id'],
self.tenant_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('073e79a6-c311-4525-82dc-6083d919cb3a')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('073e79a6-c311-4525-82dc-6083d919cb3a')
def test_flavor_non_admin_remove(self):
# Test to remove flavor access as a user without admin privileges.
- flavor_name = data_utils.rand_name(self.flavor_name_prefix)
- new_flavor_id = data_utils.rand_int_id(start=1000)
- new_flavor = self.client.create_flavor(name=flavor_name,
- ram=self.ram, vcpus=self.vcpus,
- disk=self.disk,
- id=new_flavor_id,
- is_public='False')['flavor']
- self.addCleanup(self.client.delete_flavor, new_flavor['id'])
+ flavor = self.create_flavor(ram=self.ram, vcpus=self.vcpus,
+ disk=self.disk, is_public='False')
+
# Add flavor access to a tenant.
- self.client.add_flavor_access(new_flavor['id'], self.tenant_id)
- self.addCleanup(self.client.remove_flavor_access,
- new_flavor['id'], self.tenant_id)
+ self.admin_flavors_client.add_flavor_access(flavor['id'],
+ self.tenant_id)
+ self.addCleanup(self.admin_flavors_client.remove_flavor_access,
+ flavor['id'], self.tenant_id)
self.assertRaises(lib_exc.Forbidden,
self.flavors_client.remove_flavor_access,
- new_flavor['id'],
+ flavor['id'],
self.tenant_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('f3592cc0-0306-483c-b210-9a7b5346eddc')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('f3592cc0-0306-483c-b210-9a7b5346eddc')
def test_add_flavor_access_duplicate(self):
# Create a new flavor.
- flavor_name = data_utils.rand_name(self.flavor_name_prefix)
- new_flavor_id = data_utils.rand_int_id(start=1000)
- new_flavor = self.client.create_flavor(name=flavor_name,
- ram=self.ram, vcpus=self.vcpus,
- disk=self.disk,
- id=new_flavor_id,
- is_public='False')['flavor']
- self.addCleanup(self.client.delete_flavor, new_flavor['id'])
+ flavor = self.create_flavor(ram=self.ram, vcpus=self.vcpus,
+ disk=self.disk, is_public='False')
# Add flavor access to a tenant.
- self.client.add_flavor_access(new_flavor['id'], self.tenant_id)
- self.addCleanup(self.client.remove_flavor_access,
- new_flavor['id'], self.tenant_id)
+ self.admin_flavors_client.add_flavor_access(flavor['id'],
+ self.tenant_id)
+ self.addCleanup(self.admin_flavors_client.remove_flavor_access,
+ flavor['id'], self.tenant_id)
# An exception should be raised when adding flavor access to the same
# tenant
self.assertRaises(lib_exc.Conflict,
- self.client.add_flavor_access,
- new_flavor['id'],
+ self.admin_flavors_client.add_flavor_access,
+ flavor['id'],
self.tenant_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('1f710927-3bc7-4381-9f82-0ca6e42644b7')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('1f710927-3bc7-4381-9f82-0ca6e42644b7')
def test_remove_flavor_access_not_found(self):
# Create a new flavor.
- flavor_name = data_utils.rand_name(self.flavor_name_prefix)
- new_flavor_id = data_utils.rand_int_id(start=1000)
- new_flavor = self.client.create_flavor(name=flavor_name,
- ram=self.ram, vcpus=self.vcpus,
- disk=self.disk,
- id=new_flavor_id,
- is_public='False')['flavor']
- self.addCleanup(self.client.delete_flavor, new_flavor['id'])
+ flavor = self.create_flavor(ram=self.ram, vcpus=self.vcpus,
+ disk=self.disk, is_public='False')
# An exception should be raised when flavor access is not found
self.assertRaises(lib_exc.NotFound,
- self.client.remove_flavor_access,
- new_flavor['id'],
- str(uuid.uuid4()))
+ self.admin_flavors_client.remove_flavor_access,
+ flavor['id'],
+ self.os_alt.servers_client.tenant_id)
diff --git a/tempest/api/compute/admin/test_flavors_extra_specs.py b/tempest/api/compute/admin/test_flavors_extra_specs.py
index 661cd18..4d27a22 100644
--- a/tempest/api/compute/admin/test_flavors_extra_specs.py
+++ b/tempest/api/compute/admin/test_flavors_extra_specs.py
@@ -14,8 +14,9 @@
# under the License.
from tempest.api.compute import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.common import utils
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
class FlavorsExtraSpecsTestJSON(base.BaseV2ComputeAdminTest):
@@ -28,16 +29,11 @@
@classmethod
def skip_checks(cls):
super(FlavorsExtraSpecsTestJSON, cls).skip_checks()
- if not test.is_extension_enabled('OS-FLV-EXT-DATA', 'compute'):
+ if not utils.is_extension_enabled('OS-FLV-EXT-DATA', 'compute'):
msg = "OS-FLV-EXT-DATA extension not enabled."
raise cls.skipException(msg)
@classmethod
- def setup_clients(cls):
- super(FlavorsExtraSpecsTestJSON, cls).setup_clients()
- cls.client = cls.os_adm.flavors_client
-
- @classmethod
def resource_setup(cls):
super(FlavorsExtraSpecsTestJSON, cls).resource_setup()
flavor_name = data_utils.rand_name('test_flavor')
@@ -45,71 +41,75 @@
vcpus = 1
disk = 10
ephemeral = 10
- cls.new_flavor_id = data_utils.rand_int_id(start=1000)
+ new_flavor_id = data_utils.rand_int_id(start=1000)
swap = 1024
rxtx = 1
# Create a flavor so as to set/get/unset extra specs
- cls.flavor = cls.client.create_flavor(name=flavor_name,
- ram=ram, vcpus=vcpus,
- disk=disk,
- id=cls.new_flavor_id,
- ephemeral=ephemeral,
- swap=swap,
- rxtx_factor=rxtx)['flavor']
+ cls.flavor = cls.admin_flavors_client.create_flavor(
+ name=flavor_name,
+ ram=ram, vcpus=vcpus,
+ disk=disk,
+ id=new_flavor_id,
+ ephemeral=ephemeral,
+ swap=swap,
+ rxtx_factor=rxtx)['flavor']
+ cls.addClassResourceCleanup(
+ cls.admin_flavors_client.wait_for_resource_deletion,
+ cls.flavor['id'])
+ cls.addClassResourceCleanup(cls.admin_flavors_client.delete_flavor,
+ cls.flavor['id'])
- @classmethod
- def resource_cleanup(cls):
- cls.client.delete_flavor(cls.flavor['id'])
- cls.client.wait_for_resource_deletion(cls.flavor['id'])
- super(FlavorsExtraSpecsTestJSON, cls).resource_cleanup()
-
- @test.idempotent_id('0b2f9d4b-1ca2-4b99-bb40-165d4bb94208')
+ @decorators.idempotent_id('0b2f9d4b-1ca2-4b99-bb40-165d4bb94208')
def test_flavor_set_get_update_show_unset_keys(self):
# Test to SET, GET, UPDATE, SHOW, UNSET flavor extra
# spec as a user with admin privileges.
# Assigning extra specs values that are to be set
specs = {"key1": "value1", "key2": "value2"}
# SET extra specs to the flavor created in setUp
- set_body = self.client.set_flavor_extra_spec(self.flavor['id'],
- **specs)['extra_specs']
+ set_body = self.admin_flavors_client.set_flavor_extra_spec(
+ self.flavor['id'], **specs)['extra_specs']
self.assertEqual(set_body, specs)
# GET extra specs and verify
- get_body = (self.client.list_flavor_extra_specs(self.flavor['id'])
- ['extra_specs'])
+ get_body = (self.admin_flavors_client.list_flavor_extra_specs(
+ self.flavor['id'])['extra_specs'])
self.assertEqual(get_body, specs)
# UPDATE the value of the extra specs key1
update_body = \
- self.client.update_flavor_extra_spec(self.flavor['id'],
- "key1",
- key1="value")
+ self.admin_flavors_client.update_flavor_extra_spec(
+ self.flavor['id'], "key1", key1="value")
self.assertEqual({"key1": "value"}, update_body)
# GET extra specs and verify the value of the key2
# is the same as before
- get_body = (self.client.list_flavor_extra_specs(self.flavor['id'])
- ['extra_specs'])
+ get_body = self.admin_flavors_client.list_flavor_extra_specs(
+ self.flavor['id'])['extra_specs']
self.assertEqual(get_body, {"key1": "value", "key2": "value2"})
# UNSET extra specs that were set in this test
- self.client.unset_flavor_extra_spec(self.flavor['id'], "key1")
- self.client.unset_flavor_extra_spec(self.flavor['id'], "key2")
+ self.admin_flavors_client.unset_flavor_extra_spec(self.flavor['id'],
+ "key1")
+ self.admin_flavors_client.unset_flavor_extra_spec(self.flavor['id'],
+ "key2")
+ get_body = self.admin_flavors_client.list_flavor_extra_specs(
+ self.flavor['id'])['extra_specs']
+ self.assertEmpty(get_body)
- @test.idempotent_id('a99dad88-ae1c-4fba-aeb4-32f898218bd0')
+ @decorators.idempotent_id('a99dad88-ae1c-4fba-aeb4-32f898218bd0')
def test_flavor_non_admin_get_all_keys(self):
specs = {"key1": "value1", "key2": "value2"}
- self.client.set_flavor_extra_spec(self.flavor['id'], **specs)
- body = (self.flavors_client.list_flavor_extra_specs(self.flavor['id'])
- ['extra_specs'])
+ self.admin_flavors_client.set_flavor_extra_spec(self.flavor['id'],
+ **specs)
+ body = (self.flavors_client.list_flavor_extra_specs(
+ self.flavor['id'])['extra_specs'])
for key in specs:
self.assertEqual(body[key], specs[key])
- @test.idempotent_id('12805a7f-39a3-4042-b989-701d5cad9c90')
+ @decorators.idempotent_id('12805a7f-39a3-4042-b989-701d5cad9c90')
def test_flavor_non_admin_get_specific_key(self):
- body = self.client.set_flavor_extra_spec(self.flavor['id'],
- key1="value1",
- key2="value2")['extra_specs']
+ body = self.admin_flavors_client.set_flavor_extra_spec(
+ self.flavor['id'], key1="value1", key2="value2")['extra_specs']
self.assertEqual(body['key1'], 'value1')
self.assertIn('key2', body)
body = self.flavors_client.show_flavor_extra_spec(
diff --git a/tempest/api/compute/admin/test_flavors_extra_specs_negative.py b/tempest/api/compute/admin/test_flavors_extra_specs_negative.py
index 14646e8..5cde39e 100644
--- a/tempest/api/compute/admin/test_flavors_extra_specs_negative.py
+++ b/tempest/api/compute/admin/test_flavors_extra_specs_negative.py
@@ -14,11 +14,11 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.compute import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.common import utils
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class FlavorsExtraSpecsNegativeTestJSON(base.BaseV2ComputeAdminTest):
@@ -30,16 +30,11 @@
@classmethod
def skip_checks(cls):
super(FlavorsExtraSpecsNegativeTestJSON, cls).skip_checks()
- if not test.is_extension_enabled('OS-FLV-EXT-DATA', 'compute'):
+ if not utils.is_extension_enabled('OS-FLV-EXT-DATA', 'compute'):
msg = "OS-FLV-EXT-DATA extension not enabled."
raise cls.skipException(msg)
@classmethod
- def setup_clients(cls):
- super(FlavorsExtraSpecsNegativeTestJSON, cls).setup_clients()
- cls.client = cls.os_adm.flavors_client
-
- @classmethod
def resource_setup(cls):
super(FlavorsExtraSpecsNegativeTestJSON, cls).resource_setup()
@@ -48,26 +43,26 @@
vcpus = 1
disk = 10
ephemeral = 10
- cls.new_flavor_id = data_utils.rand_int_id(start=1000)
+ new_flavor_id = data_utils.rand_int_id(start=1000)
swap = 1024
rxtx = 1
# Create a flavor
- cls.flavor = cls.client.create_flavor(name=flavor_name,
- ram=ram, vcpus=vcpus,
- disk=disk,
- id=cls.new_flavor_id,
- ephemeral=ephemeral,
- swap=swap,
- rxtx_factor=rxtx)['flavor']
+ cls.flavor = cls.admin_flavors_client.create_flavor(
+ name=flavor_name,
+ ram=ram, vcpus=vcpus,
+ disk=disk,
+ id=new_flavor_id,
+ ephemeral=ephemeral,
+ swap=swap,
+ rxtx_factor=rxtx)['flavor']
+ cls.addClassResourceCleanup(
+ cls.admin_flavors_client.wait_for_resource_deletion,
+ cls.flavor['id'])
+ cls.addClassResourceCleanup(cls.admin_flavors_client.delete_flavor,
+ cls.flavor['id'])
- @classmethod
- def resource_cleanup(cls):
- cls.client.delete_flavor(cls.flavor['id'])
- cls.client.wait_for_resource_deletion(cls.flavor['id'])
- super(FlavorsExtraSpecsNegativeTestJSON, cls).resource_cleanup()
-
- @test.attr(type=['negative'])
- @test.idempotent_id('a00a3b81-5641-45a8-ab2b-4a8ec41e1d7d')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('a00a3b81-5641-45a8-ab2b-4a8ec41e1d7d')
def test_flavor_non_admin_set_keys(self):
# Test to SET flavor extra spec as a user without admin privileges.
self.assertRaises(lib_exc.Forbidden,
@@ -75,11 +70,11 @@
self.flavor['id'],
key1="value1", key2="value2")
- @test.attr(type=['negative'])
- @test.idempotent_id('1ebf4ef8-759e-48fe-a801-d451d80476fb')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('1ebf4ef8-759e-48fe-a801-d451d80476fb')
def test_flavor_non_admin_update_specific_key(self):
# non admin user is not allowed to update flavor extra spec
- body = self.client.set_flavor_extra_spec(
+ body = self.admin_flavors_client.set_flavor_extra_spec(
self.flavor['id'], key1="value1", key2="value2")['extra_specs']
self.assertEqual(body['key1'], 'value1')
self.assertRaises(lib_exc.Forbidden,
@@ -89,50 +84,49 @@
'key1',
key1='value1_new')
- @test.attr(type=['negative'])
- @test.idempotent_id('28f12249-27c7-44c1-8810-1f382f316b11')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('28f12249-27c7-44c1-8810-1f382f316b11')
def test_flavor_non_admin_unset_keys(self):
- self.client.set_flavor_extra_spec(self.flavor['id'],
- key1="value1", key2="value2")
+ self.admin_flavors_client.set_flavor_extra_spec(
+ self.flavor['id'], key1="value1", key2="value2")
self.assertRaises(lib_exc.Forbidden,
self.flavors_client.unset_flavor_extra_spec,
self.flavor['id'],
'key1')
- @test.attr(type=['negative'])
- @test.idempotent_id('440b9f3f-3c7f-4293-a106-0ceda350f8de')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('440b9f3f-3c7f-4293-a106-0ceda350f8de')
def test_flavor_unset_nonexistent_key(self):
- nonexistent_key = data_utils.rand_name('flavor_key')
self.assertRaises(lib_exc.NotFound,
- self.client.unset_flavor_extra_spec,
+ self.admin_flavors_client.unset_flavor_extra_spec,
self.flavor['id'],
- nonexistent_key)
+ 'nonexistent_key')
- @test.attr(type=['negative'])
- @test.idempotent_id('329a7be3-54b2-48be-8052-bf2ce4afd898')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('329a7be3-54b2-48be-8052-bf2ce4afd898')
def test_flavor_get_nonexistent_key(self):
self.assertRaises(lib_exc.NotFound,
self.flavors_client.show_flavor_extra_spec,
self.flavor['id'],
"nonexistent_key")
- @test.attr(type=['negative'])
- @test.idempotent_id('25b822b8-9f49-44f6-80de-d99f0482e5cb')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('25b822b8-9f49-44f6-80de-d99f0482e5cb')
def test_flavor_update_mismatch_key(self):
# the key will be updated should be match the key in the body
self.assertRaises(lib_exc.BadRequest,
- self.client.update_flavor_extra_spec,
+ self.admin_flavors_client.update_flavor_extra_spec,
self.flavor['id'],
"key2",
key1="value")
- @test.attr(type=['negative'])
- @test.idempotent_id('f5889590-bf66-41cc-b4b1-6e6370cfd93f')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('f5889590-bf66-41cc-b4b1-6e6370cfd93f')
def test_flavor_update_more_key(self):
# there should be just one item in the request body
self.assertRaises(lib_exc.BadRequest,
- self.client.update_flavor_extra_spec,
+ self.admin_flavors_client.update_flavor_extra_spec,
self.flavor['id'],
"key1",
key1="value",
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 456363c..72d09ed 100644
--- a/tempest/api/compute/admin/test_floating_ips_bulk.py
+++ b/tempest/api/compute/admin/test_floating_ips_bulk.py
@@ -16,9 +16,11 @@
import netaddr
from tempest.api.compute import base
+from tempest.common import utils
from tempest import config
-from tempest import exceptions
-from tempest import test
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions
CONF = config.CONF
@@ -29,11 +31,12 @@
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):
super(FloatingIPsBulkAdminTestJSON, cls).setup_clients()
- cls.client = cls.os_adm.floating_ips_bulk_client
+ cls.client = cls.os_admin.floating_ips_bulk_client
@classmethod
def resource_setup(cls):
@@ -54,14 +57,8 @@
raise exceptions.InvalidConfiguration(msg)
return
- def _delete_floating_ips_bulk(self, ip_range):
- try:
- self.client.delete_floating_ips_bulk(ip_range)
- except Exception:
- pass
-
- @test.idempotent_id('2c8f145f-8012-4cb8-ac7e-95a587f0e4ab')
- @test.services('network')
+ @decorators.idempotent_id('2c8f145f-8012-4cb8-ac7e-95a587f0e4ab')
+ @utils.services('network')
def test_create_list_delete_floating_ips_bulk(self):
# Create, List and delete the Floating IPs Bulk
pool = 'test_pool'
@@ -73,10 +70,11 @@
pool,
interface)
['floating_ips_bulk_create'])
- self.addCleanup(self._delete_floating_ips_bulk, self.ip_range)
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.client.delete_floating_ips_bulk, self.ip_range)
self.assertEqual(self.ip_range, body['ip_range'])
ips_list = self.client.list_floating_ips_bulk()['floating_ip_info']
- self.assertNotEqual(0, len(ips_list))
+ self.assertNotEmpty(ips_list)
for ip in netaddr.IPNetwork(self.ip_range).iter_hosts():
self.assertIn(str(ip), map(lambda x: x['address'], ips_list))
body = (self.client.delete_floating_ips_bulk(self.ip_range)
diff --git a/tempest/api/compute/admin/test_hosts.py b/tempest/api/compute/admin/test_hosts.py
index f6ea3a4..c246685 100644
--- a/tempest/api/compute/admin/test_hosts.py
+++ b/tempest/api/compute/admin/test_hosts.py
@@ -14,60 +14,57 @@
from tempest.api.compute import base
from tempest.common import tempest_fixtures as fixtures
-from tempest import test
+from tempest.lib import decorators
class HostsAdminTestJSON(base.BaseV2ComputeAdminTest):
"""Tests hosts API using admin privileges."""
+ max_microversion = '2.42'
+
@classmethod
def setup_clients(cls):
super(HostsAdminTestJSON, cls).setup_clients()
- cls.client = cls.os_adm.hosts_client
+ cls.client = cls.os_admin.hosts_client
- @test.idempotent_id('9bfaf98d-e2cb-44b0-a07e-2558b2821e4f')
+ @decorators.idempotent_id('9bfaf98d-e2cb-44b0-a07e-2558b2821e4f')
def test_list_hosts(self):
hosts = self.client.list_hosts()['hosts']
- self.assertTrue(len(hosts) >= 2, str(hosts))
+ self.assertGreaterEqual(len(hosts), 2, str(hosts))
- @test.idempotent_id('5dc06f5b-d887-47a2-bb2a-67762ef3c6de')
+ @decorators.idempotent_id('5dc06f5b-d887-47a2-bb2a-67762ef3c6de')
def test_list_hosts_with_zone(self):
self.useFixture(fixtures.LockFixture('availability_zone'))
hosts = self.client.list_hosts()['hosts']
host = hosts[0]
hosts = self.client.list_hosts(zone=host['zone'])['hosts']
- self.assertTrue(len(hosts) >= 1)
+ self.assertNotEmpty(hosts)
self.assertIn(host, hosts)
- @test.idempotent_id('9af3c171-fbf4-4150-a624-22109733c2a6')
+ @decorators.idempotent_id('9af3c171-fbf4-4150-a624-22109733c2a6')
def test_list_hosts_with_a_blank_zone(self):
# If send the request with a blank zone, the request will be successful
# and it will return all the hosts list
hosts = self.client.list_hosts(zone='')['hosts']
- self.assertNotEqual(0, len(hosts))
+ self.assertNotEmpty(hosts)
- @test.idempotent_id('c6ddbadb-c94e-4500-b12f-8ffc43843ff8')
+ @decorators.idempotent_id('c6ddbadb-c94e-4500-b12f-8ffc43843ff8')
def test_list_hosts_with_nonexistent_zone(self):
# If send the request with a nonexistent zone, the request will be
- # successful and no hosts will be retured
+ # successful and no hosts will be returned
hosts = self.client.list_hosts(zone='xxx')['hosts']
- self.assertEqual(0, len(hosts))
+ self.assertEmpty(hosts)
- @test.idempotent_id('38adbb12-aee2-4498-8aec-329c72423aa4')
+ @decorators.idempotent_id('38adbb12-aee2-4498-8aec-329c72423aa4')
def test_show_host_detail(self):
hosts = self.client.list_hosts()['hosts']
hosts = [host for host in hosts if host['service'] == 'compute']
- self.assertTrue(len(hosts) >= 1)
+ self.assertNotEmpty(hosts)
for host in hosts:
hostname = host['host_name']
resources = self.client.show_host(hostname)['host']
- self.assertTrue(len(resources) >= 1)
+ self.assertNotEmpty(resources)
host_resource = resources[0]['resource']
- self.assertIsNotNone(host_resource)
- self.assertIsNotNone(host_resource['cpu'])
- self.assertIsNotNone(host_resource['disk_gb'])
- self.assertIsNotNone(host_resource['memory_mb'])
- self.assertIsNotNone(host_resource['project'])
self.assertEqual(hostname, host_resource['host'])
diff --git a/tempest/api/compute/admin/test_hosts_negative.py b/tempest/api/compute/admin/test_hosts_negative.py
index 65ada4d..8a91ae2 100644
--- a/tempest/api/compute/admin/test_hosts_negative.py
+++ b/tempest/api/compute/admin/test_hosts_negative.py
@@ -12,156 +12,133 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.compute import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class HostsAdminNegativeTestJSON(base.BaseV2ComputeAdminTest):
"""Tests hosts API using admin privileges."""
+ max_microversion = '2.42'
+
@classmethod
def setup_clients(cls):
super(HostsAdminNegativeTestJSON, cls).setup_clients()
- cls.client = cls.os_adm.hosts_client
- cls.non_admin_client = cls.os.hosts_client
+ cls.client = cls.os_admin.hosts_client
+ cls.non_admin_client = cls.os_primary.hosts_client
- def _get_host_name(self):
- hosts = self.client.list_hosts()['hosts']
- self.assertTrue(len(hosts) >= 1)
- hostname = hosts[0]['host_name']
- return hostname
+ @classmethod
+ def resource_setup(cls):
+ super(HostsAdminNegativeTestJSON, cls).resource_setup()
+ hosts = cls.client.list_hosts()['hosts']
+ if not hosts:
+ raise lib_exc.NotFound("no host found")
+ cls.hostname = hosts[0]['host_name']
- @test.attr(type=['negative'])
- @test.idempotent_id('dd032027-0210-4d9c-860e-69b1b8deed5f')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('dd032027-0210-4d9c-860e-69b1b8deed5f')
def test_list_hosts_with_non_admin_user(self):
self.assertRaises(lib_exc.Forbidden,
self.non_admin_client.list_hosts)
- @test.attr(type=['negative'])
- @test.idempotent_id('e75b0a1a-041f-47a1-8b4a-b72a6ff36d3f')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('e75b0a1a-041f-47a1-8b4a-b72a6ff36d3f')
def test_show_host_detail_with_nonexistent_hostname(self):
- nonexitent_hostname = data_utils.rand_name('rand_hostname')
self.assertRaises(lib_exc.NotFound,
- self.client.show_host, nonexitent_hostname)
+ self.client.show_host, 'nonexistent_hostname')
- @test.attr(type=['negative'])
- @test.idempotent_id('19ebe09c-bfd4-4b7c-81a2-e2e0710f59cc')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('19ebe09c-bfd4-4b7c-81a2-e2e0710f59cc')
def test_show_host_detail_with_non_admin_user(self):
- hostname = self._get_host_name()
-
self.assertRaises(lib_exc.Forbidden,
self.non_admin_client.show_host,
- hostname)
+ self.hostname)
- @test.attr(type=['negative'])
- @test.idempotent_id('e40c72b1-0239-4ed6-ba21-81a184df1f7c')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('e40c72b1-0239-4ed6-ba21-81a184df1f7c')
def test_update_host_with_non_admin_user(self):
- hostname = self._get_host_name()
-
self.assertRaises(lib_exc.Forbidden,
self.non_admin_client.update_host,
- hostname,
+ self.hostname,
status='enable',
maintenance_mode='enable')
- @test.attr(type=['negative'])
- @test.idempotent_id('fbe2bf3e-3246-4a95-a59f-94e4e298ec77')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('fbe2bf3e-3246-4a95-a59f-94e4e298ec77')
def test_update_host_with_invalid_status(self):
# 'status' can only be 'enable' or 'disable'
- hostname = self._get_host_name()
-
self.assertRaises(lib_exc.BadRequest,
self.client.update_host,
- hostname,
+ self.hostname,
status='invalid',
maintenance_mode='enable')
- @test.attr(type=['negative'])
- @test.idempotent_id('ab1e230e-5e22-41a9-8699-82b9947915d4')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('ab1e230e-5e22-41a9-8699-82b9947915d4')
def test_update_host_with_invalid_maintenance_mode(self):
# 'maintenance_mode' can only be 'enable' or 'disable'
- hostname = self._get_host_name()
-
self.assertRaises(lib_exc.BadRequest,
self.client.update_host,
- hostname,
+ self.hostname,
status='enable',
maintenance_mode='invalid')
- @test.attr(type=['negative'])
- @test.idempotent_id('0cd85f75-6992-4a4a-b1bd-d11e37fd0eee')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('0cd85f75-6992-4a4a-b1bd-d11e37fd0eee')
def test_update_host_without_param(self):
# 'status' or 'maintenance_mode' needed for host update
- hostname = self._get_host_name()
-
self.assertRaises(lib_exc.BadRequest,
self.client.update_host,
- hostname)
+ self.hostname)
- @test.attr(type=['negative'])
- @test.idempotent_id('23c92146-2100-4d68-b2d6-c7ade970c9c1')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('23c92146-2100-4d68-b2d6-c7ade970c9c1')
def test_update_nonexistent_host(self):
- nonexitent_hostname = data_utils.rand_name('rand_hostname')
-
self.assertRaises(lib_exc.NotFound,
self.client.update_host,
- nonexitent_hostname,
+ 'nonexistent_hostname',
status='enable',
maintenance_mode='enable')
- @test.attr(type=['negative'])
- @test.idempotent_id('0d981ac3-4320-4898-b674-82b61fbb60e4')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('0d981ac3-4320-4898-b674-82b61fbb60e4')
def test_startup_nonexistent_host(self):
- nonexitent_hostname = data_utils.rand_name('rand_hostname')
-
self.assertRaises(lib_exc.NotFound,
self.client.startup_host,
- nonexitent_hostname)
+ 'nonexistent_hostname')
- @test.attr(type=['negative'])
- @test.idempotent_id('9f4ebb7e-b2ae-4e5b-a38f-0fd1bb0ddfca')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('9f4ebb7e-b2ae-4e5b-a38f-0fd1bb0ddfca')
def test_startup_host_with_non_admin_user(self):
- hostname = self._get_host_name()
-
self.assertRaises(lib_exc.Forbidden,
self.non_admin_client.startup_host,
- hostname)
+ self.hostname)
- @test.attr(type=['negative'])
- @test.idempotent_id('9e637444-29cf-4244-88c8-831ae82c31b6')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('9e637444-29cf-4244-88c8-831ae82c31b6')
def test_shutdown_nonexistent_host(self):
- nonexitent_hostname = data_utils.rand_name('rand_hostname')
-
self.assertRaises(lib_exc.NotFound,
self.client.shutdown_host,
- nonexitent_hostname)
+ 'nonexistent_hostname')
- @test.attr(type=['negative'])
- @test.idempotent_id('a803529c-7e3f-4d3c-a7d6-8e1c203d27f6')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('a803529c-7e3f-4d3c-a7d6-8e1c203d27f6')
def test_shutdown_host_with_non_admin_user(self):
- hostname = self._get_host_name()
-
self.assertRaises(lib_exc.Forbidden,
self.non_admin_client.shutdown_host,
- hostname)
+ self.hostname)
- @test.attr(type=['negative'])
- @test.idempotent_id('f86bfd7b-0b13-4849-ae29-0322e83ee58b')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('f86bfd7b-0b13-4849-ae29-0322e83ee58b')
def test_reboot_nonexistent_host(self):
- nonexitent_hostname = data_utils.rand_name('rand_hostname')
-
self.assertRaises(lib_exc.NotFound,
self.client.reboot_host,
- nonexitent_hostname)
+ 'nonexistent_hostname')
- @test.attr(type=['negative'])
- @test.idempotent_id('02d79bb9-eb57-4612-abf6-2cb38897d2f8')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('02d79bb9-eb57-4612-abf6-2cb38897d2f8')
def test_reboot_host_with_non_admin_user(self):
- hostname = self._get_host_name()
-
self.assertRaises(lib_exc.Forbidden,
self.non_admin_client.reboot_host,
- hostname)
+ self.hostname)
diff --git a/tempest/api/compute/admin/test_hypervisor.py b/tempest/api/compute/admin/test_hypervisor.py
index 113ec40..b23c59f 100644
--- a/tempest/api/compute/admin/test_hypervisor.py
+++ b/tempest/api/compute/admin/test_hypervisor.py
@@ -14,67 +14,57 @@
# under the License.
from tempest.api.compute import base
-from tempest import test
+from tempest.lib import decorators
-class HypervisorAdminTestJSON(base.BaseV2ComputeAdminTest):
+class HypervisorAdminTestBase(base.BaseV2ComputeAdminTest):
"""Tests Hypervisors API that require admin privileges"""
@classmethod
def setup_clients(cls):
- super(HypervisorAdminTestJSON, cls).setup_clients()
- cls.client = cls.os_adm.hypervisor_client
+ super(HypervisorAdminTestBase, cls).setup_clients()
+ cls.client = cls.os_admin.hypervisor_client
def _list_hypervisors(self):
# List of hypervisors
hypers = self.client.list_hypervisors()['hypervisors']
return hypers
- def assertHypervisors(self, hypers):
- self.assertTrue(len(hypers) > 0, "No hypervisors found: %s" % hypers)
- @test.idempotent_id('7f0ceacd-c64d-4e96-b8ee-d02943142cc5')
+class HypervisorAdminTestJSON(HypervisorAdminTestBase):
+ """Tests Hypervisors API that require admin privileges"""
+
+ @decorators.idempotent_id('7f0ceacd-c64d-4e96-b8ee-d02943142cc5')
def test_get_hypervisor_list(self):
# List of hypervisor and available hypervisors hostname
hypers = self._list_hypervisors()
- self.assertHypervisors(hypers)
+ self.assertNotEmpty(hypers, "No hypervisors found.")
- @test.idempotent_id('1e7fdac2-b672-4ad1-97a4-bad0e3030118')
+ @decorators.idempotent_id('1e7fdac2-b672-4ad1-97a4-bad0e3030118')
def test_get_hypervisor_list_details(self):
# Display the details of the all hypervisor
hypers = self.client.list_hypervisors(detail=True)['hypervisors']
- self.assertHypervisors(hypers)
+ self.assertNotEmpty(hypers, "No hypervisors found.")
- @test.idempotent_id('94ff9eae-a183-428e-9cdb-79fde71211cc')
+ @decorators.idempotent_id('94ff9eae-a183-428e-9cdb-79fde71211cc')
def test_get_hypervisor_show_details(self):
# Display the details of the specified hypervisor
hypers = self._list_hypervisors()
- self.assertHypervisors(hypers)
+ self.assertNotEmpty(hypers, "No hypervisors found.")
details = self.client.show_hypervisor(hypers[0]['id'])['hypervisor']
- self.assertTrue(len(details) > 0)
+ self.assertNotEmpty(details)
self.assertEqual(details['hypervisor_hostname'],
hypers[0]['hypervisor_hostname'])
- @test.idempotent_id('e81bba3f-6215-4e39-a286-d52d2f906862')
- def test_get_hypervisor_show_servers(self):
- # Show instances about the specific hypervisors
- hypers = self._list_hypervisors()
- self.assertHypervisors(hypers)
-
- hostname = hypers[0]['hypervisor_hostname']
- hypervisors = (self.client.list_servers_on_hypervisor(hostname)
- ['hypervisors'])
- self.assertTrue(len(hypervisors) > 0)
-
- @test.idempotent_id('797e4f28-b6e0-454d-a548-80cc77c00816')
+ @decorators.idempotent_id('797e4f28-b6e0-454d-a548-80cc77c00816')
def test_get_hypervisor_stats(self):
# Verify the stats of the all hypervisor
stats = (self.client.show_hypervisor_statistics()
['hypervisor_statistics'])
- self.assertTrue(len(stats) > 0)
+ self.assertNotEmpty(stats)
- @test.idempotent_id('91a50d7d-1c2b-4f24-b55a-a1fe20efca70')
+ @decorators.idempotent_id('91a50d7d-1c2b-4f24-b55a-a1fe20efca70')
def test_get_hypervisor_uptime(self):
# Verify that GET shows the specified hypervisor uptime
hypers = self._list_hypervisors()
@@ -87,7 +77,7 @@
ironic_only = True
hypers_without_ironic = []
for hyper in hypers:
- details = (self.client.show_hypervisor(hypers[0]['id'])
+ details = (self.client.show_hypervisor(hyper['id'])
['hypervisor'])
if details['hypervisor_type'] != 'ironic':
hypers_without_ironic.append(hyper)
@@ -104,7 +94,7 @@
try:
uptime = (self.client.show_hypervisor_uptime(hyper['id'])
['hypervisor'])
- if len(uptime) > 0:
+ if uptime:
has_valid_uptime = True
break
except Exception:
@@ -113,10 +103,25 @@
has_valid_uptime,
"None of the hypervisors had a valid uptime: %s" % hypers)
- @test.idempotent_id('d7e1805b-3b14-4a3b-b6fd-50ec6d9f361f')
+
+class HypervisorAdminUnderV252Test(HypervisorAdminTestBase):
+ max_microversion = '2.52'
+
+ @decorators.idempotent_id('e81bba3f-6215-4e39-a286-d52d2f906862')
+ def test_get_hypervisor_show_servers(self):
+ # Show instances about the specific hypervisors
+ hypers = self._list_hypervisors()
+ self.assertNotEmpty(hypers, "No hypervisors found.")
+
+ hostname = hypers[0]['hypervisor_hostname']
+ hypervisors = (self.client.list_servers_on_hypervisor(hostname)
+ ['hypervisors'])
+ self.assertNotEmpty(hypervisors)
+
+ @decorators.idempotent_id('d7e1805b-3b14-4a3b-b6fd-50ec6d9f361f')
def test_search_hypervisor(self):
hypers = self._list_hypervisors()
- self.assertHypervisors(hypers)
+ self.assertNotEmpty(hypers, "No hypervisors found.")
hypers = self.client.search_hypervisor(
hypers[0]['hypervisor_hostname'])['hypervisors']
- self.assertHypervisors(hypers)
+ self.assertNotEmpty(hypers, "No hypervisors found.")
diff --git a/tempest/api/compute/admin/test_hypervisor_negative.py b/tempest/api/compute/admin/test_hypervisor_negative.py
index 0e8012a..0056376 100644
--- a/tempest/api/compute/admin/test_hypervisor_negative.py
+++ b/tempest/api/compute/admin/test_hypervisor_negative.py
@@ -13,22 +13,19 @@
# License for the specific language governing permissions and limitations
# under the License.
-import uuid
-
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.compute import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
-class HypervisorAdminNegativeTestJSON(base.BaseV2ComputeAdminTest):
+class HypervisorAdminNegativeTestBase(base.BaseV2ComputeAdminTest):
"""Tests Hypervisors API that require admin privileges"""
@classmethod
def setup_clients(cls):
- super(HypervisorAdminNegativeTestJSON, cls).setup_clients()
- cls.client = cls.os_adm.hypervisor_client
+ super(HypervisorAdminNegativeTestBase, cls).setup_clients()
+ cls.client = cls.os_admin.hypervisor_client
cls.non_adm_client = cls.hypervisor_client
def _list_hypervisors(self):
@@ -36,109 +33,115 @@
hypers = self.client.list_hypervisors()['hypervisors']
return hypers
- @test.attr(type=['negative'])
- @test.idempotent_id('c136086a-0f67-4b2b-bc61-8482bd68989f')
+
+class HypervisorAdminNegativeTestJSON(HypervisorAdminNegativeTestBase):
+ """Tests Hypervisors API that require admin privileges"""
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('c136086a-0f67-4b2b-bc61-8482bd68989f')
def test_show_nonexistent_hypervisor(self):
- nonexistent_hypervisor_id = str(uuid.uuid4())
+ nonexistent_hypervisor_id = data_utils.rand_uuid()
self.assertRaises(
lib_exc.NotFound,
self.client.show_hypervisor,
nonexistent_hypervisor_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('51e663d0-6b89-4817-a465-20aca0667d03')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('51e663d0-6b89-4817-a465-20aca0667d03')
def test_show_hypervisor_with_non_admin_user(self):
hypers = self._list_hypervisors()
- self.assertTrue(len(hypers) > 0)
+ self.assertNotEmpty(hypers)
self.assertRaises(
lib_exc.Forbidden,
self.non_adm_client.show_hypervisor,
hypers[0]['id'])
- @test.attr(type=['negative'])
- @test.idempotent_id('2a0a3938-832e-4859-95bf-1c57c236b924')
- def test_show_servers_with_non_admin_user(self):
- hypers = self._list_hypervisors()
- self.assertTrue(len(hypers) > 0)
-
- self.assertRaises(
- lib_exc.Forbidden,
- self.non_adm_client.list_servers_on_hypervisor,
- hypers[0]['id'])
-
- @test.attr(type=['negative'])
- @test.idempotent_id('02463d69-0ace-4d33-a4a8-93d7883a2bba')
- def test_show_servers_with_nonexistent_hypervisor(self):
- nonexistent_hypervisor_id = str(uuid.uuid4())
-
- self.assertRaises(
- lib_exc.NotFound,
- self.client.list_servers_on_hypervisor,
- nonexistent_hypervisor_id)
-
- @test.attr(type=['negative'])
- @test.idempotent_id('e2b061bb-13f9-40d8-9d6e-d5bf17595849')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('e2b061bb-13f9-40d8-9d6e-d5bf17595849')
def test_get_hypervisor_stats_with_non_admin_user(self):
self.assertRaises(
lib_exc.Forbidden,
self.non_adm_client.show_hypervisor_statistics)
- @test.attr(type=['negative'])
- @test.idempotent_id('f60aa680-9a3a-4c7d-90e1-fae3a4891303')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('f60aa680-9a3a-4c7d-90e1-fae3a4891303')
def test_get_nonexistent_hypervisor_uptime(self):
- nonexistent_hypervisor_id = str(uuid.uuid4())
+ nonexistent_hypervisor_id = data_utils.rand_uuid()
self.assertRaises(
lib_exc.NotFound,
self.client.show_hypervisor_uptime,
nonexistent_hypervisor_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('6c3461f9-c04c-4e2a-bebb-71dc9cb47df2')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('6c3461f9-c04c-4e2a-bebb-71dc9cb47df2')
def test_get_hypervisor_uptime_with_non_admin_user(self):
hypers = self._list_hypervisors()
- self.assertTrue(len(hypers) > 0)
+ self.assertNotEmpty(hypers)
self.assertRaises(
lib_exc.Forbidden,
self.non_adm_client.show_hypervisor_uptime,
hypers[0]['id'])
- @test.attr(type=['negative'])
- @test.idempotent_id('51b3d536-9b14-409c-9bce-c6f7c794994e')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('51b3d536-9b14-409c-9bce-c6f7c794994e')
def test_get_hypervisor_list_with_non_admin_user(self):
# List of hypervisor and available services with non admin user
self.assertRaises(
lib_exc.Forbidden,
self.non_adm_client.list_hypervisors)
- @test.attr(type=['negative'])
- @test.idempotent_id('dc02db05-e801-4c5f-bc8e-d915290ab345')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('dc02db05-e801-4c5f-bc8e-d915290ab345')
def test_get_hypervisor_list_details_with_non_admin_user(self):
# List of hypervisor details and available services with non admin user
self.assertRaises(
lib_exc.Forbidden,
self.non_adm_client.list_hypervisors, detail=True)
- @test.attr(type=['negative'])
- @test.idempotent_id('19a45cc1-1000-4055-b6d2-28e8b2ec4faa')
- def test_search_nonexistent_hypervisor(self):
- nonexistent_hypervisor_name = data_utils.rand_name('test_hypervisor')
+
+class HypervisorAdminNegativeUnderV252Test(HypervisorAdminNegativeTestBase):
+ max_microversion = '2.52'
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('2a0a3938-832e-4859-95bf-1c57c236b924')
+ def test_show_servers_with_non_admin_user(self):
+ hypers = self._list_hypervisors()
+ self.assertNotEmpty(hypers)
+
+ self.assertRaises(
+ lib_exc.Forbidden,
+ self.non_adm_client.list_servers_on_hypervisor,
+ hypers[0]['id'])
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('02463d69-0ace-4d33-a4a8-93d7883a2bba')
+ def test_show_servers_with_nonexistent_hypervisor(self):
+ nonexistent_hypervisor_id = data_utils.rand_uuid()
self.assertRaises(
lib_exc.NotFound,
- self.client.search_hypervisor,
- nonexistent_hypervisor_name)
+ self.client.list_servers_on_hypervisor,
+ nonexistent_hypervisor_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('5b6a6c79-5dc1-4fa5-9c58-9c8085948e74')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('5b6a6c79-5dc1-4fa5-9c58-9c8085948e74')
def test_search_hypervisor_with_non_admin_user(self):
hypers = self._list_hypervisors()
- self.assertTrue(len(hypers) > 0)
+ self.assertNotEmpty(hypers)
self.assertRaises(
lib_exc.Forbidden,
self.non_adm_client.search_hypervisor,
hypers[0]['hypervisor_hostname'])
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('19a45cc1-1000-4055-b6d2-28e8b2ec4faa')
+ def test_search_nonexistent_hypervisor(self):
+ self.assertRaises(
+ lib_exc.NotFound,
+ self.client.search_hypervisor,
+ 'nonexistent_hypervisor_name')
diff --git a/tempest/api/compute/admin/test_instance_usage_audit_log.py b/tempest/api/compute/admin/test_instance_usage_audit_log.py
index 6976783..e4a2ffd 100644
--- a/tempest/api/compute/admin/test_instance_usage_audit_log.py
+++ b/tempest/api/compute/admin/test_instance_usage_audit_log.py
@@ -18,7 +18,7 @@
from six.moves.urllib import parse as urllib
from tempest.api.compute import base
-from tempest import test
+from tempest.lib import decorators
class InstanceUsageAuditLogTestJSON(base.BaseV2ComputeAdminTest):
@@ -26,9 +26,9 @@
@classmethod
def setup_clients(cls):
super(InstanceUsageAuditLogTestJSON, cls).setup_clients()
- cls.adm_client = cls.os_adm.instance_usages_audit_log_client
+ cls.adm_client = cls.os_admin.instance_usages_audit_log_client
- @test.idempotent_id('25319919-33d9-424f-9f99-2c203ee48b9d')
+ @decorators.idempotent_id('25319919-33d9-424f-9f99-2c203ee48b9d')
def test_list_instance_usage_audit_logs(self):
# list instance usage audit logs
body = (self.adm_client.list_instance_usage_audit_logs()
@@ -41,7 +41,7 @@
for item in expected_items:
self.assertIn(item, body)
- @test.idempotent_id('6e40459d-7c5f-400b-9e83-449fbc8e7feb')
+ @decorators.idempotent_id('6e40459d-7c5f-400b-9e83-449fbc8e7feb')
def test_get_instance_usage_audit_log(self):
# Get instance usage audit log before specified time
now = datetime.datetime.now()
diff --git a/tempest/api/compute/admin/test_instance_usage_audit_log_negative.py b/tempest/api/compute/admin/test_instance_usage_audit_log_negative.py
index eea3103..de8e221 100644
--- a/tempest/api/compute/admin/test_instance_usage_audit_log_negative.py
+++ b/tempest/api/compute/admin/test_instance_usage_audit_log_negative.py
@@ -16,10 +16,10 @@
import datetime
from six.moves.urllib import parse as urllib
-from tempest_lib import exceptions as lib_exc
from tempest.api.compute import base
-from tempest import test
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class InstanceUsageAuditLogNegativeTestJSON(base.BaseV2ComputeAdminTest):
@@ -27,10 +27,10 @@
@classmethod
def setup_clients(cls):
super(InstanceUsageAuditLogNegativeTestJSON, cls).setup_clients()
- cls.adm_client = cls.os_adm.instance_usages_audit_log_client
+ cls.adm_client = cls.os_admin.instance_usages_audit_log_client
- @test.attr(type=['negative'])
- @test.idempotent_id('a9d33178-d2c9-4131-ad3b-f4ca8d0308a2')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('a9d33178-d2c9-4131-ad3b-f4ca8d0308a2')
def test_instance_usage_audit_logs_with_nonadmin_user(self):
# the instance_usage_audit_logs API just can be accessed by admin user
self.assertRaises(lib_exc.Forbidden,
@@ -42,8 +42,8 @@
show_instance_usage_audit_log,
urllib.quote(now.strftime("%Y-%m-%d %H:%M:%S")))
- @test.attr(type=['negative'])
- @test.idempotent_id('9b952047-3641-41c7-ba91-a809fc5974c8')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('9b952047-3641-41c7-ba91-a809fc5974c8')
def test_get_instance_usage_audit_logs_with_invalid_time(self):
self.assertRaises(lib_exc.BadRequest,
self.adm_client.show_instance_usage_audit_log,
diff --git a/tempest/api/compute/admin/test_keypairs_v210.py b/tempest/api/compute/admin/test_keypairs_v210.py
new file mode 100644
index 0000000..24ea8a1
--- /dev/null
+++ b/tempest/api/compute/admin/test_keypairs_v210.py
@@ -0,0 +1,77 @@
+# Copyright 2016 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.keypairs import base
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+
+
+class KeyPairsV210TestJSON(base.BaseKeypairTest):
+ credentials = ['primary', 'admin']
+ min_microversion = '2.10'
+
+ @classmethod
+ def setup_clients(cls):
+ super(KeyPairsV210TestJSON, cls).setup_clients()
+ cls.client = cls.os_admin.keypairs_client
+ cls.non_admin_client = cls.os_primary.keypairs_client
+
+ def _create_and_check_keypairs(self, user_id):
+ key_list = list()
+ for _ in range(2):
+ k_name = data_utils.rand_name('keypair')
+ keypair = self.create_keypair(k_name,
+ keypair_type='ssh',
+ user_id=user_id,
+ client=self.client)
+ self.assertEqual(k_name, keypair['name'],
+ "The created keypair name is not equal "
+ "to the requested name!")
+ self.assertEqual(user_id, keypair['user_id'],
+ "The created keypair is not for requested user!")
+ keypair.pop('private_key', None)
+ keypair.pop('user_id')
+ key_list.append(keypair)
+ return key_list
+
+ @decorators.idempotent_id('3c8484af-cfb3-48f6-b8ba-d5d58bbf3eac')
+ def test_admin_manage_keypairs_for_other_users(self):
+ user_id = self.non_admin_client.user_id
+ key_list = self._create_and_check_keypairs(user_id)
+ first_keyname = key_list[0]['name']
+ keypair_detail = self.client.show_keypair(first_keyname,
+ user_id=user_id)['keypair']
+ self.assertEqual(first_keyname, keypair_detail['name'])
+ self.assertEqual(user_id, keypair_detail['user_id'],
+ "The fetched keypair is not for requested user!")
+ # Create a admin keypair
+ admin_keypair = self.create_keypair(keypair_type='ssh',
+ client=self.client)
+ admin_keypair.pop('private_key', None)
+ admin_keypair.pop('user_id')
+
+ # Admin fetch keypairs list of non admin user
+ keypairs = self.client.list_keypairs(user_id=user_id)['keypairs']
+ fetched_list = [keypair['keypair'] for keypair in keypairs]
+
+ # Check admin keypair is not present in non admin user keypairs list
+ self.assertNotIn(admin_keypair, fetched_list,
+ "The fetched user keypairs has admin keypair!")
+
+ # Now check if all the created keypairs are in the fetched list
+ missing_kps = [kp for kp in key_list if kp not in fetched_list]
+ self.assertFalse(missing_kps,
+ "Failed to find keypairs %s in fetched list"
+ % ', '.join(m_key['name'] for m_key in missing_kps))
diff --git a/tempest/api/compute/admin/test_live_migration.py b/tempest/api/compute/admin/test_live_migration.py
index 7c4c30c..bc38144 100644
--- a/tempest/api/compute/admin/test_live_migration.py
+++ b/tempest/api/compute/admin/test_live_migration.py
@@ -13,23 +13,29 @@
# License for the specific language governing permissions and limitations
# under the License.
+import time
+from oslo_log import log as logging
import testtools
from tempest.api.compute import base
+from tempest.common import compute
+from tempest.common import utils
from tempest.common import waiters
from tempest import config
-from tempest import test
+from tempest.lib import decorators
CONF = config.CONF
+LOG = logging.getLogger(__name__)
-class LiveBlockMigrationTestJSON(base.BaseV2ComputeAdminTest):
- _host_key = 'OS-EXT-SRV-ATTR:host'
+class LiveMigrationTest(base.BaseV2ComputeAdminTest):
+ max_microversion = '2.24'
+ block_migration = None
@classmethod
def skip_checks(cls):
- super(LiveBlockMigrationTestJSON, cls).skip_checks()
+ super(LiveMigrationTest, cls).skip_checks()
if not CONF.compute_feature_enabled.live_migration:
skip_msg = ("%s skipped as live-migration is "
@@ -40,56 +46,49 @@
"Less than 2 compute nodes, skipping migration test.")
@classmethod
- def setup_clients(cls):
- super(LiveBlockMigrationTestJSON, cls).setup_clients()
- cls.admin_hosts_client = cls.os_adm.hosts_client
- cls.admin_servers_client = cls.os_adm.servers_client
- cls.admin_migration_client = cls.os_adm.migrations_client
+ def setup_credentials(cls):
+ # These tests don't attempt any SSH validation nor do they use
+ # floating IPs on the instance, so all we need is a network and
+ # a subnet so the instance being migrated has a single port, but
+ # we need that to make sure we are properly updating the port
+ # host bindings during the live migration.
+ # TODO(mriedem): SSH validation before and after the instance is
+ # live migrated would be a nice test wrinkle addition.
+ cls.set_network_resources(network=True, subnet=True)
+ super(LiveMigrationTest, cls).setup_credentials()
@classmethod
- def _get_compute_hostnames(cls):
- body = cls.admin_hosts_client.list_hosts()['hosts']
- return [
- host_record['host_name']
- for host_record in body
- if host_record['service'] == 'compute'
- ]
+ def setup_clients(cls):
+ super(LiveMigrationTest, cls).setup_clients()
+ cls.admin_migration_client = cls.os_admin.migrations_client
- def _get_server_details(self, server_id):
- body = self.admin_servers_client.show_server(server_id)['server']
- return body
-
- def _get_host_for_server(self, server_id):
- return self._get_server_details(server_id)[self._host_key]
-
- def _migrate_server_to(self, server_id, dest_host, volume_backed):
- block_migration = (CONF.compute_feature_enabled.
- block_migration_for_live_migration and
- not volume_backed)
- body = self.admin_servers_client.live_migrate_server(
+ def _migrate_server_to(self, server_id, dest_host, volume_backed=False):
+ kwargs = dict()
+ block_migration = getattr(self, 'block_migration', None)
+ if self.block_migration is None:
+ kwargs['disk_over_commit'] = False
+ block_migration = (CONF.compute_feature_enabled.
+ block_migration_for_live_migration and
+ not volume_backed)
+ self.admin_servers_client.live_migrate_server(
server_id, host=dest_host, block_migration=block_migration,
- disk_over_commit=False)
- return body
+ **kwargs)
- def _get_host_other_than(self, host):
- for target_host in self._get_compute_hostnames():
- if host != target_host:
- return target_host
+ def _live_migrate(self, server_id, target_host, state,
+ volume_backed=False):
+ self._migrate_server_to(server_id, target_host, volume_backed)
+ waiters.wait_for_server_status(self.servers_client, server_id, state)
+ migration_list = (self.admin_migration_client.list_migrations()
+ ['migrations'])
- def _get_server_status(self, server_id):
- return self._get_server_details(server_id)['status']
-
- def _create_server(self, volume_backed=False):
- server = self.create_test_server(wait_until="ACTIVE",
- volume_backed=volume_backed)
- return server['id']
-
- def _volume_clean_up(self, server_id, volume_id):
- body = self.volumes_client.show_volume(volume_id)['volume']
- if body['status'] == 'in-use':
- self.servers_client.detach_volume(server_id, volume_id)
- self.volumes_client.wait_for_volume_status(volume_id, 'available')
- self.volumes_client.delete_volume(volume_id)
+ msg = ("Live Migration failed. Migrations list for Instance "
+ "%s: [" % server_id)
+ for live_migration in migration_list:
+ if (live_migration['instance_uuid'] == server_id):
+ msg += "\n%s" % live_migration
+ msg += "]"
+ self.assertEqual(target_host, self.get_host_for_server(server_id),
+ msg)
def _test_live_migration(self, state='ACTIVE', volume_backed=False):
"""Tests live migration between two hosts.
@@ -103,73 +102,143 @@
volume_backed, *block* migration is not used.
"""
# Live migrate an instance to another host
- server_id = self._create_server(volume_backed=volume_backed)
- actual_host = self._get_host_for_server(server_id)
- target_host = self._get_host_other_than(actual_host)
+ server_id = self.create_test_server(wait_until="ACTIVE",
+ volume_backed=volume_backed)['id']
+ source_host = self.get_host_for_server(server_id)
+ destination_host = self.get_host_other_than(server_id)
if state == 'PAUSED':
self.admin_servers_client.pause_server(server_id)
waiters.wait_for_server_status(self.admin_servers_client,
server_id, state)
- self._migrate_server_to(server_id, target_host, volume_backed)
- waiters.wait_for_server_status(self.servers_client, server_id, state)
- migration_list = (self.admin_migration_client.list_migrations()
- ['migrations'])
+ LOG.info("Live migrate from source %s to destination %s",
+ source_host, destination_host)
+ self._live_migrate(server_id, destination_host, state, volume_backed)
+ if CONF.compute_feature_enabled.live_migrate_back_and_forth:
+ # If live_migrate_back_and_forth is enabled it is a grenade job.
+ # Therefore test should validate whether LM is compatible in both
+ # ways, so live migrate VM back to the source host
+ LOG.info("Live migrate back to source %s", source_host)
+ self._live_migrate(server_id, source_host, state, volume_backed)
- msg = ("Live Migration failed. Migrations list for Instance "
- "%s: [" % server_id)
- for live_migration in migration_list:
- if (live_migration['instance_uuid'] == server_id):
- msg += "\n%s" % live_migration
- msg += "]"
- self.assertEqual(target_host, self._get_host_for_server(server_id),
- msg)
-
- @test.idempotent_id('1dce86b8-eb04-4c03-a9d8-9c1dc3ee0c7b')
+ @decorators.idempotent_id('1dce86b8-eb04-4c03-a9d8-9c1dc3ee0c7b')
def test_live_block_migration(self):
self._test_live_migration()
- @test.idempotent_id('1e107f21-61b2-4988-8f22-b196e938ab88')
+ @decorators.idempotent_id('1e107f21-61b2-4988-8f22-b196e938ab88')
@testtools.skipUnless(CONF.compute_feature_enabled.pause,
'Pause is not available.')
- @testtools.skipUnless(CONF.compute_feature_enabled
- .live_migrate_paused_instances,
- 'Live migration of paused instances is not '
- 'available.')
def test_live_block_migration_paused(self):
self._test_live_migration(state='PAUSED')
- @test.idempotent_id('5071cf17-3004-4257-ae61-73a84e28badd')
- @test.services('volume')
+ @testtools.skipUnless(CONF.compute_feature_enabled.
+ volume_backed_live_migration,
+ 'Volume-backed live migration not available')
+ @decorators.idempotent_id('5071cf17-3004-4257-ae61-73a84e28badd')
+ @utils.services('volume')
def test_volume_backed_live_migration(self):
self._test_live_migration(volume_backed=True)
- @test.idempotent_id('e19c0cc6-6720-4ed8-be83-b6603ed5c812')
+ @decorators.idempotent_id('e19c0cc6-6720-4ed8-be83-b6603ed5c812')
@testtools.skipIf(not CONF.compute_feature_enabled.
block_migration_for_live_migration,
'Block Live migration not available')
@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_id = self._create_server()
- actual_host = self._get_host_for_server(server_id)
- target_host = self._get_host_other_than(actual_host)
+ server = self.create_test_server(wait_until="ACTIVE")
+ server_id = server['id']
+ target_host = self.get_host_other_than(server_id)
- volume = self.volumes_client.create_volume(
- display_name='test')['volume']
-
- self.volumes_client.wait_for_volume_status(volume['id'],
- 'available')
- self.addCleanup(self._volume_clean_up, server_id, volume['id'])
+ volume = self.create_volume()
# Attach the volume to the server
- self.servers_client.attach_volume(server_id, volumeId=volume['id'],
- device='/dev/xvdb')
- self.volumes_client.wait_for_volume_status(volume['id'], 'in-use')
+ self.attach_volume(server, volume, device='/dev/xvdb')
+ server = self.admin_servers_client.show_server(server_id)['server']
+ volume_id1 = server["os-extended-volumes:volumes_attached"][0]["id"]
+ self._live_migrate(server_id, target_host, 'ACTIVE')
- self._migrate_server_to(server_id, target_host)
- waiters.wait_for_server_status(self.servers_client,
- server_id, 'ACTIVE')
- self.assertEqual(target_host, self._get_host_for_server(server_id))
+ server = self.admin_servers_client.show_server(server_id)['server']
+ volume_id2 = server["os-extended-volumes:volumes_attached"][0]["id"]
+
+ self.assertEqual(volume_id1, volume_id2)
+
+
+class LiveMigrationRemoteConsolesV26Test(LiveMigrationTest):
+ min_microversion = '2.6'
+ max_microversion = 'latest'
+
+ @decorators.idempotent_id('6190af80-513e-4f0f-90f2-9714e84955d7')
+ @testtools.skipUnless(CONF.compute_feature_enabled.serial_console,
+ 'Serial console not supported.')
+ @testtools.skipUnless(
+ compute.is_scheduler_filter_enabled("DifferentHostFilter"),
+ 'DifferentHostFilter is not available.')
+ def test_live_migration_serial_console(self):
+ """Test the live-migration of an instance which has a serial console
+
+ The serial console feature of an instance uses ports on the host.
+ These ports need to be updated when they are already in use by
+ another instance on the target host. This test checks if this
+ update behavior is correctly done, by connecting to the serial
+ consoles of the instances before and after the live migration.
+ """
+ server01_id = self.create_test_server(wait_until='ACTIVE')['id']
+ hints = {'different_host': server01_id}
+ server02_id = self.create_test_server(scheduler_hints=hints,
+ wait_until='ACTIVE')['id']
+ host01_id = self.get_host_for_server(server01_id)
+ host02_id = self.get_host_for_server(server02_id)
+ self.assertNotEqual(host01_id, host02_id)
+
+ # At this step we have 2 instances on different hosts, both with
+ # serial consoles, both with port 10000 (the default value).
+ # https://bugs.launchpad.net/nova/+bug/1455252 describes the issue
+ # when live-migrating in such a scenario.
+
+ self._verify_console_interaction(server01_id)
+ self._verify_console_interaction(server02_id)
+
+ self._live_migrate(server01_id, host02_id, 'ACTIVE')
+ self._verify_console_interaction(server01_id)
+ # At this point, both instances have a valid serial console
+ # connection, which means the ports got updated.
+
+ def _verify_console_interaction(self, server_id):
+ body = self.servers_client.get_remote_console(server_id,
+ console_type='serial',
+ protocol='serial')
+ console_url = body['remote_console']['url']
+ data = "test_live_migration_serial_console"
+ console_output = ''
+ t = 0.0
+ interval = 0.1
+
+ ws = compute.create_websocket(console_url)
+ try:
+ # NOTE (markus_z): It can take a long time until the terminal
+ # of the instance is available for interaction. Hence the
+ # long timeout value.
+ while data not in console_output and t <= 120.0:
+ try:
+ ws.send_frame(data)
+ received = ws.receive_frame()
+ console_output += received
+ except Exception:
+ # In case we had an issue with send/receive on the
+ # websocket connection, we create a new one.
+ ws = compute.create_websocket(console_url)
+ time.sleep(interval)
+ t += interval
+ finally:
+ ws.close()
+ self.assertIn(data, console_output)
+
+
+class LiveAutoBlockMigrationV225Test(LiveMigrationTest):
+ min_microversion = '2.25'
+ max_microversion = 'latest'
+ block_migration = 'auto'
diff --git a/tempest/api/compute/admin/test_live_migration_negative.py b/tempest/api/compute/admin/test_live_migration_negative.py
new file mode 100644
index 0000000..deabbc2
--- /dev/null
+++ b/tempest/api/compute/admin/test_live_migration_negative.py
@@ -0,0 +1,63 @@
+# 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 tempest.api.compute import base
+from tempest.common import waiters
+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 LiveMigrationNegativeTest(base.BaseV2ComputeAdminTest):
+ @classmethod
+ def skip_checks(cls):
+ super(LiveMigrationNegativeTest, cls).skip_checks()
+ if not CONF.compute_feature_enabled.live_migration:
+ raise cls.skipException("Live migration is not enabled")
+
+ def _migrate_server_to(self, server_id, dest_host):
+ bmflm = CONF.compute_feature_enabled.block_migration_for_live_migration
+ self.admin_servers_client.live_migrate_server(
+ server_id, host=dest_host, block_migration=bmflm,
+ disk_over_commit=False)
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('7fb7856e-ae92-44c9-861a-af62d7830bcb')
+ def test_invalid_host_for_migration(self):
+ # Migrating to an invalid host should not change the status
+ target_host = data_utils.rand_name('host')
+ server = self.create_test_server(wait_until="ACTIVE")
+
+ self.assertRaises(lib_exc.BadRequest, self._migrate_server_to,
+ server['id'], target_host)
+ waiters.wait_for_server_status(self.servers_client, server['id'],
+ 'ACTIVE')
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('6e2f94f5-2ee8-4830-bef5-5bc95bb0795b')
+ def test_live_block_migration_suspended(self):
+ server = self.create_test_server(wait_until="ACTIVE")
+
+ self.admin_servers_client.suspend_server(server['id'])
+ waiters.wait_for_server_status(self.servers_client,
+ server['id'], 'SUSPENDED')
+
+ destination_host = self.get_host_other_than(server['id'])
+
+ self.assertRaises(lib_exc.Conflict, self._migrate_server_to,
+ server['id'], destination_host)
diff --git a/tempest/api/compute/admin/test_migrations.py b/tempest/api/compute/admin/test_migrations.py
index f81d665..a6b71b2 100644
--- a/tempest/api/compute/admin/test_migrations.py
+++ b/tempest/api/compute/admin/test_migrations.py
@@ -17,7 +17,9 @@
from tempest.api.compute import base
from tempest.common import waiters
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions
CONF = config.CONF
@@ -27,14 +29,14 @@
@classmethod
def setup_clients(cls):
super(MigrationsAdminTest, cls).setup_clients()
- cls.client = cls.os_adm.migrations_client
+ cls.client = cls.os_admin.migrations_client
- @test.idempotent_id('75c0b83d-72a0-4cf8-a153-631e83e7d53f')
+ @decorators.idempotent_id('75c0b83d-72a0-4cf8-a153-631e83e7d53f')
def test_list_migrations(self):
# Admin can get the migrations list
self.client.list_migrations()
- @test.idempotent_id('1b512062-8093-438e-b47a-37d2f597cd64')
+ @decorators.idempotent_id('1b512062-8093-438e-b47a-37d2f597cd64')
@testtools.skipUnless(CONF.compute_feature_enabled.resize,
'Resize not available.')
def test_list_migrations_in_flavor_resize_situation(self):
@@ -42,14 +44,105 @@
server = self.create_test_server(wait_until="ACTIVE")
server_id = server['id']
- self.servers_client.resize_server(server_id, self.flavor_ref_alt)
- waiters.wait_for_server_status(self.servers_client,
- server_id, 'VERIFY_RESIZE')
- self.servers_client.confirm_resize_server(server_id)
- waiters.wait_for_server_status(self.servers_client,
- server_id, 'ACTIVE')
+ self.resize_server(server_id, self.flavor_ref_alt)
body = self.client.list_migrations()['migrations']
instance_uuids = [x['instance_uuid'] for x in body]
self.assertIn(server_id, instance_uuids)
+
+ def _flavor_clean_up(self, flavor_id):
+ try:
+ self.admin_flavors_client.delete_flavor(flavor_id)
+ self.admin_flavors_client.wait_for_resource_deletion(flavor_id)
+ except exceptions.NotFound:
+ pass
+
+ @decorators.idempotent_id('33f1fec3-ba18-4470-8e4e-1d888e7c3593')
+ @testtools.skipUnless(CONF.compute_feature_enabled.resize,
+ 'Resize not available.')
+ def test_resize_server_revert_deleted_flavor(self):
+ # Tests that we can revert the resize on an instance whose original
+ # flavor has been deleted.
+
+ # First we have to create a flavor that we can delete so make a copy
+ # of the normal flavor from which we'd create a server.
+ flavor = self.admin_flavors_client.show_flavor(
+ self.flavor_ref)['flavor']
+ flavor = self.admin_flavors_client.create_flavor(
+ name=data_utils.rand_name('test_resize_flavor_'),
+ ram=flavor['ram'],
+ disk=flavor['disk'],
+ vcpus=flavor['vcpus']
+ )['flavor']
+ self.addCleanup(self._flavor_clean_up, flavor['id'])
+
+ # Set extra specs same as self.flavor_ref for the created flavor,
+ # because the environment may need some special extra specs to
+ # create server which should have been contained in
+ # self.flavor_ref.
+ extra_spec_keys = self.admin_flavors_client.list_flavor_extra_specs(
+ self.flavor_ref)['extra_specs']
+ if extra_spec_keys:
+ self.admin_flavors_client.set_flavor_extra_spec(
+ flavor['id'], **extra_spec_keys)
+
+ # Now boot a server with the copied flavor.
+ server = self.create_test_server(
+ wait_until='ACTIVE', flavor=flavor['id'])
+
+ # Delete the flavor we used to boot the instance.
+ self._flavor_clean_up(flavor['id'])
+
+ # Now resize the server and wait for it to go into verify state.
+ self.servers_client.resize_server(server['id'], self.flavor_ref_alt)
+ waiters.wait_for_server_status(self.servers_client, server['id'],
+ 'VERIFY_RESIZE')
+
+ # Now revert the resize, it should be OK even though the original
+ # flavor used to boot the server was deleted.
+ self.servers_client.revert_resize_server(server['id'])
+ waiters.wait_for_server_status(self.servers_client, server['id'],
+ 'ACTIVE')
+
+ server = self.servers_client.show_server(server['id'])['server']
+ self.assertEqual(flavor['id'], server['flavor']['id'])
+
+ def _test_cold_migrate_server(self, revert=False):
+ if CONF.compute.min_compute_nodes < 2:
+ msg = "Less than 2 compute nodes, skipping multinode tests."
+ raise self.skipException(msg)
+
+ server = self.create_test_server(wait_until="ACTIVE")
+ src_host = self.admin_servers_client.show_server(
+ server['id'])['server']['OS-EXT-SRV-ATTR:host']
+
+ self.admin_servers_client.migrate_server(server['id'])
+
+ waiters.wait_for_server_status(self.servers_client,
+ server['id'], 'VERIFY_RESIZE')
+
+ if revert:
+ self.servers_client.revert_resize_server(server['id'])
+ assert_func = self.assertEqual
+ else:
+ self.servers_client.confirm_resize_server(server['id'])
+ assert_func = self.assertNotEqual
+
+ waiters.wait_for_server_status(self.servers_client,
+ server['id'], 'ACTIVE')
+ dst_host = self.admin_servers_client.show_server(
+ server['id'])['server']['OS-EXT-SRV-ATTR:host']
+ assert_func(src_host, dst_host)
+
+ @decorators.idempotent_id('4bf0be52-3b6f-4746-9a27-3143636fe30d')
+ @testtools.skipUnless(CONF.compute_feature_enabled.cold_migration,
+ 'Cold migration not available.')
+ def test_cold_migration(self):
+ self._test_cold_migrate_server(revert=False)
+
+ @decorators.idempotent_id('caa1aa8b-f4ef-4374-be0d-95f001c2ac2d')
+ @testtools.skipUnless(CONF.compute_feature_enabled.cold_migration,
+ 'Cold migration not available.')
+ def test_revert_cold_migration(self):
+ self._test_cold_migrate_server(revert=True)
diff --git a/tempest/api/compute/admin/test_networks.py b/tempest/api/compute/admin/test_networks.py
index e5c8790..87ce39d 100644
--- a/tempest/api/compute/admin/test_networks.py
+++ b/tempest/api/compute/admin/test_networks.py
@@ -15,7 +15,7 @@
from tempest.api.compute import base
from tempest import config
-from tempest import test
+from tempest.lib import decorators
CONF = config.CONF
@@ -24,15 +24,15 @@
"""Tests Nova Networks API that usually requires admin privileges.
API docs:
- http://developer.openstack.org/api-ref-compute-v2-ext.html#ext-os-networks
+ https://developer.openstack.org/api-ref/compute/#networks-os-networks-deprecated
"""
@classmethod
def setup_clients(cls):
super(NetworksTest, cls).setup_clients()
- cls.client = cls.os_adm.compute_networks_client
+ cls.client = cls.os_admin.compute_networks_client
- @test.idempotent_id('d206d211-8912-486f-86e2-a9d090d1f416')
+ @decorators.idempotent_id('d206d211-8912-486f-86e2-a9d090d1f416')
def test_get_network(self):
networks = self.client.list_networks()['networks']
if CONF.compute.fixed_network_name:
@@ -42,14 +42,18 @@
"{0} networks with label {1}".format(
len(configured_network),
CONF.compute.fixed_network_name))
+ elif CONF.network.public_network_id:
+ configured_network = [x for x in networks if x['id'] ==
+ CONF.network.public_network_id]
else:
- configured_network = networks
+ raise self.skipException(
+ "Environment has no known-for-sure existing network.")
configured_network = configured_network[0]
network = (self.client.show_network(configured_network['id'])
['network'])
self.assertEqual(configured_network['label'], network['label'])
- @test.idempotent_id('df3d1046-6fa5-4b2c-ad0c-cfa46a351cb9')
+ @decorators.idempotent_id('df3d1046-6fa5-4b2c-ad0c-cfa46a351cb9')
def test_list_all_networks(self):
networks = self.client.list_networks()['networks']
# Check the configured network is in the list
@@ -57,5 +61,5 @@
configured_network = CONF.compute.fixed_network_name
self.assertIn(configured_network, [x['label'] for x in networks])
else:
- network_name = map(lambda x: x['label'], networks)
- self.assertGreaterEqual(len(network_name), 1)
+ network_labels = [x['label'] for x in networks]
+ self.assertNotEmpty(network_labels)
diff --git a/tempest/api/compute/admin/test_quotas.py b/tempest/api/compute/admin/test_quotas.py
index 2907e26..df534bc 100644
--- a/tempest/api/compute/admin/test_quotas.py
+++ b/tempest/api/compute/admin/test_quotas.py
@@ -14,13 +14,13 @@
# under the License.
from oslo_log import log as logging
-import six
from testtools import matchers
from tempest.api.compute import base
+from tempest.common import identity
from tempest.common import tempest_fixtures as fixtures
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
LOG = logging.getLogger(__name__)
@@ -36,7 +36,7 @@
@classmethod
def setup_clients(cls):
super(QuotasAdminTestJSON, cls).setup_clients()
- cls.adm_client = cls.os_adm.quotas_client
+ cls.adm_client = cls.os_admin.quotas_client
@classmethod
def resource_setup(cls):
@@ -46,15 +46,21 @@
# tenant most of them should be skipped if we can't do that
cls.demo_tenant_id = cls.quotas_client.tenant_id
- cls.default_quota_set = set(('injected_file_content_bytes',
- 'metadata_items', 'injected_files',
- 'ram', 'floating_ips',
- 'fixed_ips', 'key_pairs',
- 'injected_file_path_bytes',
- 'instances', 'security_group_rules',
- 'cores', 'security_groups'))
+ cls.default_quota_set = set(('metadata_items', 'ram', 'key_pairs',
+ 'instances', 'cores',
+ 'server_group_members', 'server_groups'))
+ if cls.is_requested_microversion_compatible('2.35'):
+ cls.default_quota_set = \
+ cls.default_quota_set | set(['fixed_ips', 'floating_ips',
+ 'security_group_rules',
+ 'security_groups'])
+ if cls.is_requested_microversion_compatible('2.56'):
+ cls.default_quota_set = \
+ cls.default_quota_set | set(['injected_file_content_bytes',
+ 'injected_file_path_bytes',
+ 'injected_files'])
- @test.idempotent_id('3b0a7c8f-cf58-46b8-a60c-715a32a8ba7d')
+ @decorators.idempotent_id('3b0a7c8f-cf58-46b8-a60c-715a32a8ba7d')
def test_get_default_quotas(self):
# Admin can get the default resource quota set for a tenant
expected_quota_set = self.default_quota_set | set(['id'])
@@ -64,17 +70,24 @@
for quota in expected_quota_set:
self.assertIn(quota, quota_set.keys())
- @test.idempotent_id('55fbe2bf-21a9-435b-bbd2-4162b0ed799a')
+ @decorators.idempotent_id('55fbe2bf-21a9-435b-bbd2-4162b0ed799a')
def test_update_all_quota_resources_for_tenant(self):
# Admin can update all the resource quota limits for a tenant
default_quota_set = self.adm_client.show_default_quota_set(
self.demo_tenant_id)['quota_set']
- new_quota_set = {'injected_file_content_bytes': 20480,
- 'metadata_items': 256, 'injected_files': 10,
- 'ram': 10240, 'floating_ips': 20, 'fixed_ips': 10,
- 'key_pairs': 200, 'injected_file_path_bytes': 512,
- 'instances': 20, 'security_group_rules': 20,
- 'cores': 2, 'security_groups': 20}
+ new_quota_set = {'metadata_items': 256, 'ram': 10240,
+ 'key_pairs': 200, 'instances': 20,
+ 'server_groups': 20,
+ 'server_group_members': 20, 'cores': 2}
+ if self.is_requested_microversion_compatible('2.35'):
+ new_quota_set.update({'fixed_ips': 10, 'floating_ips': 20,
+ 'security_group_rules': 20,
+ 'security_groups': 20})
+ if self.is_requested_microversion_compatible('2.56'):
+ new_quota_set.update({'injected_file_content_bytes': 20480,
+ 'injected_file_path_bytes': 512,
+ 'injected_files': 10})
+
# Update limits for all quota resources
quota_set = self.adm_client.update_quota_set(
self.demo_tenant_id,
@@ -82,28 +95,22 @@
**new_quota_set)['quota_set']
default_quota_set.pop('id')
- # NOTE(PhilDay) The following is safe as we're not updating these
- # two quota values yet. Once the Nova change to add these is merged
- # and the client updated to support them this can be removed
- if 'server_groups' in default_quota_set:
- default_quota_set.pop('server_groups')
- if 'server_group_members' in default_quota_set:
- default_quota_set.pop('server_group_members')
self.addCleanup(self.adm_client.update_quota_set,
self.demo_tenant_id, **default_quota_set)
for quota in new_quota_set:
self.assertIn(quota, quota_set.keys())
# TODO(afazekas): merge these test cases
- @test.idempotent_id('ce9e0815-8091-4abd-8345-7fe5b85faa1d')
+ @decorators.idempotent_id('ce9e0815-8091-4abd-8345-7fe5b85faa1d')
def test_get_updated_quotas(self):
# Verify that GET shows the updated quota set of project
project_name = data_utils.rand_name('cpu_quota_project')
project_desc = project_name + '-desc'
- project = self.identity_utils.create_project(name=project_name,
- description=project_desc)
+ project = identity.identity_utils(self.os_admin).create_project(
+ name=project_name, description=project_desc)
project_id = project['id']
- self.addCleanup(self.identity_utils.delete_project, project_id)
+ self.addCleanup(identity.identity_utils(self.os_admin).delete_project,
+ project_id)
self.adm_client.update_quota_set(project_id, ram='5120')
quota_set = self.adm_client.show_quota_set(project_id)['quota_set']
@@ -113,14 +120,12 @@
user_name = data_utils.rand_name('cpu_quota_user')
password = data_utils.rand_password()
email = user_name + '@testmail.tm'
- user = self.identity_utils.create_user(username=user_name,
- password=password,
- project=project,
- email=email)
- if 'user' in user:
- user = user['user']
+ user = identity.identity_utils(self.os_admin).create_user(
+ username=user_name, password=password, project=project,
+ email=email)
user_id = user['id']
- self.addCleanup(self.identity_utils.delete_user, user_id)
+ self.addCleanup(identity.identity_utils(self.os_admin).delete_user,
+ user_id)
self.adm_client.update_quota_set(project_id,
user_id=user_id,
@@ -129,15 +134,16 @@
project_id, user_id=user_id)['quota_set']
self.assertEqual(2048, quota_set['ram'])
- @test.idempotent_id('389d04f0-3a41-405f-9317-e5f86e3c44f0')
+ @decorators.idempotent_id('389d04f0-3a41-405f-9317-e5f86e3c44f0')
def test_delete_quota(self):
# Admin can delete the resource quota set for a project
project_name = data_utils.rand_name('ram_quota_project')
project_desc = project_name + '-desc'
- project = self.identity_utils.create_project(name=project_name,
- description=project_desc)
+ project = identity.identity_utils(self.os_admin).create_project(
+ name=project_name, description=project_desc)
project_id = project['id']
- self.addCleanup(self.identity_utils.delete_project, project_id)
+ self.addCleanup(identity.identity_utils(self.os_admin).delete_project,
+ project_id)
quota_set_default = (self.adm_client.show_quota_set(project_id)
['quota_set'])
ram_default = quota_set_default['ram']
@@ -162,30 +168,28 @@
@classmethod
def resource_setup(cls):
super(QuotaClassesAdminTestJSON, cls).resource_setup()
- cls.adm_client = cls.os_adm.quota_classes_client
+ cls.adm_client = cls.os_admin.quota_classes_client
def _restore_default_quotas(self, original_defaults):
LOG.debug("restoring quota class defaults")
- self.adm_client.update_quota_class_set(
- 'default', **original_defaults)['quota_class_set']
+ self.adm_client.update_quota_class_set('default', **original_defaults)
# NOTE(sdague): this test is problematic as it changes
# global state, and possibly needs to be part of a set of
# tests that get run all by themselves at the end under a
# 'danger' flag.
- @test.idempotent_id('7932ab0f-5136-4075-b201-c0e2338df51a')
+ @decorators.idempotent_id('7932ab0f-5136-4075-b201-c0e2338df51a')
def test_update_default_quotas(self):
LOG.debug("get the current 'default' quota class values")
body = (self.adm_client.show_quota_class_set('default')
['quota_class_set'])
- self.assertIn('id', body)
self.assertEqual('default', body.pop('id'))
# restore the defaults when the test is done
self.addCleanup(self._restore_default_quotas, body.copy())
# increment all of the values for updating the default quota class
- for quota, default in six.iteritems(body):
+ for quota, default in body.items():
# NOTE(sdague): we need to increment a lot, otherwise
- # there is a real chance that we go from -1 (unlimitted)
+ # there is a real chance that we go from -1 (unlimited)
# to a very small number which causes issues.
body[quota] = default + 100
LOG.debug("update limits for the default quota class set")
diff --git a/tempest/api/compute/admin/test_quotas_negative.py b/tempest/api/compute/admin/test_quotas_negative.py
index bdbfde4..f90ff92 100644
--- a/tempest/api/compute/admin/test_quotas_negative.py
+++ b/tempest/api/compute/admin/test_quotas_negative.py
@@ -12,37 +12,50 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import decorators
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.compute import base
-from tempest.common.utils import data_utils
+from tempest.common import utils
from tempest import config
-from tempest import test
+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 QuotasAdminNegativeTestJSON(base.BaseV2ComputeAdminTest):
+class QuotasAdminNegativeTestBase(base.BaseV2ComputeAdminTest):
force_tenant_isolation = True
@classmethod
def setup_clients(cls):
- super(QuotasAdminNegativeTestJSON, cls).setup_clients()
- cls.client = cls.os.quotas_client
- cls.adm_client = cls.os_adm.quotas_client
+ super(QuotasAdminNegativeTestBase, cls).setup_clients()
+ cls.client = cls.os_primary.quotas_client
+ cls.adm_client = cls.os_admin.quotas_client
cls.sg_client = cls.security_groups_client
cls.sgr_client = cls.security_group_rules_client
@classmethod
def resource_setup(cls):
- super(QuotasAdminNegativeTestJSON, cls).resource_setup()
+ super(QuotasAdminNegativeTestBase, cls).resource_setup()
# NOTE(afazekas): these test cases should always create and use a new
# tenant most of them should be skipped if we can't do that
cls.demo_tenant_id = cls.client.tenant_id
- @test.attr(type=['negative'])
- @test.idempotent_id('733abfe8-166e-47bb-8363-23dbd7ff3476')
+ def _update_quota(self, quota_item, quota_value):
+ quota_set = (self.adm_client.show_quota_set(self.demo_tenant_id)
+ ['quota_set'])
+ default_quota_value = quota_set[quota_item]
+
+ self.adm_client.update_quota_set(self.demo_tenant_id,
+ force=True,
+ **{quota_item: quota_value})
+ self.addCleanup(self.adm_client.update_quota_set, self.demo_tenant_id,
+ **{quota_item: default_quota_value})
+
+
+class QuotasAdminNegativeTest(QuotasAdminNegativeTestBase):
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('733abfe8-166e-47bb-8363-23dbd7ff3476')
def test_update_quota_normal_user(self):
self.assertRaises(lib_exc.Forbidden,
self.client.update_quota_set,
@@ -51,81 +64,45 @@
# TODO(afazekas): Add dedicated tenant to the skipped quota tests.
# It can be moved into the setUpClass as well.
- @test.attr(type=['negative'])
- @test.idempotent_id('91058876-9947-4807-9f22-f6eb17140d9b')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('91058876-9947-4807-9f22-f6eb17140d9b')
def test_create_server_when_cpu_quota_is_full(self):
# Disallow server creation when tenant's vcpu quota is full
- quota_set = (self.adm_client.show_quota_set(self.demo_tenant_id)
- ['quota_set'])
- default_vcpu_quota = quota_set['cores']
- vcpu_quota = 0 # Set the quota to zero to conserve resources
-
- self.adm_client.update_quota_set(self.demo_tenant_id,
- force=True,
- cores=vcpu_quota)
-
- self.addCleanup(self.adm_client.update_quota_set, self.demo_tenant_id,
- cores=default_vcpu_quota)
+ self._update_quota('cores', 0)
self.assertRaises((lib_exc.Forbidden, lib_exc.OverLimit),
self.create_test_server)
- @test.attr(type=['negative'])
- @test.idempotent_id('6fdd7012-584d-4327-a61c-49122e0d5864')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('6fdd7012-584d-4327-a61c-49122e0d5864')
def test_create_server_when_memory_quota_is_full(self):
# Disallow server creation when tenant's memory quota is full
- quota_set = (self.adm_client.show_quota_set(self.demo_tenant_id)
- ['quota_set'])
- default_mem_quota = quota_set['ram']
- mem_quota = 0 # Set the quota to zero to conserve resources
-
- self.adm_client.update_quota_set(self.demo_tenant_id,
- force=True,
- ram=mem_quota)
-
- self.addCleanup(self.adm_client.update_quota_set, self.demo_tenant_id,
- ram=default_mem_quota)
+ self._update_quota('ram', 0)
self.assertRaises((lib_exc.Forbidden, lib_exc.OverLimit),
self.create_test_server)
- @test.attr(type=['negative'])
- @test.idempotent_id('7c6be468-0274-449a-81c3-ac1c32ee0161')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('7c6be468-0274-449a-81c3-ac1c32ee0161')
def test_create_server_when_instances_quota_is_full(self):
# Once instances quota limit is reached, disallow server creation
- quota_set = (self.adm_client.show_quota_set(self.demo_tenant_id)
- ['quota_set'])
- default_instances_quota = quota_set['instances']
- instances_quota = 0 # Set quota to zero to disallow server creation
-
- self.adm_client.update_quota_set(self.demo_tenant_id,
- force=True,
- instances=instances_quota)
- self.addCleanup(self.adm_client.update_quota_set, self.demo_tenant_id,
- instances=default_instances_quota)
+ self._update_quota('instances', 0)
self.assertRaises((lib_exc.Forbidden, lib_exc.OverLimit),
self.create_test_server)
+
+class QuotasSecurityGroupAdminNegativeTest(QuotasAdminNegativeTestBase):
+ max_microversion = '2.35'
+
@decorators.skip_because(bug="1186354",
condition=CONF.service_available.neutron)
- @test.idempotent_id('7c6c8f3b-2bf6-4918-b240-57b136a66aa0')
- @test.services('network')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('7c6c8f3b-2bf6-4918-b240-57b136a66aa0')
+ @utils.services('network')
def test_security_groups_exceed_limit(self):
# Negative test: Creation Security Groups over limit should FAIL
-
- quota_set = (self.adm_client.show_quota_set(self.demo_tenant_id)
- ['quota_set'])
- default_sg_quota = quota_set['security_groups']
-
# Set the quota to number of used security groups
sg_quota = self.limits_client.show_limits()['limits']['absolute'][
'totalSecurityGroupsUsed']
-
- self.adm_client.update_quota_set(self.demo_tenant_id,
- force=True,
- security_groups=sg_quota)
-
- self.addCleanup(self.adm_client.update_quota_set,
- self.demo_tenant_id,
- security_groups=default_sg_quota)
+ self._update_quota('security_groups', sg_quota)
# Check we cannot create anymore
# A 403 Forbidden or 413 Overlimit (old behaviour) exception
@@ -136,25 +113,13 @@
@decorators.skip_because(bug="1186354",
condition=CONF.service_available.neutron)
- @test.attr(type=['negative'])
- @test.idempotent_id('6e9f436d-f1ed-4f8e-a493-7275dfaa4b4d')
- @test.services('network')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('6e9f436d-f1ed-4f8e-a493-7275dfaa4b4d')
+ @utils.services('network')
def test_security_groups_rules_exceed_limit(self):
# Negative test: Creation of Security Group Rules should FAIL
# when we reach limit maxSecurityGroupRules
-
- quota_set = (self.adm_client.show_quota_set(self.demo_tenant_id)
- ['quota_set'])
- default_sg_rules_quota = quota_set['security_group_rules']
- sg_rules_quota = 0 # Set the quota to zero to conserve resources
-
- self.adm_client.update_quota_set(self.demo_tenant_id,
- force=True,
- security_group_rules=sg_rules_quota)
-
- self.addCleanup(self.adm_client.update_quota_set,
- self.demo_tenant_id,
- security_group_rules=default_sg_rules_quota)
+ self._update_quota('security_group_rules', 0)
s_name = data_utils.rand_name('securitygroup')
s_description = data_utils.rand_name('description')
diff --git a/tempest/api/compute/admin/test_security_group_default_rules.py b/tempest/api/compute/admin/test_security_group_default_rules.py
index 74f3caa..bca6a22 100644
--- a/tempest/api/compute/admin/test_security_group_default_rules.py
+++ b/tempest/api/compute/admin/test_security_group_default_rules.py
@@ -12,17 +12,18 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
import testtools
from tempest.api.compute import base
from tempest import config
-from tempest import test
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
CONF = config.CONF
class SecurityGroupDefaultRulesTest(base.BaseV2ComputeAdminTest):
+ max_microversion = '2.35'
@classmethod
# TODO(GMann): Once Bug# 1311500 is fixed, these test can run
@@ -38,7 +39,7 @@
@classmethod
def setup_clients(cls):
super(SecurityGroupDefaultRulesTest, cls).setup_clients()
- cls.adm_client = cls.os_adm.security_group_default_rules_client
+ cls.adm_client = cls.os_admin.security_group_default_rules_client
def _create_security_group_default_rules(self, ip_protocol='tcp',
from_port=22, to_port=22,
@@ -55,7 +56,7 @@
self.assertEqual(cidr, rule['ip_range']['cidr'])
return rule
- @test.idempotent_id('6d880615-eec3-4d29-97c5-7a074dde239d')
+ @decorators.idempotent_id('6d880615-eec3-4d29-97c5-7a074dde239d')
def test_create_delete_security_group_default_rules(self):
# Create and delete Security Group default rule
ip_protocols = ['tcp', 'udp', 'icmp']
@@ -67,7 +68,7 @@
self.adm_client.show_security_group_default_rule,
rule['id'])
- @test.idempotent_id('4d752e0a-33a1-4c3a-b498-ff8667ca22e5')
+ @decorators.idempotent_id('4d752e0a-33a1-4c3a-b498-ff8667ca22e5')
def test_create_security_group_default_rule_without_cidr(self):
ip_protocol = 'udp'
from_port = 80
@@ -81,7 +82,7 @@
self.assertNotEqual(0, rule['id'])
self.assertEqual('0.0.0.0/0', rule['ip_range']['cidr'])
- @test.idempotent_id('29f2d218-69b0-4a95-8f3d-6bd0ef732b3a')
+ @decorators.idempotent_id('29f2d218-69b0-4a95-8f3d-6bd0ef732b3a')
def test_create_security_group_default_rule_with_blank_cidr(self):
ip_protocol = 'icmp'
from_port = 10
@@ -97,7 +98,7 @@
self.assertNotEqual(0, rule['id'])
self.assertEqual('0.0.0.0/0', rule['ip_range']['cidr'])
- @test.idempotent_id('6e6de55e-9146-4ae0-89f2-3569586e0b9b')
+ @decorators.idempotent_id('6e6de55e-9146-4ae0-89f2-3569586e0b9b')
def test_security_group_default_rules_list(self):
ip_protocol = 'tcp'
from_port = 22
@@ -111,10 +112,10 @@
rule['id'])
rules = (self.adm_client.list_security_group_default_rules()
['security_group_default_rules'])
- self.assertNotEqual(0, len(rules))
+ self.assertNotEmpty(rules)
self.assertIn(rule, rules)
- @test.idempotent_id('15cbb349-86b4-4f71-a048-04b7ef3f150b')
+ @decorators.idempotent_id('15cbb349-86b4-4f71-a048-04b7ef3f150b')
def test_default_security_group_default_rule_show(self):
ip_protocol = 'tcp'
from_port = 22
diff --git a/tempest/api/compute/admin/test_security_groups.py b/tempest/api/compute/admin/test_security_groups.py
index 1494745..f0178aa 100644
--- a/tempest/api/compute/admin/test_security_groups.py
+++ b/tempest/api/compute/admin/test_security_groups.py
@@ -14,19 +14,18 @@
# under the License.
from tempest.api.compute import base
-from tempest.common.utils import data_utils
-from tempest import config
-from tempest import test
-
-CONF = config.CONF
+from tempest.common import utils
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
class SecurityGroupsTestAdminJSON(base.BaseV2ComputeAdminTest):
+ max_microversion = '2.35'
@classmethod
def setup_clients(cls):
super(SecurityGroupsTestAdminJSON, cls).setup_clients()
- cls.adm_client = cls.os_adm.compute_security_groups_client
+ cls.adm_client = cls.os_admin.compute_security_groups_client
cls.client = cls.security_groups_client
def _delete_security_group(self, securitygroup_id, admin=True):
@@ -35,14 +34,14 @@
else:
self.client.delete_security_group(securitygroup_id)
- @test.idempotent_id('49667619-5af9-4c63-ab5d-2cfdd1c8f7f1')
- @test.services('network')
+ @decorators.idempotent_id('49667619-5af9-4c63-ab5d-2cfdd1c8f7f1')
+ @utils.services('network')
def test_list_security_groups_list_all_tenants_filter(self):
# Admin can list security groups of all tenants
# List of all security groups created
security_group_list = []
# Create two security groups for a non-admin tenant
- for i in range(2):
+ for _ in range(2):
name = data_utils.rand_name('securitygroup')
description = data_utils.rand_name('description')
securitygroup = self.client.create_security_group(
@@ -53,7 +52,7 @@
client_tenant_id = securitygroup['tenant_id']
# Create two security groups for admin tenant
- for i in range(2):
+ for _ in range(2):
name = data_utils.rand_name('securitygroup')
description = data_utils.rand_name('description')
adm_securitygroup = self.adm_client.create_security_group(
@@ -65,7 +64,7 @@
# Fetch all security groups based on 'all_tenants' search filter
fetched_list = self.adm_client.list_security_groups(
all_tenants='true')['security_groups']
- sec_group_id_list = map(lambda sg: sg['id'], fetched_list)
+ sec_group_id_list = [sg['id'] for sg in fetched_list]
# Now check if all created Security Groups are present in fetched list
for sec_group in security_group_list:
self.assertIn(sec_group['id'], sec_group_id_list)
diff --git a/tempest/api/compute/admin/test_server_diagnostics.py b/tempest/api/compute/admin/test_server_diagnostics.py
new file mode 100644
index 0000000..005efdd
--- /dev/null
+++ b/tempest/api/compute/admin/test_server_diagnostics.py
@@ -0,0 +1,76 @@
+# Copyright 2017 Mirantis Inc.
+#
+# 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 time
+
+from tempest.api.compute import base
+from tempest.lib import decorators
+
+
+class ServerDiagnosticsTest(base.BaseV2ComputeAdminTest):
+ min_microversion = None
+ max_microversion = '2.47'
+
+ @classmethod
+ def setup_clients(cls):
+ super(ServerDiagnosticsTest, cls).setup_clients()
+ cls.client = cls.os_admin.servers_client
+
+ @decorators.idempotent_id('31ff3486-b8a0-4f56-a6c0-aab460531db3')
+ def test_get_server_diagnostics(self):
+ server_id = self.create_test_server(wait_until='ACTIVE')['id']
+ diagnostics = self.client.show_server_diagnostics(server_id)
+
+ # NOTE(snikitin): Before microversion 2.48 response data from each
+ # hypervisor (libvirt, xen, vmware) was different. None of the fields
+ # were equal. As this test is common for libvirt, xen and vmware CI
+ # jobs we can't check any field in the response because all fields are
+ # different.
+ self.assertNotEmpty(diagnostics)
+
+
+class ServerDiagnosticsV248Test(base.BaseV2ComputeAdminTest):
+ min_microversion = '2.48'
+ max_microversion = 'latest'
+
+ @classmethod
+ def setup_clients(cls):
+ super(ServerDiagnosticsV248Test, cls).setup_clients()
+ cls.client = cls.os_admin.servers_client
+
+ @decorators.idempotent_id('64d0d48c-dff1-11e6-bf01-fe55135034f3')
+ def test_get_server_diagnostics(self):
+ server_id = self.create_test_server(wait_until='ACTIVE')['id']
+ # Response status and filed types will be checked by json schema
+ self.client.show_server_diagnostics(server_id)
+
+ # NOTE(snikitin): This is a special case for Xen hypervisor. In Xen
+ # case we're getting diagnostics stats from the RRDs which are updated
+ # every 5 seconds. It means that diagnostics information may be
+ # incomplete during first 5 seconds of VM life. In such cases methods
+ # which get diagnostics stats from Xen may raise exceptions or
+ # return `NaN` values. Such behavior must be handled correctly.
+ # Response must contain all diagnostics fields (may be with `None`
+ # values) and response status must be 200. Line above checks it by
+ # json schema.
+ time.sleep(10)
+ diagnostics = self.client.show_server_diagnostics(server_id)
+
+ # NOTE(snikitin): After 10 seconds diagnostics fields must contain
+ # not None values. But we will check only "memory_details.maximum"
+ # field because only this field meets all the following conditions:
+ # 1) This field may be unset because of Xen 5 seconds timeout.
+ # 2) This field is present in responses from all three supported
+ # hypervisors (libvirt, xen, vmware).
+ self.assertIsNotNone(diagnostics['memory_details']['maximum'])
diff --git a/tempest/api/compute/admin/test_server_diagnostics_negative.py b/tempest/api/compute/admin/test_server_diagnostics_negative.py
new file mode 100644
index 0000000..d5b6674
--- /dev/null
+++ b/tempest/api/compute/admin/test_server_diagnostics_negative.py
@@ -0,0 +1,40 @@
+# Copyright 2017 Mirantis Inc.
+#
+# 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 import decorators
+from tempest.lib import exceptions as lib_exc
+
+
+class ServerDiagnosticsNegativeTest(base.BaseV2ComputeAdminTest):
+ min_microversion = None
+ max_microversion = '2.47'
+
+ @classmethod
+ def setup_clients(cls):
+ super(ServerDiagnosticsNegativeTest, cls).setup_clients()
+ cls.client = cls.servers_client
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('e84e2234-60d2-42fa-8b30-e2d3049724ac')
+ def test_get_server_diagnostics_by_non_admin(self):
+ # Non-admin user cannot view server diagnostics according to policy
+ server_id = self.create_test_server(wait_until='ACTIVE')['id']
+ self.assertRaises(lib_exc.Forbidden,
+ self.client.show_server_diagnostics, server_id)
+
+
+class ServerDiagnosticsNegativeV248Test(ServerDiagnosticsNegativeTest):
+ min_microversion = '2.48'
+ max_microversion = 'latest'
diff --git a/tempest/api/compute/admin/test_servers.py b/tempest/api/compute/admin/test_servers.py
index fd6f105..cdfc44a 100644
--- a/tempest/api/compute/admin/test_servers.py
+++ b/tempest/api/compute/admin/test_servers.py
@@ -12,49 +12,38 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import decorators
-
from tempest.api.compute import base
-from tempest.common import fixed_network
-from tempest.common.utils import data_utils
from tempest.common import waiters
-from tempest import test
+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):
"""Tests Servers API using admin privileges"""
- _host_key = 'OS-EXT-SRV-ATTR:host'
-
@classmethod
def setup_clients(cls):
super(ServersAdminTestJSON, cls).setup_clients()
- cls.client = cls.os_adm.servers_client
+ cls.client = cls.os_admin.servers_client
cls.non_admin_client = cls.servers_client
- cls.flavors_client = cls.os_adm.flavors_client
@classmethod
def resource_setup(cls):
super(ServersAdminTestJSON, cls).resource_setup()
- cls.s1_name = data_utils.rand_name('server')
- server = cls.create_test_server(name=cls.s1_name,
- wait_until='ACTIVE')
+ cls.s1_name = data_utils.rand_name(cls.__name__ + '-server')
+ server = cls.create_test_server(name=cls.s1_name)
cls.s1_id = server['id']
- cls.s2_name = data_utils.rand_name('server')
+ cls.s2_name = data_utils.rand_name(cls.__name__ + '-server')
server = cls.create_test_server(name=cls.s2_name,
wait_until='ACTIVE')
cls.s2_id = server['id']
+ waiters.wait_for_server_status(cls.non_admin_client,
+ cls.s1_id, 'ACTIVE')
- @test.idempotent_id('51717b38-bdc1-458b-b636-1cf82d99f62f')
- def test_list_servers_by_admin(self):
- # Listing servers by admin user returns empty list by default
- body = self.client.list_servers(detail=True)
- servers = body['servers']
- self.assertEqual([], servers)
-
- @test.idempotent_id('06f960bb-15bb-48dc-873d-f96e89be7870')
+ @decorators.idempotent_id('06f960bb-15bb-48dc-873d-f96e89be7870')
def test_list_servers_filter_by_error_status(self):
# Filter the list of servers by server error status
params = {'status': 'error'}
@@ -70,19 +59,44 @@
self.assertIn(self.s1_id, map(lambda x: x['id'], servers))
self.assertNotIn(self.s2_id, map(lambda x: x['id'], servers))
- @test.idempotent_id('9f5579ae-19b4-4985-a091-2a5d56106580')
+ @decorators.idempotent_id('d56e9540-73ed-45e0-9b88-98fc419087eb')
+ def test_list_servers_detailed_filter_by_invalid_status(self):
+ params = {'status': 'invalid_status'}
+ 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):
+ # Listing servers by admin user returns a list which doesn't
+ # contain the other tenants' server by default
+ body = self.client.list_servers(detail=True)
+ servers = body['servers']
+
+ # This case is for the test environments which contain
+ # the existing servers before testing
+ servers_name = [server['name'] for server in servers]
+ self.assertNotIn(self.s1_name, servers_name)
+ self.assertNotIn(self.s2_name, servers_name)
+
+ @decorators.idempotent_id('9f5579ae-19b4-4985-a091-2a5d56106580')
def test_list_servers_by_admin_with_all_tenants(self):
# Listing servers by admin user with all tenants parameter
# Here should be listed all servers
params = {'all_tenants': ''}
body = self.client.list_servers(detail=True, **params)
servers = body['servers']
- servers_name = map(lambda x: x['name'], servers)
+ servers_name = [server['name'] for server in servers]
self.assertIn(self.s1_name, servers_name)
self.assertIn(self.s2_name, servers_name)
- @test.idempotent_id('7e5d6b8f-454a-4ba1-8ae2-da857af8338b')
+ @decorators.related_bug('1659811')
+ @decorators.idempotent_id('7e5d6b8f-454a-4ba1-8ae2-da857af8338b')
def test_list_servers_by_admin_with_specified_tenant(self):
# In nova v2, tenant_id is ignored unless all_tenants is specified
@@ -91,43 +105,44 @@
params = {'tenant_id': tenant_id}
body = self.client.list_servers(detail=True, **params)
servers = body['servers']
- self.assertEqual([], servers)
+ servers_name = [x['name'] for x in servers]
+ self.assertNotIn(self.s1_name, servers_name)
+ self.assertNotIn(self.s2_name, servers_name)
- # List the admin tenant which has no servers
+ # List the primary tenant with all_tenants is specified
+ params = {'all_tenants': '', 'tenant_id': tenant_id}
+ body = self.client.list_servers(detail=True, **params)
+ servers = body['servers']
+ servers_name = [x['name'] for x in servers]
+ self.assertIn(self.s1_name, servers_name)
+ self.assertIn(self.s2_name, servers_name)
+
+ # List the admin tenant shouldn't get servers created by other tenants
admin_tenant_id = self.client.tenant_id
params = {'all_tenants': '', 'tenant_id': admin_tenant_id}
body = self.client.list_servers(detail=True, **params)
servers = body['servers']
- self.assertEqual([], servers)
+ servers_name = [x['name'] for x in servers]
+ self.assertNotIn(self.s1_name, servers_name)
+ self.assertNotIn(self.s2_name, servers_name)
- @test.idempotent_id('86c7a8f7-50cf-43a9-9bac-5b985317134f')
+ @decorators.idempotent_id('86c7a8f7-50cf-43a9-9bac-5b985317134f')
def test_list_servers_filter_by_exist_host(self):
# Filter the list of servers by existent host
- name = data_utils.rand_name('server')
- flavor = self.flavor_ref
- image_id = self.image_ref
- network = self.get_tenant_network()
- network_kwargs = fixed_network.set_networks_kwarg(network)
- test_server = self.client.create_server(name=name, imageRef=image_id,
- flavorRef=flavor,
- **network_kwargs)['server']
- self.addCleanup(self.client.delete_server, test_server['id'])
- waiters.wait_for_server_status(self.client,
- test_server['id'], 'ACTIVE')
- server = self.client.show_server(test_server['id'])['server']
- self.assertEqual(server['status'], 'ACTIVE')
- hostname = server[self._host_key]
- params = {'host': hostname}
- body = self.client.list_servers(**params)
- servers = body['servers']
- nonexistent_params = {'host': 'nonexistent_host'}
+ server = self.client.show_server(self.s1_id)['server']
+ hostname = server['OS-EXT-SRV-ATTR:host']
+ params = {'host': hostname, 'all_tenants': '1'}
+ servers = self.client.list_servers(**params)['servers']
+ self.assertIn(server['id'], map(lambda x: x['id'], servers))
+
+ nonexistent_params = {'host': 'nonexistent_host',
+ 'all_tenants': '1'}
nonexistent_body = self.client.list_servers(**nonexistent_params)
nonexistent_servers = nonexistent_body['servers']
- self.assertIn(test_server['id'], map(lambda x: x['id'], servers))
- self.assertNotIn(test_server['id'],
+ self.assertNotIn(server['id'],
map(lambda x: x['id'], nonexistent_servers))
- @test.idempotent_id('ee8ae470-db70-474d-b752-690b7892cab1')
+ @decorators.idempotent_id('ee8ae470-db70-474d-b752-690b7892cab1')
def test_reset_state_server(self):
# Reset server's state to 'error'
self.client.reset_state(self.s1_id, state='error')
@@ -143,18 +158,7 @@
server = self.client.show_server(self.s1_id)['server']
self.assertEqual(server['status'], 'ACTIVE')
- @decorators.skip_because(bug="1240043")
- @test.idempotent_id('31ff3486-b8a0-4f56-a6c0-aab460531db3')
- def test_get_server_diagnostics_by_admin(self):
- # Retrieve server diagnostics by admin user
- diagnostic = self.client.show_server_diagnostics(self.s1_id)
- basic_attrs = ['rx_packets', 'rx_errors', 'rx_drop',
- 'tx_packets', 'tx_errors', 'tx_drop',
- 'read_req', 'write_req', 'cpu', 'memory']
- for key in basic_attrs:
- self.assertIn(key, str(diagnostic.keys()))
-
- @test.idempotent_id('682cb127-e5bb-4f53-87ce-cb9003604442')
+ @decorators.idempotent_id('682cb127-e5bb-4f53-87ce-cb9003604442')
def test_rebuild_server_in_error_state(self):
# The server in error state should be rebuilt using the provided
# image and changed to ACTIVE state
@@ -182,7 +186,7 @@
rebuilt_image_id = server['image']['id']
self.assertEqual(self.image_ref_alt, rebuilt_image_id)
- @test.idempotent_id('7a1323b4-a6a2-497a-96cb-76c07b945c71')
+ @decorators.idempotent_id('7a1323b4-a6a2-497a-96cb-76c07b945c71')
def test_reset_network_inject_network_info(self):
# Reset Network of a Server
server = self.create_test_server(wait_until='ACTIVE')
@@ -190,7 +194,7 @@
# Inject the Network Info into Server
self.client.inject_network_info(server['id'])
- @test.idempotent_id('fdcd9b33-0903-4e00-a1f7-b5f6543068d6')
+ @decorators.idempotent_id('fdcd9b33-0903-4e00-a1f7-b5f6543068d6')
def test_create_server_with_scheduling_hint(self):
# Create a server with scheduler hints.
hints = {
diff --git a/tempest/api/compute/admin/test_servers_negative.py b/tempest/api/compute/admin/test_servers_negative.py
index 055dc1b..f720b84 100644
--- a/tempest/api/compute/admin/test_servers_negative.py
+++ b/tempest/api/compute/admin/test_servers_negative.py
@@ -12,17 +12,15 @@
# License for the specific language governing permissions and limitations
# under the License.
-import uuid
-
-from tempest_lib import exceptions as lib_exc
import testtools
from tempest.api.compute import base
from tempest.common import tempest_fixtures as fixtures
-from tempest.common.utils import data_utils
from tempest.common import waiters
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
CONF = config.CONF
@@ -33,120 +31,95 @@
@classmethod
def setup_clients(cls):
super(ServersAdminNegativeTestJSON, cls).setup_clients()
- cls.client = cls.os_adm.servers_client
- cls.non_adm_client = cls.servers_client
- cls.flavors_client = cls.os_adm.flavors_client
+ cls.client = cls.os_admin.servers_client
+ cls.quotas_client = cls.os_admin.quotas_client
@classmethod
def resource_setup(cls):
super(ServersAdminNegativeTestJSON, cls).resource_setup()
cls.tenant_id = cls.client.tenant_id
- cls.s1_name = data_utils.rand_name('server')
- server = cls.create_test_server(name=cls.s1_name,
- wait_until='ACTIVE')
+ server = cls.create_test_server(wait_until='ACTIVE')
cls.s1_id = server['id']
- def _get_unused_flavor_id(self):
- flavor_id = data_utils.rand_int_id(start=1000)
- while True:
- try:
- self.flavors_client.show_flavor(flavor_id)
- except lib_exc.NotFound:
- break
- flavor_id = data_utils.rand_int_id(start=1000)
- return flavor_id
-
- @test.idempotent_id('28dcec23-f807-49da-822c-56a92ea3c687')
+ @decorators.idempotent_id('28dcec23-f807-49da-822c-56a92ea3c687')
@testtools.skipUnless(CONF.compute_feature_enabled.resize,
'Resize not available.')
- @test.attr(type=['negative'])
+ @decorators.attr(type=['negative'])
def test_resize_server_using_overlimit_ram(self):
# NOTE(mriedem): Avoid conflicts with os-quota-class-sets tests.
self.useFixture(fixtures.LockFixture('compute_quotas'))
- flavor_name = data_utils.rand_name("flavor")
- flavor_id = self._get_unused_flavor_id()
- quota_set = (self.quotas_client.show_default_quota_set(self.tenant_id)
- ['quota_set'])
- ram = int(quota_set['ram']) + 1
- vcpus = 8
- disk = 10
- flavor_ref = self.flavors_client.create_flavor(name=flavor_name,
- ram=ram, vcpus=vcpus,
- disk=disk,
- id=flavor_id)['flavor']
- self.addCleanup(self.flavors_client.delete_flavor, flavor_id)
+ quota_set = self.quotas_client.show_quota_set(
+ self.tenant_id)['quota_set']
+ ram = quota_set['ram']
+ if ram == -1:
+ raise self.skipException("ram quota set is -1,"
+ " cannot test overlimit")
+ ram += 1
+ vcpus = 1
+ disk = 5
+ flavor_ref = self.create_flavor(ram=ram, vcpus=vcpus, disk=disk)
self.assertRaises((lib_exc.Forbidden, lib_exc.OverLimit),
self.client.resize_server,
- self.servers[0]['id'],
+ self.s1_id,
flavor_ref['id'])
- @test.idempotent_id('7368a427-2f26-4ad9-9ba9-911a0ec2b0db')
+ @decorators.idempotent_id('7368a427-2f26-4ad9-9ba9-911a0ec2b0db')
@testtools.skipUnless(CONF.compute_feature_enabled.resize,
'Resize not available.')
- @test.attr(type=['negative'])
+ @decorators.attr(type=['negative'])
def test_resize_server_using_overlimit_vcpus(self):
# NOTE(mriedem): Avoid conflicts with os-quota-class-sets tests.
self.useFixture(fixtures.LockFixture('compute_quotas'))
- flavor_name = data_utils.rand_name("flavor")
- flavor_id = self._get_unused_flavor_id()
+ quota_set = self.quotas_client.show_quota_set(
+ self.tenant_id)['quota_set']
+ vcpus = quota_set['cores']
+ if vcpus == -1:
+ raise self.skipException("cores quota set is -1,"
+ " cannot test overlimit")
+ vcpus += 1
ram = 512
- quota_set = (self.quotas_client.show_default_quota_set(self.tenant_id)
- ['quota_set'])
- vcpus = int(quota_set['cores']) + 1
- disk = 10
- flavor_ref = self.flavors_client.create_flavor(name=flavor_name,
- ram=ram, vcpus=vcpus,
- disk=disk,
- id=flavor_id)['flavor']
- self.addCleanup(self.flavors_client.delete_flavor, flavor_id)
+ disk = 5
+ flavor_ref = self.create_flavor(ram=ram, vcpus=vcpus, disk=disk)
self.assertRaises((lib_exc.Forbidden, lib_exc.OverLimit),
self.client.resize_server,
- self.servers[0]['id'],
+ self.s1_id,
flavor_ref['id'])
- @test.attr(type=['negative'])
- @test.idempotent_id('b0b4d8af-1256-41ef-9ee7-25f1c19dde80')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('b0b4d8af-1256-41ef-9ee7-25f1c19dde80')
def test_reset_state_server_invalid_state(self):
self.assertRaises(lib_exc.BadRequest,
self.client.reset_state, self.s1_id,
state='invalid')
- @test.attr(type=['negative'])
- @test.idempotent_id('4cdcc984-fab0-4577-9a9d-6d558527ee9d')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('4cdcc984-fab0-4577-9a9d-6d558527ee9d')
def test_reset_state_server_invalid_type(self):
self.assertRaises(lib_exc.BadRequest,
self.client.reset_state, self.s1_id,
state=1)
- @test.attr(type=['negative'])
- @test.idempotent_id('e741298b-8df2-46f0-81cb-8f814ff2504c')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('e741298b-8df2-46f0-81cb-8f814ff2504c')
def test_reset_state_server_nonexistent_server(self):
self.assertRaises(lib_exc.NotFound,
self.client.reset_state, '999', state='error')
- @test.attr(type=['negative'])
- @test.idempotent_id('e84e2234-60d2-42fa-8b30-e2d3049724ac')
- def test_get_server_diagnostics_by_non_admin(self):
- # Non-admin user can not view server diagnostics according to policy
- self.assertRaises(lib_exc.Forbidden,
- self.non_adm_client.show_server_diagnostics,
- self.s1_id)
-
- @test.attr(type=['negative'])
- @test.idempotent_id('46a4e1ca-87ae-4d28-987a-1b6b136a0221')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('46a4e1ca-87ae-4d28-987a-1b6b136a0221')
def test_migrate_non_existent_server(self):
# migrate a non existent server
self.assertRaises(lib_exc.NotFound,
self.client.migrate_server,
- str(uuid.uuid4()))
+ data_utils.rand_uuid())
- @test.idempotent_id('b0b17f83-d14e-4fc4-8f31-bcc9f3cfa629')
- @testtools.skipUnless(CONF.compute_feature_enabled.resize,
- 'Resize not available.')
+ @decorators.idempotent_id('b0b17f83-d14e-4fc4-8f31-bcc9f3cfa629')
+ @testtools.skipUnless(CONF.compute_feature_enabled.cold_migration,
+ 'Cold migration not available.')
@testtools.skipUnless(CONF.compute_feature_enabled.suspend,
'Suspend is not available.')
- @test.attr(type=['negative'])
+ @decorators.attr(type=['negative'])
def test_migrate_server_invalid_state(self):
# create server.
server = self.create_test_server(wait_until='ACTIVE')
@@ -155,7 +128,7 @@
self.client.suspend_server(server_id)
waiters.wait_for_server_status(self.client,
server_id, 'SUSPENDED')
- # migrate an suspended server should fail
+ # migrate a suspended server should fail
self.assertRaises(lib_exc.Conflict,
self.client.migrate_server,
server_id)
diff --git a/tempest/api/compute/admin/test_servers_on_multinodes.py b/tempest/api/compute/admin/test_servers_on_multinodes.py
new file mode 100644
index 0000000..d32a5b4
--- /dev/null
+++ b/tempest/api/compute/admin/test_servers_on_multinodes.py
@@ -0,0 +1,130 @@
+# Copyright 2016 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.
+
+import testtools
+
+from tempest.api.compute import base
+from tempest.common import compute
+from tempest import config
+from tempest.lib import decorators
+
+CONF = config.CONF
+
+
+class ServersOnMultiNodesTest(base.BaseV2ComputeAdminTest):
+
+ @classmethod
+ def resource_setup(cls):
+ super(ServersOnMultiNodesTest, cls).resource_setup()
+ cls.server01 = cls.create_test_server(wait_until='ACTIVE')['id']
+ cls.host01 = cls._get_host(cls.server01)
+
+ @classmethod
+ def skip_checks(cls):
+ super(ServersOnMultiNodesTest, cls).skip_checks()
+
+ if CONF.compute.min_compute_nodes < 2:
+ raise cls.skipException(
+ "Less than 2 compute nodes, skipping multi-nodes test.")
+
+ @classmethod
+ def _get_host(cls, server_id):
+ return cls.os_admin.servers_client.show_server(
+ server_id)['server']['OS-EXT-SRV-ATTR:host']
+
+ def _create_servers_with_group(self, policy):
+ group_id = self.create_test_server_group(policy=[policy])['id']
+ hints = {'group': group_id}
+ reservation_id = self.create_test_server(
+ scheduler_hints=hints, wait_until='ACTIVE', min_count=2,
+ return_reservation_id=True)['reservation_id']
+
+ # Get the servers using the reservation_id.
+ servers = self.servers_client.list_servers(
+ detail=True, reservation_id=reservation_id)['servers']
+ self.assertEqual(2, len(servers))
+
+ # Assert the servers are in the group.
+ server_group = self.server_groups_client.show_server_group(
+ group_id)['server_group']
+ hosts = {}
+ for server in servers:
+ self.assertIn(server['id'], server_group['members'])
+ hosts[server['id']] = self._get_host(server['id'])
+
+ return hosts
+
+ @decorators.idempotent_id('26a9d5df-6890-45f2-abc4-a659290cb130')
+ @testtools.skipUnless(
+ compute.is_scheduler_filter_enabled("SameHostFilter"),
+ 'SameHostFilter is not available.')
+ def test_create_servers_on_same_host(self):
+ hints = {'same_host': self.server01}
+ server02 = self.create_test_server(scheduler_hints=hints,
+ wait_until='ACTIVE')['id']
+ host02 = self._get_host(server02)
+ self.assertEqual(self.host01, host02)
+
+ @decorators.idempotent_id('cc7ca884-6e3e-42a3-a92f-c522fcf25e8e')
+ @testtools.skipUnless(
+ compute.is_scheduler_filter_enabled("DifferentHostFilter"),
+ 'DifferentHostFilter is not available.')
+ def test_create_servers_on_different_hosts(self):
+ hints = {'different_host': self.server01}
+ server02 = self.create_test_server(scheduler_hints=hints,
+ wait_until='ACTIVE')['id']
+ host02 = self._get_host(server02)
+ self.assertNotEqual(self.host01, host02)
+
+ @decorators.idempotent_id('7869cc84-d661-4e14-9f00-c18cdc89cf57')
+ @testtools.skipUnless(
+ compute.is_scheduler_filter_enabled("DifferentHostFilter"),
+ 'DifferentHostFilter is not available.')
+ def test_create_servers_on_different_hosts_with_list_of_servers(self):
+ # This scheduler-hint supports list of servers also.
+ hints = {'different_host': [self.server01]}
+ server02 = self.create_test_server(scheduler_hints=hints,
+ wait_until='ACTIVE')['id']
+ host02 = self._get_host(server02)
+ self.assertNotEqual(self.host01, host02)
+
+ @decorators.idempotent_id('f8bd0867-e459-45f5-ba53-59134552fe04')
+ @testtools.skipUnless(
+ compute.is_scheduler_filter_enabled("ServerGroupAntiAffinityFilter"),
+ 'ServerGroupAntiAffinityFilter is not available.')
+ def test_create_server_with_scheduler_hint_group_anti_affinity(self):
+ """Tests the ServerGroupAntiAffinityFilter
+
+ Creates two servers in an anti-affinity server group and
+ asserts the servers are in the group and on different hosts.
+ """
+ hosts = self._create_servers_with_group('anti-affinity')
+ hostnames = hosts.values()
+ self.assertNotEqual(hostnames[0], hostnames[1],
+ 'Servers are on the same host: %s' % hosts)
+
+ @decorators.idempotent_id('9d2e924a-baf4-11e7-b856-fa163e65f5ce')
+ @testtools.skipUnless(
+ compute.is_scheduler_filter_enabled("ServerGroupAffinityFilter"),
+ 'ServerGroupAffinityFilter is not available.')
+ def test_create_server_with_scheduler_hint_group_affinity(self):
+ """Tests the ServerGroupAffinityFilter
+
+ Creates two servers in an affinity server group and
+ asserts the servers are in the group and on same host.
+ """
+ hosts = self._create_servers_with_group('affinity')
+ hostnames = hosts.values()
+ self.assertEqual(hostnames[0], hostnames[1],
+ 'Servers are on the different hosts: %s' % hosts)
diff --git a/tempest/api/compute/admin/test_services.py b/tempest/api/compute/admin/test_services.py
index 8648b9f..73e191b 100644
--- a/tempest/api/compute/admin/test_services.py
+++ b/tempest/api/compute/admin/test_services.py
@@ -15,7 +15,7 @@
# under the License.
from tempest.api.compute import base
-from tempest import test
+from tempest.lib import decorators
class ServicesAdminTestJSON(base.BaseV2ComputeAdminTest):
@@ -24,22 +24,22 @@
@classmethod
def setup_clients(cls):
super(ServicesAdminTestJSON, cls).setup_clients()
- cls.client = cls.os_adm.services_client
+ cls.client = cls.os_admin.services_client
- @test.idempotent_id('5be41ef4-53d1-41cc-8839-5c2a48a1b283')
+ @decorators.idempotent_id('5be41ef4-53d1-41cc-8839-5c2a48a1b283')
def test_list_services(self):
services = self.client.list_services()['services']
- self.assertNotEqual(0, len(services))
+ self.assertNotEmpty(services)
- @test.idempotent_id('f345b1ec-bc6e-4c38-a527-3ca2bc00bef5')
+ @decorators.idempotent_id('f345b1ec-bc6e-4c38-a527-3ca2bc00bef5')
def test_get_service_by_service_binary_name(self):
binary_name = 'nova-compute'
services = self.client.list_services(binary=binary_name)['services']
- self.assertNotEqual(0, len(services))
+ self.assertNotEmpty(services)
for service in services:
self.assertEqual(binary_name, service['binary'])
- @test.idempotent_id('affb42d5-5b4b-43c8-8b0b-6dca054abcca')
+ @decorators.idempotent_id('affb42d5-5b4b-43c8-8b0b-6dca054abcca')
def test_get_service_by_host_name(self):
services = self.client.list_services()['services']
host_name = services[0]['host']
@@ -56,15 +56,3 @@
# sort the lists before comparing, to take out dependency
# on order.
self.assertEqual(sorted(s1), sorted(s2))
-
- @test.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/admin/test_services_negative.py b/tempest/api/compute/admin/test_services_negative.py
index e57401a..201670a 100644
--- a/tempest/api/compute/admin/test_services_negative.py
+++ b/tempest/api/compute/admin/test_services_negative.py
@@ -12,10 +12,9 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.compute import base
-from tempest import test
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class ServicesAdminNegativeTestJSON(base.BaseV2ComputeAdminTest):
@@ -24,17 +23,17 @@
@classmethod
def setup_clients(cls):
super(ServicesAdminNegativeTestJSON, cls).setup_clients()
- cls.client = cls.os_adm.services_client
+ cls.client = cls.os_admin.services_client
cls.non_admin_client = cls.services_client
- @test.attr(type=['negative'])
- @test.idempotent_id('1126d1f8-266e-485f-a687-adc547492646')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('1126d1f8-266e-485f-a687-adc547492646')
def test_list_services_with_non_admin_user(self):
self.assertRaises(lib_exc.Forbidden,
self.non_admin_client.list_services)
- @test.attr(type=['negative'])
- @test.idempotent_id('d0884a69-f693-4e79-a9af-232d15643bf7')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('d0884a69-f693-4e79-a9af-232d15643bf7')
def test_get_service_by_invalid_params(self):
# return all services if send the request with invalid parameter
services = self.client.list_services()['services']
@@ -42,20 +41,20 @@
['services'])
self.assertEqual(len(services), len(services_xxx))
- @test.attr(type=['negative'])
- @test.idempotent_id('1e966d4a-226e-47c7-b601-0b18a27add54')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('1e966d4a-226e-47c7-b601-0b18a27add54')
def test_get_service_by_invalid_service_and_valid_host(self):
services = self.client.list_services()['services']
host_name = services[0]['host']
services = self.client.list_services(host=host_name,
binary='xxx')['services']
- self.assertEqual(0, len(services))
+ self.assertEmpty(services)
- @test.attr(type=['negative'])
- @test.idempotent_id('64e7e7fb-69e8-4cb6-a71d-8d5eb0c98655')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('64e7e7fb-69e8-4cb6-a71d-8d5eb0c98655')
def test_get_service_with_valid_service_and_invalid_host(self):
services = self.client.list_services()['services']
binary_name = services[0]['binary']
services = self.client.list_services(host='xxx',
binary=binary_name)['services']
- self.assertEqual(0, len(services))
+ self.assertEmpty(services)
diff --git a/tempest/api/compute/admin/test_simple_tenant_usage.py b/tempest/api/compute/admin/test_simple_tenant_usage.py
index e5c17ca..d4c60b3 100644
--- a/tempest/api/compute/admin/test_simple_tenant_usage.py
+++ b/tempest/api/compute/admin/test_simple_tenant_usage.py
@@ -16,8 +16,9 @@
import datetime
from tempest.api.compute import base
-from tempest import test
-from tempest_lib import exceptions as e
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as e
# Time that waits for until returning valid response
# TODO(takmatsu): Ideally this value would come from configuration.
@@ -29,8 +30,8 @@
@classmethod
def setup_clients(cls):
super(TenantUsagesTestJSON, cls).setup_clients()
- cls.adm_client = cls.os_adm.tenant_usages_client
- cls.client = cls.os.tenant_usages_client
+ cls.adm_client = cls.os_admin.tenant_usages_client
+ cls.client = cls.os_primary.tenant_usages_client
@classmethod
def resource_setup(cls):
@@ -59,10 +60,12 @@
return True
except e.InvalidHTTPResponseBody:
return False
- test.call_until_true(is_valid, duration, 1)
+ self.assertEqual(test_utils.call_until_true(is_valid, duration, 1),
+ True, "%s not return valid response in %s secs" % (
+ func.__name__, duration))
return self.resp
- @test.idempotent_id('062c8ae9-9912-4249-8b51-e38d664e926e')
+ @decorators.idempotent_id('062c8ae9-9912-4249-8b51-e38d664e926e')
def test_list_usage_all_tenants(self):
# Get usage for all tenants
tenant_usage = self.call_until_valid(
@@ -70,7 +73,7 @@
start=self.start, end=self.end, detailed="1")['tenant_usages'][0]
self.assertEqual(len(tenant_usage), 8)
- @test.idempotent_id('94135049-a4c5-4934-ad39-08fa7da4f22e')
+ @decorators.idempotent_id('94135049-a4c5-4934-ad39-08fa7da4f22e')
def test_get_usage_tenant(self):
# Get usage for a specific tenant
tenant_usage = self.call_until_valid(
@@ -79,7 +82,7 @@
self.assertEqual(len(tenant_usage), 8)
- @test.idempotent_id('9d00a412-b40e-4fd9-8eba-97b496316116')
+ @decorators.idempotent_id('9d00a412-b40e-4fd9-8eba-97b496316116')
def test_get_usage_tenant_with_non_admin_user(self):
# Get usage for a specific tenant with non admin user
tenant_usage = self.call_until_valid(
diff --git a/tempest/api/compute/admin/test_simple_tenant_usage_negative.py b/tempest/api/compute/admin/test_simple_tenant_usage_negative.py
index e9b4ad4..cb60b8d 100644
--- a/tempest/api/compute/admin/test_simple_tenant_usage_negative.py
+++ b/tempest/api/compute/admin/test_simple_tenant_usage_negative.py
@@ -14,10 +14,10 @@
# under the License.
import datetime
-from tempest_lib import exceptions as lib_exc
from tempest.api.compute import base
-from tempest import test
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class TenantUsagesNegativeTestJSON(base.BaseV2ComputeAdminTest):
@@ -25,8 +25,8 @@
@classmethod
def setup_clients(cls):
super(TenantUsagesNegativeTestJSON, cls).setup_clients()
- cls.adm_client = cls.os_adm.tenant_usages_client
- cls.client = cls.os.tenant_usages_client
+ cls.adm_client = cls.os_admin.tenant_usages_client
+ cls.client = cls.os_primary.tenant_usages_client
@classmethod
def resource_setup(cls):
@@ -40,8 +40,8 @@
# Returns formatted datetime
return at.strftime('%Y-%m-%dT%H:%M:%S.%f')
- @test.attr(type=['negative'])
- @test.idempotent_id('8b21e135-d94b-4991-b6e9-87059609c8ed')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('8b21e135-d94b-4991-b6e9-87059609c8ed')
def test_get_usage_tenant_with_empty_tenant_id(self):
# Get usage for a specific tenant empty
params = {'start': self.start,
@@ -50,8 +50,8 @@
self.adm_client.show_tenant_usage,
'', **params)
- @test.attr(type=['negative'])
- @test.idempotent_id('4079dd2a-9e8d-479f-869d-6fa985ce45b6')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('4079dd2a-9e8d-479f-869d-6fa985ce45b6')
def test_get_usage_tenant_with_invalid_date(self):
# Get usage for tenant with invalid date
params = {'start': self.end,
@@ -60,8 +60,8 @@
self.adm_client.show_tenant_usage,
self.client.tenant_id, **params)
- @test.attr(type=['negative'])
- @test.idempotent_id('bbe6fe2c-15d8-404c-a0a2-44fad0ad5cc7')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('bbe6fe2c-15d8-404c-a0a2-44fad0ad5cc7')
def test_list_usage_all_tenants_with_non_admin_user(self):
# Get usage for all tenants with non admin user
params = {'start': self.start,
diff --git a/tempest/api/compute/admin/test_volume_swap.py b/tempest/api/compute/admin/test_volume_swap.py
new file mode 100644
index 0000000..ed8cf20
--- /dev/null
+++ b/tempest/api/compute/admin/test_volume_swap.py
@@ -0,0 +1,180 @@
+# 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 time
+
+from tempest.api.compute import base
+from tempest.common import utils
+from tempest.common import waiters
+from tempest import config
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+CONF = config.CONF
+
+
+class TestVolumeSwapBase(base.BaseV2ComputeAdminTest):
+
+ @classmethod
+ def skip_checks(cls):
+ super(TestVolumeSwapBase, cls).skip_checks()
+ if not CONF.compute_feature_enabled.swap_volume:
+ raise cls.skipException("Swapping volumes is not supported.")
+
+ def wait_for_server_volume_swap(self, server_id, old_volume_id,
+ new_volume_id):
+ """Waits for a server to swap the old volume to a new one."""
+ volume_attachments = self.servers_client.list_volume_attachments(
+ server_id)['volumeAttachments']
+ attached_volume_ids = [attachment['volumeId']
+ for attachment in volume_attachments]
+ start = int(time.time())
+
+ while (old_volume_id in attached_volume_ids) \
+ or (new_volume_id not in attached_volume_ids):
+ time.sleep(self.servers_client.build_interval)
+ volume_attachments = self.servers_client.list_volume_attachments(
+ server_id)['volumeAttachments']
+ attached_volume_ids = [attachment['volumeId']
+ for attachment in volume_attachments]
+
+ if int(time.time()) - start >= self.servers_client.build_timeout:
+ old_vol_bdm_status = 'in BDM' \
+ if old_volume_id in attached_volume_ids else 'not in BDM'
+ new_vol_bdm_status = 'in BDM' \
+ if new_volume_id in attached_volume_ids else 'not in BDM'
+ message = ('Failed to swap old volume %(old_volume_id)s '
+ '(current %(old_vol_bdm_status)s) to new volume '
+ '%(new_volume_id)s (current %(new_vol_bdm_status)s)'
+ ' on server %(server_id)s within the required time '
+ '(%(timeout)s s)' %
+ {'old_volume_id': old_volume_id,
+ 'old_vol_bdm_status': old_vol_bdm_status,
+ 'new_volume_id': new_volume_id,
+ 'new_vol_bdm_status': new_vol_bdm_status,
+ 'server_id': server_id,
+ 'timeout': self.servers_client.build_timeout})
+ raise lib_exc.TimeoutException(message)
+
+
+class TestVolumeSwap(TestVolumeSwapBase):
+ """The test suite for swapping of volume with admin user.
+
+ The following is the scenario outline:
+ 1. Create a volume "volume1" with non-admin.
+ 2. Create a volume "volume2" with non-admin.
+ 3. Boot an instance "instance1" with non-admin.
+ 4. Attach "volume1" to "instance1" with non-admin.
+ 5. Swap volume from "volume1" to "volume2" as admin.
+ 6. Check the swap volume is successful and "volume2"
+ is attached to "instance1" and "volume1" is in available state.
+ 7. Swap volume from "volume2" to "volume1" as admin.
+ 8. Check the swap volume is successful and "volume1"
+ is attached to "instance1" and "volume2" is in available state.
+ """
+
+ @decorators.idempotent_id('1769f00d-a693-4d67-a631-6a3496773813')
+ @utils.services('volume')
+ def test_volume_swap(self):
+ # Create two volumes.
+ # NOTE(gmann): Volumes are created before server creation so that
+ # volumes cleanup can happen successfully irrespective of which volume
+ # is attached to server.
+ volume1 = self.create_volume()
+ volume2 = self.create_volume()
+ # Boot server
+ server = self.create_test_server(wait_until='ACTIVE')
+ # Attach "volume1" to server
+ self.attach_volume(server, volume1)
+ # Swap volume from "volume1" to "volume2"
+ self.admin_servers_client.update_attached_volume(
+ server['id'], volume1['id'], volumeId=volume2['id'])
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume1['id'], 'available')
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume2['id'], 'in-use')
+ self.wait_for_server_volume_swap(server['id'], volume1['id'],
+ volume2['id'])
+ # Verify "volume2" is attached to the server
+ vol_attachments = self.servers_client.list_volume_attachments(
+ server['id'])['volumeAttachments']
+ self.assertEqual(1, len(vol_attachments))
+ self.assertIn(volume2['id'], vol_attachments[0]['volumeId'])
+
+ # Swap volume from "volume2" to "volume1"
+ self.admin_servers_client.update_attached_volume(
+ server['id'], volume2['id'], volumeId=volume1['id'])
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume2['id'], 'available')
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume1['id'], 'in-use')
+ self.wait_for_server_volume_swap(server['id'], volume2['id'],
+ volume1['id'])
+ # Verify "volume1" is attached to the server
+ vol_attachments = self.servers_client.list_volume_attachments(
+ server['id'])['volumeAttachments']
+ self.assertEqual(1, len(vol_attachments))
+ self.assertIn(volume1['id'], vol_attachments[0]['volumeId'])
+
+
+class TestMultiAttachVolumeSwap(TestVolumeSwapBase):
+ min_microversion = '2.60'
+ max_microversion = 'latest'
+
+ @classmethod
+ def skip_checks(cls):
+ super(TestMultiAttachVolumeSwap, cls).skip_checks()
+ if not CONF.compute_feature_enabled.volume_multiattach:
+ raise cls.skipException('Volume multi-attach is not available.')
+
+ @decorators.idempotent_id('e8f8f9d1-d7b7-4cd2-8213-ab85ef697b6e')
+ @utils.services('volume')
+ def test_volume_swap_with_multiattach(self):
+ # Create two volumes.
+ # NOTE(gmann): Volumes are created before server creation so that
+ # volumes cleanup can happen successfully irrespective of which volume
+ # is attached to server.
+ volume1 = self.create_volume(multiattach=True)
+ volume2 = self.create_volume(multiattach=True)
+
+ # Boot server1
+ server1 = self.create_test_server(wait_until='ACTIVE')
+ # Attach volume1 to server1
+ self.attach_volume(server1, volume1)
+ # Boot server2
+ server2 = self.create_test_server(wait_until='ACTIVE')
+ # Attach volume1 to server2
+ self.attach_volume(server2, volume1)
+
+ # Swap volume1 to volume2 on server1, volume1 should remain attached
+ # to server 2
+ self.admin_servers_client.update_attached_volume(
+ server1['id'], volume1['id'], volumeId=volume2['id'])
+ # volume1 will return to in-use after the swap
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume1['id'], 'in-use')
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume2['id'], 'in-use')
+ self.wait_for_server_volume_swap(server1['id'], volume1['id'],
+ volume2['id'])
+
+ # Verify volume2 is attached to server1
+ vol_attachments = self.servers_client.list_volume_attachments(
+ server1['id'])['volumeAttachments']
+ self.assertEqual(1, len(vol_attachments))
+ self.assertIn(volume2['id'], vol_attachments[0]['volumeId'])
+
+ # Verify volume1 is still attached to server2
+ vol_attachments = self.servers_client.list_volume_attachments(
+ server2['id'])['volumeAttachments']
+ self.assertEqual(1, len(vol_attachments))
+ self.assertIn(volume1['id'], vol_attachments[0]['volumeId'])
diff --git a/tempest/api/compute/admin/test_volumes_negative.py b/tempest/api/compute/admin/test_volumes_negative.py
new file mode 100644
index 0000000..4a7f36f
--- /dev/null
+++ b/tempest/api/compute/admin/test_volumes_negative.py
@@ -0,0 +1,59 @@
+# Copyright 2016 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 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 VolumesAdminNegativeTest(base.BaseV2ComputeAdminTest):
+
+ @classmethod
+ def skip_checks(cls):
+ super(VolumesAdminNegativeTest, cls).skip_checks()
+ if not CONF.service_available.cinder:
+ skip_msg = ("%s skipped as Cinder is not available" % cls.__name__)
+ raise cls.skipException(skip_msg)
+
+ @classmethod
+ def resource_setup(cls):
+ super(VolumesAdminNegativeTest, cls).resource_setup()
+ cls.server = cls.create_test_server(wait_until='ACTIVE')
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('309b5ecd-0585-4a7e-a36f-d2b2bf55259d')
+ def test_update_attached_volume_with_nonexistent_volume_in_uri(self):
+ volume = self.create_volume()
+ nonexistent_volume = data_utils.rand_uuid()
+ self.assertRaises(lib_exc.NotFound,
+ self.admin_servers_client.update_attached_volume,
+ self.server['id'], nonexistent_volume,
+ volumeId=volume['id'])
+
+ @decorators.related_bug('1629110', status_code=400)
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('7dcac15a-b107-46d3-a5f6-cb863f4e454a')
+ def test_update_attached_volume_with_nonexistent_volume_in_body(self):
+ volume = self.create_volume()
+ self.attach_volume(self.server, volume)
+
+ nonexistent_volume = data_utils.rand_uuid()
+ self.assertRaises(lib_exc.BadRequest,
+ self.admin_servers_client.update_attached_volume,
+ self.server['id'], volume['id'],
+ volumeId=nonexistent_volume)
diff --git a/tempest/api/compute/api_microversion_fixture.py b/tempest/api/compute/api_microversion_fixture.py
new file mode 100644
index 0000000..695af52
--- /dev/null
+++ b/tempest/api/compute/api_microversion_fixture.py
@@ -0,0 +1,31 @@
+# Copyright 2016 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.
+
+import fixtures
+
+from tempest.lib.services.compute import base_compute_client
+
+
+class APIMicroversionFixture(fixtures.Fixture):
+
+ def __init__(self, compute_microversion):
+ self.compute_microversion = compute_microversion
+
+ def _setUp(self):
+ super(APIMicroversionFixture, self)._setUp()
+ base_compute_client.COMPUTE_MICROVERSION = self.compute_microversion
+ self.addCleanup(self._reset_compute_microversion)
+
+ def _reset_compute_microversion(self):
+ base_compute_client.COMPUTE_MICROVERSION = None
diff --git a/tempest/api/compute/base.py b/tempest/api/compute/base.py
index f7b3655..d0c1973 100644
--- a/tempest/api/compute/base.py
+++ b/tempest/api/compute/base.py
@@ -16,13 +16,17 @@
import time
from oslo_log import log as logging
-from tempest_lib import exceptions as lib_exc
-from tempest.common import api_version_utils
+from tempest.api.compute import api_microversion_fixture
from tempest.common import compute
-from tempest.common.utils import data_utils
from tempest.common import waiters
from tempest import config
+from tempest import exceptions
+from tempest.lib.common import api_version_request
+from tempest.lib.common import api_version_utils
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import exceptions as lib_exc
import tempest.test
CONF = config.CONF
@@ -45,8 +49,8 @@
super(BaseV2ComputeTest, cls).skip_checks()
if not CONF.service_available.nova:
raise cls.skipException("Nova is not available")
- cfg_min_version = CONF.compute_feature_enabled.min_microversion
- cfg_max_version = CONF.compute_feature_enabled.max_microversion
+ cfg_min_version = CONF.compute.min_microversion
+ cfg_max_version = CONF.compute.max_microversion
api_version_utils.check_skip_with_microversion(cls.min_microversion,
cls.max_microversion,
cfg_min_version,
@@ -55,57 +59,92 @@
@classmethod
def setup_credentials(cls):
cls.set_network_resources()
- cls.request_microversion = (
- api_version_utils.select_request_microversion(
- cls.min_microversion,
- CONF.compute_feature_enabled.min_microversion))
- if cls.request_microversion:
- cls.services_microversion = {
- CONF.compute.catalog_type: cls.request_microversion}
super(BaseV2ComputeTest, cls).setup_credentials()
@classmethod
def setup_clients(cls):
super(BaseV2ComputeTest, cls).setup_clients()
- cls.servers_client = cls.os.servers_client
- cls.server_groups_client = cls.os.server_groups_client
- cls.flavors_client = cls.os.flavors_client
- cls.images_client = cls.os.images_client
- cls.extensions_client = cls.os.extensions_client
- cls.floating_ip_pools_client = cls.os.floating_ip_pools_client
- cls.floating_ips_client = cls.os.compute_floating_ips_client
- cls.keypairs_client = cls.os.keypairs_client
- cls.security_group_rules_client = cls.os.security_group_rules_client
- cls.security_groups_client = cls.os.compute_security_groups_client
- cls.quotas_client = cls.os.quotas_client
- cls.quota_classes_client = cls.os.quota_classes_client
- cls.compute_networks_client = cls.os.compute_networks_client
- cls.limits_client = cls.os.limits_client
- cls.volumes_extensions_client = cls.os.volumes_extensions_client
- cls.snapshots_extensions_client = cls.os.snapshots_extensions_client
- cls.interfaces_client = cls.os.interfaces_client
- cls.fixed_ips_client = cls.os.fixed_ips_client
- cls.availability_zone_client = cls.os.availability_zone_client
- cls.agents_client = cls.os.agents_client
- cls.aggregates_client = cls.os.aggregates_client
- cls.services_client = cls.os.services_client
+ cls.servers_client = cls.os_primary.servers_client
+ cls.server_groups_client = cls.os_primary.server_groups_client
+ cls.flavors_client = cls.os_primary.flavors_client
+ cls.compute_images_client = cls.os_primary.compute_images_client
+ cls.extensions_client = cls.os_primary.extensions_client
+ cls.floating_ip_pools_client = cls.os_primary.floating_ip_pools_client
+ cls.floating_ips_client = cls.os_primary.compute_floating_ips_client
+ cls.keypairs_client = cls.os_primary.keypairs_client
+ cls.security_group_rules_client = (
+ cls.os_primary.compute_security_group_rules_client)
+ cls.security_groups_client =\
+ cls.os_primary.compute_security_groups_client
+ cls.quotas_client = cls.os_primary.quotas_client
+ cls.compute_networks_client = cls.os_primary.compute_networks_client
+ cls.limits_client = cls.os_primary.limits_client
+ cls.volumes_extensions_client =\
+ cls.os_primary.volumes_extensions_client
+ cls.snapshots_extensions_client =\
+ cls.os_primary.snapshots_extensions_client
+ cls.interfaces_client = cls.os_primary.interfaces_client
+ cls.fixed_ips_client = cls.os_primary.fixed_ips_client
+ cls.availability_zone_client = cls.os_primary.availability_zone_client
+ cls.agents_client = cls.os_primary.agents_client
+ cls.aggregates_client = cls.os_primary.aggregates_client
+ cls.services_client = cls.os_primary.services_client
cls.instance_usages_audit_log_client = (
- cls.os.instance_usages_audit_log_client)
- cls.hypervisor_client = cls.os.hypervisor_client
- cls.certificates_client = cls.os.certificates_client
- cls.migrations_client = cls.os.migrations_client
+ cls.os_primary.instance_usages_audit_log_client)
+ cls.hypervisor_client = cls.os_primary.hypervisor_client
+ cls.certificates_client = cls.os_primary.certificates_client
+ cls.migrations_client = cls.os_primary.migrations_client
cls.security_group_default_rules_client = (
- cls.os.security_group_default_rules_client)
- cls.versions_client = cls.os.compute_versions_client
+ cls.os_primary.security_group_default_rules_client)
+ cls.versions_client = cls.os_primary.compute_versions_client
+ if CONF.service_available.cinder:
+ cls.volumes_client = cls.os_primary.volumes_client_latest
+ if CONF.service_available.glance:
+ if CONF.image_feature_enabled.api_v1:
+ cls.images_client = cls.os_primary.image_client
+ elif CONF.image_feature_enabled.api_v2:
+ cls.images_client = cls.os_primary.image_client_v2
+ else:
+ raise lib_exc.InvalidConfiguration(
+ 'Either api_v1 or api_v2 must be True in '
+ '[image-feature-enabled].')
+ cls._check_depends_on_nova_network()
- if CONF.volume_feature_enabled.api_v1:
- cls.volumes_client = cls.os.volumes_client
+ @classmethod
+ def _check_depends_on_nova_network(cls):
+ # Since nova-network APIs were removed from Nova in the Rocky release,
+ # determine, based on the max version from the version document, if
+ # the compute API is >Queens and if so, skip tests that rely on
+ # nova-network.
+ if not getattr(cls, 'depends_on_nova_network', False):
+ return
+ versions = cls.versions_client.list_versions()['versions']
+ # Find the v2.1 version which will tell us our max version for the
+ # compute API we're testing against.
+ for version in versions:
+ if version['id'] == 'v2.1':
+ max_version = api_version_request.APIVersionRequest(
+ version['version'])
+ break
else:
- cls.volumes_client = cls.os.volumes_v2_client
+ LOG.warning(
+ 'Unable to determine max v2.1 compute API version: %s',
+ versions)
+ return
+
+ # The max compute API version in Queens is 2.60 so we cap
+ # at that version.
+ queens = api_version_request.APIVersionRequest('2.60')
+ if max_version > queens:
+ raise cls.skipException('nova-network is gone')
@classmethod
def resource_setup(cls):
super(BaseV2ComputeTest, cls).resource_setup()
+ cls.request_microversion = (
+ api_version_utils.select_request_microversion(
+ cls.min_microversion,
+ CONF.compute.min_microversion))
cls.build_interval = CONF.compute.build_interval
cls.build_timeout = CONF.compute.build_timeout
cls.image_ref = CONF.compute.image_ref
@@ -115,40 +154,36 @@
cls.ssh_user = CONF.validation.image_ssh_user
cls.image_ssh_user = CONF.validation.image_ssh_user
cls.image_ssh_password = CONF.validation.image_ssh_password
- cls.servers = []
- cls.images = []
- cls.security_groups = []
- cls.server_groups = []
@classmethod
- def resource_cleanup(cls):
- cls.clear_images()
- cls.clear_servers()
- cls.clear_security_groups()
- cls.clear_server_groups()
- super(BaseV2ComputeTest, cls).resource_cleanup()
+ def is_requested_microversion_compatible(cls, max_version):
+ """Check the compatibility of selected request microversion
- @classmethod
- def clear_servers(cls):
- LOG.debug('Clearing servers: %s', ','.join(
- server['id'] for server in cls.servers))
- for server in cls.servers:
- try:
- cls.servers_client.delete_server(server['id'])
- except lib_exc.NotFound:
- # Something else already cleaned up the server, nothing to be
- # worried about
- pass
- except Exception:
- LOG.exception('Deleting server %s failed' % server['id'])
+ 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'.
- for server in cls.servers:
- try:
- waiters.wait_for_server_termination(cls.servers_client,
- server['id'])
- except Exception:
- LOG.exception('Waiting for deletion of server %s failed'
- % server['id'])
+ :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):
@@ -173,48 +208,21 @@
raise
@classmethod
- def clear_images(cls):
- LOG.debug('Clearing images: %s', ','.join(cls.images))
- for image_id in cls.images:
+ def clear_resources(cls, resource_name, resources, resource_del_func):
+ LOG.debug('Clearing %s: %s', resource_name,
+ ','.join(map(str, resources)))
+ for res_id in resources:
try:
- cls.images_client.delete_image(image_id)
- except lib_exc.NotFound:
- # The image may have already been deleted which is OK.
- pass
- except Exception:
- LOG.exception('Exception raised deleting image %s' % image_id)
-
- @classmethod
- def clear_security_groups(cls):
- LOG.debug('Clearing security groups: %s', ','.join(
- str(sg['id']) for sg in cls.security_groups))
- for sg in cls.security_groups:
- try:
- cls.security_groups_client.delete_security_group(sg['id'])
- except lib_exc.NotFound:
- # The security group may have already been deleted which is OK.
- pass
+ test_utils.call_and_ignore_notfound_exc(
+ resource_del_func, res_id)
except Exception as exc:
- LOG.info('Exception raised deleting security group %s',
- sg['id'])
+ LOG.exception('Exception raised deleting %s: %s',
+ resource_name, res_id)
LOG.exception(exc)
@classmethod
- def clear_server_groups(cls):
- LOG.debug('Clearing server groups: %s', ','.join(cls.server_groups))
- for server_group_id in cls.server_groups:
- try:
- cls.server_groups_client.delete_server_group(server_group_id)
- except lib_exc.NotFound:
- # The server-group may have already been deleted which is OK.
- pass
- except Exception:
- LOG.exception('Exception raised deleting server-group %s',
- server_group_id)
-
- @classmethod
def create_test_server(cls, validatable=False, volume_backed=False,
- **kwargs):
+ validation_resources=None, **kwargs):
"""Wrapper utility that returns a test server.
This wrapper utility calls the common create test server and
@@ -224,17 +232,41 @@
:param validatable: Whether the server will be pingable or sshable.
:param volume_backed: Whether the instance is volume backed or not.
+ :param validation_resources: Dictionary of validation resources as
+ returned by `get_class_validation_resources`.
+ :param kwargs: Extra arguments are passed down to the
+ `compute.create_test_server` call.
"""
+ if 'name' not in kwargs:
+ kwargs['name'] = data_utils.rand_name(cls.__name__ + "-server")
+
+ request_version = api_version_request.APIVersionRequest(
+ cls.request_microversion)
+ v2_37_version = api_version_request.APIVersionRequest('2.37')
+
tenant_network = cls.get_tenant_network()
+ # NOTE(snikitin): since microversion v2.37 'networks' field is required
+ if (request_version >= v2_37_version and 'networks' not in kwargs and
+ not tenant_network):
+ kwargs['networks'] = 'none'
+
body, servers = compute.create_test_server(
- cls.os,
+ cls.os_primary,
validatable,
- validation_resources=cls.validation_resources,
+ validation_resources=validation_resources,
tenant_network=tenant_network,
volume_backed=volume_backed,
**kwargs)
- cls.servers.extend(servers)
+ # For each server schedule wait and delete, so we first delete all
+ # and then wait for all
+ for server in servers:
+ cls.addClassResourceCleanup(waiters.wait_for_server_termination,
+ cls.servers_client, server['id'])
+ for server in servers:
+ cls.addClassResourceCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ cls.servers_client.delete_server, server['id'])
return body
@@ -246,7 +278,10 @@
description = data_utils.rand_name('description')
body = cls.security_groups_client.create_security_group(
name=name, description=description)['security_group']
- cls.security_groups.append(body)
+ cls.addClassResourceCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ cls.security_groups_client.delete_security_group,
+ body['id'])
return body
@@ -258,7 +293,10 @@
policy = ['affinity']
body = cls.server_groups_client.create_server_group(
name=name, policies=policy)['server_group']
- cls.server_groups.append(body['id'])
+ cls.addClassResourceCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ cls.server_groups_client.delete_server_group,
+ body['id'])
return body
def wait_for(self, condition):
@@ -276,18 +314,6 @@
return
time.sleep(self.build_interval)
- @staticmethod
- def _delete_volume(volumes_client, volume_id):
- """Deletes the given volume and waits for it to be gone."""
- try:
- volumes_client.delete_volume(volume_id)
- # TODO(mriedem): We should move the wait_for_resource_deletion
- # into the delete_volume method as a convenience to the caller.
- volumes_client.wait_for_resource_deletion(volume_id)
- except lib_exc.NotFound:
- LOG.warning("Unable to delete volume '%s' since it was not found. "
- "Maybe it was already deleted?" % volume_id)
-
@classmethod
def prepare_instance_network(cls):
if (CONF.validation.auth_method != 'disabled' and
@@ -297,42 +323,99 @@
@classmethod
def create_image_from_server(cls, server_id, **kwargs):
- """Wrapper utility that returns an image created from the server."""
- name = data_utils.rand_name(cls.__name__ + "-image")
- if 'name' in kwargs:
- name = kwargs.pop('name')
+ """Wrapper utility that returns an image created from the server.
- image = cls.images_client.create_image(server_id, name=name)
- image_id = data_utils.parse_image_id(image.response['location'])
- cls.images.append(image_id)
+ If compute microversion >= 2.36, the returned image response will
+ be from the image service API rather than the compute image proxy API.
+ """
+ name = kwargs.pop('name',
+ data_utils.rand_name(cls.__name__ + "-image"))
+ wait_until = kwargs.pop('wait_until', None)
+ wait_for_server = kwargs.pop('wait_for_server', True)
- if 'wait_until' in kwargs:
- waiters.wait_for_image_status(cls.images_client,
- image_id, kwargs['wait_until'])
- image = cls.images_client.show_image(image_id)['image']
+ image = cls.compute_images_client.create_image(server_id, name=name,
+ **kwargs)
+ if api_version_utils.compare_version_header_to_response(
+ "OpenStack-API-Version", "compute 2.45", image.response, "lt"):
+ image_id = image['image_id']
+ else:
+ image_id = data_utils.parse_image_id(image.response['location'])
- if kwargs['wait_until'] == 'ACTIVE':
- if kwargs.get('wait_for_server', True):
+ # The compute image proxy APIs were deprecated in 2.35 so
+ # use the images client directly if the API microversion being
+ # used is >=2.36.
+ if api_version_utils.compare_version_header_to_response(
+ "OpenStack-API-Version", "compute 2.36", image.response, "lt"):
+ client = cls.images_client
+ else:
+ client = cls.compute_images_client
+ cls.addClassResourceCleanup(test_utils.call_and_ignore_notfound_exc,
+ client.delete_image, image_id)
+
+ if wait_until is not None:
+ try:
+ waiters.wait_for_image_status(client, image_id, wait_until)
+ except lib_exc.NotFound:
+ if wait_until.upper() == 'ACTIVE':
+ # If the image is not found after create_image returned
+ # that means the snapshot failed in nova-compute and nova
+ # deleted the image. There should be a compute fault
+ # recorded with the server in that case, so get the server
+ # and dump some details.
+ server = (
+ cls.servers_client.show_server(server_id)['server'])
+ if 'fault' in server:
+ raise exceptions.SnapshotNotFoundException(
+ server['fault'], image_id=image_id)
+ else:
+ raise exceptions.SnapshotNotFoundException(
+ image_id=image_id)
+ else:
+ raise
+ image = client.show_image(image_id)
+ # Compute image client returns response wrapped in 'image' element
+ # which is not the case with Glance image client.
+ if 'image' in image:
+ image = image['image']
+
+ if wait_until.upper() == 'ACTIVE':
+ if wait_for_server:
waiters.wait_for_server_status(cls.servers_client,
server_id, 'ACTIVE')
return image
@classmethod
- def rebuild_server(cls, server_id, validatable=False, **kwargs):
- # Destroy an existing server and creates a new one
- if server_id:
- try:
- cls.servers_client.delete_server(server_id)
- waiters.wait_for_server_termination(cls.servers_client,
- server_id)
- except Exception:
- LOG.exception('Failed to delete server %s' % server_id)
+ def recreate_server(cls, server_id, validatable=False, **kwargs):
+ """Destroy an existing class level server and creates a new one
+ Some test classes use a test server that can be used by multiple
+ tests. This is done to optimise runtime and test load.
+ If something goes wrong with the test server, it can be rebuilt
+ using this helper.
+
+ This helper can also be used for the initial provisioning if no
+ server_id is specified.
+
+ :param server_id: UUID of the server to be rebuilt. If None is
+ specified, a new server is provisioned.
+ :param validatable: whether to the server needs to be
+ validatable. When True, validation resources are acquired via
+ the `get_class_validation_resources` helper.
+ :param kwargs: extra paramaters are passed through to the
+ `create_test_server` call.
+ :return: the UUID of the created server.
+ """
+ if server_id:
+ cls.delete_server(server_id)
+
+ cls.password = data_utils.rand_password()
server = cls.create_test_server(
validatable,
+ validation_resources=cls.get_class_validation_resources(
+ cls.os_primary),
wait_until='ACTIVE',
+ adminPass=cls.password,
**kwargs)
- cls.password = server['adminPass']
return server['id']
@classmethod
@@ -343,27 +426,140 @@
waiters.wait_for_server_termination(cls.servers_client,
server_id)
except Exception:
- LOG.exception('Failed to delete server %s' % server_id)
+ LOG.exception('Failed to delete server %s', server_id)
+
+ @classmethod
+ def resize_server(cls, server_id, new_flavor_id, **kwargs):
+ """resize and confirm_resize an server, waits for it to be ACTIVE."""
+ cls.servers_client.resize_server(server_id, new_flavor_id, **kwargs)
+ waiters.wait_for_server_status(cls.servers_client, server_id,
+ 'VERIFY_RESIZE')
+ cls.servers_client.confirm_resize_server(server_id)
+ waiters.wait_for_server_status(cls.servers_client, server_id, 'ACTIVE')
+ server = cls.servers_client.show_server(server_id)['server']
+ # Nova API > 2.46 no longer includes flavor.id
+ if server['flavor'].get('id'):
+ if new_flavor_id != server['flavor']['id']:
+ msg = ('Flavor id of %s is not equal to new_flavor_id.'
+ % server_id)
+ raise lib_exc.TempestException(msg)
@classmethod
def delete_volume(cls, volume_id):
"""Deletes the given volume and waits for it to be gone."""
- cls._delete_volume(cls.volumes_extensions_client, volume_id)
+ try:
+ cls.volumes_client.delete_volume(volume_id)
+ # TODO(mriedem): We should move the wait_for_resource_deletion
+ # into the delete_volume method as a convenience to the caller.
+ cls.volumes_client.wait_for_resource_deletion(volume_id)
+ except lib_exc.NotFound:
+ LOG.warning("Unable to delete volume '%s' since it was not found. "
+ "Maybe it was already deleted?", volume_id)
@classmethod
- def get_server_ip(cls, server):
+ def get_server_ip(cls, server, validation_resources=None):
"""Get the server fixed or floating IP.
- For the floating IP, the address created by the validation resources
- is returned.
- For the fixed IP, the server is returned and the current mechanism of
- address extraction in the remote_client is followed.
+ Based on the configuration we're in, return a correct ip
+ address for validating that a guest is up.
+
+ :param server: The server dict as returned by the API
+ :param validation_resources: The dict of validation resources
+ provisioned for the server.
"""
if CONF.validation.connect_method == 'floating':
- ip_or_server = cls.validation_resources['floating_ip']['ip']
+ if validation_resources:
+ return validation_resources['floating_ip']['ip']
+ else:
+ msg = ('When validation.connect_method equals floating, '
+ 'validation_resources cannot be None')
+ raise exceptions.InvalidParam(invalid_param=msg)
elif CONF.validation.connect_method == 'fixed':
- ip_or_server = server
- return ip_or_server
+ addresses = server['addresses'][CONF.validation.network_for_ssh]
+ for address in addresses:
+ if address['version'] == CONF.validation.ip_version_for_ssh:
+ return address['addr']
+ raise exceptions.ServerUnreachable(server_id=server['id'])
+ else:
+ raise lib_exc.InvalidConfiguration()
+
+ def setUp(self):
+ super(BaseV2ComputeTest, self).setUp()
+ self.useFixture(api_microversion_fixture.APIMicroversionFixture(
+ self.request_microversion))
+
+ @classmethod
+ def create_volume(cls, image_ref=None, **kwargs):
+ """Create a volume and wait for it to become 'available'.
+
+ :param image_ref: Specify an image id to create a bootable volume.
+ :**kwargs: other parameters to create volume.
+ :returns: The available volume.
+ """
+ if 'size' not in kwargs:
+ kwargs['size'] = CONF.volume.volume_size
+ if 'display_name' not in kwargs:
+ vol_name = data_utils.rand_name(cls.__name__ + '-volume')
+ kwargs['display_name'] = vol_name
+ if image_ref is not None:
+ kwargs['imageRef'] = image_ref
+ volume = cls.volumes_client.create_volume(**kwargs)['volume']
+ cls.addClassResourceCleanup(
+ cls.volumes_client.wait_for_resource_deletion, volume['id'])
+ cls.addClassResourceCleanup(test_utils.call_and_ignore_notfound_exc,
+ cls.volumes_client.delete_volume,
+ volume['id'])
+ waiters.wait_for_volume_resource_status(cls.volumes_client,
+ volume['id'], 'available')
+ return volume
+
+ def _detach_volume(self, server, volume):
+ """Helper method to detach a volume.
+
+ Ignores 404 responses if the volume or server do not exist, or the
+ volume is already detached from the server.
+ """
+ try:
+ volume = self.volumes_client.show_volume(volume['id'])['volume']
+ # Check the status. You can only detach an in-use volume, otherwise
+ # the compute API will return a 400 response.
+ if volume['status'] == 'in-use':
+ self.servers_client.detach_volume(server['id'], volume['id'])
+ except lib_exc.NotFound:
+ # Ignore 404s on detach in case the server is deleted or the volume
+ # is already detached.
+ pass
+
+ def attach_volume(self, server, volume, device=None, tag=None):
+ """Attaches volume to server and waits for 'in-use' volume status.
+
+ The volume will be detached when the test tears down.
+
+ :param server: The server to which the volume will be attached.
+ :param volume: The volume to attach.
+ :param device: Optional mountpoint for the attached volume. Note that
+ this is not guaranteed for all hypervisors and is not recommended.
+ :param tag: Optional device role tag to apply to the volume.
+ """
+ attach_kwargs = dict(volumeId=volume['id'])
+ if device:
+ attach_kwargs['device'] = device
+ if tag:
+ attach_kwargs['tag'] = tag
+
+ attachment = self.servers_client.attach_volume(
+ server['id'], **attach_kwargs)['volumeAttachment']
+ # On teardown detach the volume and wait for it to be available. This
+ # is so we don't error out when trying to delete the volume during
+ # teardown.
+ self.addCleanup(waiters.wait_for_volume_resource_status,
+ self.volumes_client, volume['id'], 'available')
+ # Ignore 404s on detach in case the server is deleted or the volume
+ # is already detached.
+ self.addCleanup(self._detach_volume, server, volume)
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume['id'], 'in-use')
+ return attachment
class BaseV2ComputeAdminTest(BaseV2ComputeTest):
@@ -375,4 +571,35 @@
def setup_clients(cls):
super(BaseV2ComputeAdminTest, cls).setup_clients()
cls.availability_zone_admin_client = (
- cls.os_adm.availability_zone_client)
+ cls.os_admin.availability_zone_client)
+ cls.admin_flavors_client = cls.os_admin.flavors_client
+ cls.admin_servers_client = cls.os_admin.servers_client
+
+ def create_flavor(self, ram, vcpus, disk, name=None,
+ is_public='True', **kwargs):
+ if name is None:
+ name = data_utils.rand_name(self.__class__.__name__ + "-flavor")
+ id = kwargs.pop('id', data_utils.rand_int_id(start=1000))
+ client = self.admin_flavors_client
+ flavor = client.create_flavor(
+ ram=ram, vcpus=vcpus, disk=disk, name=name,
+ id=id, is_public=is_public, **kwargs)['flavor']
+ self.addCleanup(client.wait_for_resource_deletion, flavor['id'])
+ self.addCleanup(client.delete_flavor, flavor['id'])
+ return flavor
+
+ def get_host_for_server(self, server_id):
+ server_details = self.admin_servers_client.show_server(server_id)
+ return server_details['server']['OS-EXT-SRV-ATTR:host']
+
+ def get_host_other_than(self, server_id):
+ source_host = self.get_host_for_server(server_id)
+
+ 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:
+ return target_host
diff --git a/tempest/api/compute/certificates/test_certificates.py b/tempest/api/compute/certificates/test_certificates.py
index d5c7302..0e6c016 100644
--- a/tempest/api/compute/certificates/test_certificates.py
+++ b/tempest/api/compute/certificates/test_certificates.py
@@ -15,7 +15,7 @@
from tempest.api.compute import base
from tempest import config
-from tempest import test
+from tempest.lib import decorators
CONF = config.CONF
@@ -28,17 +28,12 @@
if not CONF.compute_feature_enabled.nova_cert:
raise cls.skipException("Nova cert is not available")
- @test.idempotent_id('c070a441-b08e-447e-a733-905909535b1b')
+ @decorators.idempotent_id('c070a441-b08e-447e-a733-905909535b1b')
def test_create_root_certificate(self):
# create certificates
- body = self.certificates_client.create_certificate()['certificate']
- self.assertIn('data', body)
- self.assertIn('private_key', body)
+ self.certificates_client.create_certificate()
- @test.idempotent_id('3ac273d0-92d2-4632-bdfc-afbc21d4606c')
+ @decorators.idempotent_id('3ac273d0-92d2-4632-bdfc-afbc21d4606c')
def test_get_root_certificate(self):
# get the root certificate
- body = (self.certificates_client.show_certificate('root')
- ['certificate'])
- self.assertIn('data', body)
- self.assertIn('private_key', body)
+ self.certificates_client.show_certificate('root')
diff --git a/tempest/api/compute/flavors/test_flavors.py b/tempest/api/compute/flavors/test_flavors.py
index 7e01296..20294e9 100644
--- a/tempest/api/compute/flavors/test_flavors.py
+++ b/tempest/api/compute/flavors/test_flavors.py
@@ -14,114 +14,111 @@
# under the License.
from tempest.api.compute import base
-from tempest import test
+from tempest.lib import decorators
class FlavorsV2TestJSON(base.BaseV2ComputeTest):
- _min_disk = 'minDisk'
- _min_ram = 'minRam'
- @classmethod
- def setup_clients(cls):
- super(FlavorsV2TestJSON, cls).setup_clients()
- cls.client = cls.flavors_client
-
- @test.attr(type='smoke')
- @test.idempotent_id('e36c0eaa-dff5-4082-ad1f-3f9a80aa3f59')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('e36c0eaa-dff5-4082-ad1f-3f9a80aa3f59')
def test_list_flavors(self):
# List of all flavors should contain the expected flavor
- flavors = self.client.list_flavors()['flavors']
- flavor = self.client.show_flavor(self.flavor_ref)['flavor']
+ flavors = self.flavors_client.list_flavors()['flavors']
+ flavor = self.flavors_client.show_flavor(self.flavor_ref)['flavor']
flavor_min_detail = {'id': flavor['id'], 'links': flavor['links'],
'name': flavor['name']}
self.assertIn(flavor_min_detail, flavors)
- @test.idempotent_id('6e85fde4-b3cd-4137-ab72-ed5f418e8c24')
+ @decorators.idempotent_id('6e85fde4-b3cd-4137-ab72-ed5f418e8c24')
def test_list_flavors_with_detail(self):
# Detailed list of all flavors should contain the expected flavor
- flavors = self.client.list_flavors(detail=True)['flavors']
- flavor = self.client.show_flavor(self.flavor_ref)['flavor']
+ flavors = self.flavors_client.list_flavors(detail=True)['flavors']
+ flavor = self.flavors_client.show_flavor(self.flavor_ref)['flavor']
self.assertIn(flavor, flavors)
- @test.attr(type='smoke')
- @test.idempotent_id('1f12046b-753d-40d2-abb6-d8eb8b30cb2f')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('1f12046b-753d-40d2-abb6-d8eb8b30cb2f')
def test_get_flavor(self):
# The expected flavor details should be returned
- flavor = self.client.show_flavor(self.flavor_ref)['flavor']
+ flavor = self.flavors_client.show_flavor(self.flavor_ref)['flavor']
self.assertEqual(self.flavor_ref, flavor['id'])
- @test.idempotent_id('8d7691b3-6ed4-411a-abc9-2839a765adab')
+ @decorators.idempotent_id('8d7691b3-6ed4-411a-abc9-2839a765adab')
def test_list_flavors_limit_results(self):
# Only the expected number of flavors should be returned
params = {'limit': 1}
- flavors = self.client.list_flavors(**params)['flavors']
+ flavors = self.flavors_client.list_flavors(**params)['flavors']
self.assertEqual(1, len(flavors))
- @test.idempotent_id('b26f6327-2886-467a-82be-cef7a27709cb')
+ @decorators.idempotent_id('b26f6327-2886-467a-82be-cef7a27709cb')
def test_list_flavors_detailed_limit_results(self):
# Only the expected number of flavors (detailed) should be returned
params = {'limit': 1}
- flavors = self.client.list_flavors(detail=True, **params)['flavors']
+ flavors = self.flavors_client.list_flavors(detail=True,
+ **params)['flavors']
self.assertEqual(1, len(flavors))
- @test.idempotent_id('e800f879-9828-4bd0-8eae-4f17189951fb')
+ @decorators.idempotent_id('e800f879-9828-4bd0-8eae-4f17189951fb')
def test_list_flavors_using_marker(self):
# The list of flavors should start from the provided marker
- flavor = self.client.show_flavor(self.flavor_ref)['flavor']
+ flavor = self.flavors_client.show_flavor(self.flavor_ref)['flavor']
flavor_id = flavor['id']
params = {'marker': flavor_id}
- flavors = self.client.list_flavors(**params)['flavors']
- self.assertFalse(any([i for i in flavors if i['id'] == flavor_id]),
+ flavors = self.flavors_client.list_flavors(**params)['flavors']
+ self.assertEmpty([i for i in flavors if i['id'] == flavor_id],
'The list of flavors did not start after the marker.')
- @test.idempotent_id('6db2f0c0-ddee-4162-9c84-0703d3dd1107')
+ @decorators.idempotent_id('6db2f0c0-ddee-4162-9c84-0703d3dd1107')
def test_list_flavors_detailed_using_marker(self):
# The list of flavors should start from the provided marker
- flavor = self.client.show_flavor(self.flavor_ref)['flavor']
+ flavor = self.flavors_client.show_flavor(self.flavor_ref)['flavor']
flavor_id = flavor['id']
params = {'marker': flavor_id}
- flavors = self.client.list_flavors(detail=True, **params)['flavors']
- self.assertFalse(any([i for i in flavors if i['id'] == flavor_id]),
+ flavors = self.flavors_client.list_flavors(detail=True,
+ **params)['flavors']
+ self.assertEmpty([i for i in flavors if i['id'] == flavor_id],
'The list of flavors did not start after the marker.')
- @test.idempotent_id('3df2743e-3034-4e57-a4cb-b6527f6eac79')
+ @decorators.idempotent_id('3df2743e-3034-4e57-a4cb-b6527f6eac79')
def test_list_flavors_detailed_filter_by_min_disk(self):
# The detailed list of flavors should be filtered by disk space
- flavor = self.client.show_flavor(self.flavor_ref)['flavor']
+ flavor = self.flavors_client.show_flavor(self.flavor_ref)['flavor']
flavor_id = flavor['id']
- params = {self._min_disk: flavor['disk'] + 1}
- flavors = self.client.list_flavors(detail=True, **params)['flavors']
- self.assertFalse(any([i for i in flavors if i['id'] == flavor_id]))
+ params = {'minDisk': flavor['disk'] + 1}
+ flavors = self.flavors_client.list_flavors(detail=True,
+ **params)['flavors']
+ self.assertEmpty([i for i in flavors if i['id'] == flavor_id])
- @test.idempotent_id('09fe7509-b4ee-4b34-bf8b-39532dc47292')
+ @decorators.idempotent_id('09fe7509-b4ee-4b34-bf8b-39532dc47292')
def test_list_flavors_detailed_filter_by_min_ram(self):
# The detailed list of flavors should be filtered by RAM
- flavor = self.client.show_flavor(self.flavor_ref)['flavor']
+ flavor = self.flavors_client.show_flavor(self.flavor_ref)['flavor']
flavor_id = flavor['id']
- params = {self._min_ram: flavor['ram'] + 1}
- flavors = self.client.list_flavors(detail=True, **params)['flavors']
- self.assertFalse(any([i for i in flavors if i['id'] == flavor_id]))
+ params = {'minRam': flavor['ram'] + 1}
+ flavors = self.flavors_client.list_flavors(detail=True,
+ **params)['flavors']
+ self.assertEmpty([i for i in flavors if i['id'] == flavor_id])
- @test.idempotent_id('10645a4d-96f5-443f-831b-730711e11dd4')
+ @decorators.idempotent_id('10645a4d-96f5-443f-831b-730711e11dd4')
def test_list_flavors_filter_by_min_disk(self):
# The list of flavors should be filtered by disk space
- flavor = self.client.show_flavor(self.flavor_ref)['flavor']
+ flavor = self.flavors_client.show_flavor(self.flavor_ref)['flavor']
flavor_id = flavor['id']
- params = {self._min_disk: flavor['disk'] + 1}
- flavors = self.client.list_flavors(**params)['flavors']
- self.assertFalse(any([i for i in flavors if i['id'] == flavor_id]))
+ params = {'minDisk': flavor['disk'] + 1}
+ flavors = self.flavors_client.list_flavors(**params)['flavors']
+ self.assertEmpty([i for i in flavors if i['id'] == flavor_id])
- @test.idempotent_id('935cf550-e7c8-4da6-8002-00f92d5edfaa')
+ @decorators.idempotent_id('935cf550-e7c8-4da6-8002-00f92d5edfaa')
def test_list_flavors_filter_by_min_ram(self):
# The list of flavors should be filtered by RAM
- flavor = self.client.show_flavor(self.flavor_ref)['flavor']
+ flavor = self.flavors_client.show_flavor(self.flavor_ref)['flavor']
flavor_id = flavor['id']
- params = {self._min_ram: flavor['ram'] + 1}
- flavors = self.client.list_flavors(**params)['flavors']
- self.assertFalse(any([i for i in flavors if i['id'] == flavor_id]))
+ params = {'minRam': flavor['ram'] + 1}
+ flavors = self.flavors_client.list_flavors(**params)['flavors']
+ self.assertEmpty([i for i in flavors if i['id'] == flavor_id])
diff --git a/tempest/api/compute/flavors/test_flavors_negative.py b/tempest/api/compute/flavors/test_flavors_negative.py
index 83f8e19..3a474e6 100644
--- a/tempest/api/compute/flavors/test_flavors_negative.py
+++ b/tempest/api/compute/flavors/test_flavors_negative.py
@@ -1,4 +1,4 @@
-# Copyright 2013 OpenStack Foundation
+# Copyright 2017 Red Hat, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -13,31 +13,66 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest.api.compute import base
-from tempest.api_schema.request.compute.v2 import flavors
-from tempest import config
-from tempest import test
+import random
+import six
+
+from tempest.api.compute import base
+from tempest.common import image as common_image
+from tempest.common import utils
+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
-load_tests = test.NegativeAutoTest.load_tests
+class FlavorsV2NegativeTest(base.BaseV2ComputeTest):
-@test.SimpleNegativeAutoTest
-class FlavorsListWithDetailsNegativeTestJSON(base.BaseV2ComputeTest,
- test.NegativeAutoTest):
- _service = CONF.compute.catalog_type
- _schema = flavors.flavor_list
+ @decorators.attr(type=['negative'])
+ @utils.services('image')
+ @decorators.idempotent_id('90f0d93a-91c1-450c-91e6-07d18172cefe')
+ def test_boot_with_low_ram(self):
+ """Try boot a vm with lower than min ram
+ Create an image with min_ram value
+ Try to create server with flavor of insufficient ram size from
+ that image
+ """
+ flavor = self.flavors_client.show_flavor(
+ CONF.compute.flavor_ref)['flavor']
+ min_img_ram = flavor['ram'] + 1
+ size = random.randint(1024, 4096)
+ image_file = six.BytesIO(data_utils.random_bytes(size))
+ params = {
+ 'name': data_utils.rand_name('image'),
+ 'container_format': CONF.image.container_formats[0],
+ 'disk_format': CONF.image.disk_formats[0],
+ 'min_ram': min_img_ram
+ }
-@test.SimpleNegativeAutoTest
-class FlavorDetailsNegativeTestJSON(base.BaseV2ComputeTest,
- test.NegativeAutoTest):
- _service = CONF.compute.catalog_type
- _schema = flavors.flavors_details
+ if CONF.image_feature_enabled.api_v1:
+ params.update({'is_public': False})
+ params = {'headers': common_image.image_meta_to_headers(**params)}
+ else:
+ params.update({'visibility': 'private'})
- @classmethod
- def resource_setup(cls):
- super(FlavorDetailsNegativeTestJSON, cls).resource_setup()
- cls.set_resource("flavor", cls.flavor_ref)
+ image = self.images_client.create_image(**params)
+ image = image['image'] if 'image' in image else image
+ self.addCleanup(self.images_client.delete_image, image['id'])
+
+ if CONF.image_feature_enabled.api_v1:
+ self.images_client.update_image(image['id'], data=image_file)
+ else:
+ self.images_client.store_image_file(image['id'], data=image_file)
+
+ self.assertEqual(min_img_ram, image['min_ram'])
+
+ # Try to create server with flavor of insufficient ram size
+ self.assertRaisesRegex(lib_exc.BadRequest,
+ "Flavor's memory is too small for "
+ "requested image",
+ self.create_test_server,
+ image_id=image['id'],
+ flavor=flavor['id'])
diff --git a/tempest/api/compute/floating_ips/base.py b/tempest/api/compute/floating_ips/base.py
index 142eaec..262a3c1 100644
--- a/tempest/api/compute/floating_ips/base.py
+++ b/tempest/api/compute/floating_ips/base.py
@@ -14,6 +14,10 @@
# under the License.
from tempest.api.compute import base
+from tempest.common import utils
+from tempest import config
+
+CONF = config.CONF
class BaseFloatingIPsTest(base.BaseV2ComputeTest):
@@ -24,3 +28,17 @@
cls.set_network_resources(network=True, subnet=True,
router=True, dhcp=True)
super(BaseFloatingIPsTest, cls).setup_credentials()
+
+ @classmethod
+ def skip_checks(cls):
+ super(BaseFloatingIPsTest, cls).skip_checks()
+ if not utils.get_service_list()['network']:
+ raise cls.skipException("network service not enabled.")
+ if not CONF.network_feature_enabled.floating_ips:
+ raise cls.skipException("Floating ips are not available")
+
+ @classmethod
+ def setup_clients(cls):
+ super(BaseFloatingIPsTest, cls).setup_clients()
+ cls.client = cls.floating_ips_client
+ cls.pools_client = cls.floating_ip_pools_client
diff --git a/tempest/api/compute/floating_ips/test_floating_ips_actions.py b/tempest/api/compute/floating_ips/test_floating_ips_actions.py
index 5b90641..2adc482 100644
--- a/tempest/api/compute/floating_ips/test_floating_ips_actions.py
+++ b/tempest/api/compute/floating_ips/test_floating_ips_actions.py
@@ -13,57 +13,27 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
+import testtools
from tempest.api.compute.floating_ips import base
-from tempest.common.utils import data_utils
-from tempest.common import waiters
-from tempest import test
+from tempest import config
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+CONF = config.CONF
class FloatingIPsTestJSON(base.BaseFloatingIPsTest):
- server_id = None
- floating_ip = None
- @classmethod
- def setup_clients(cls):
- super(FloatingIPsTestJSON, cls).setup_clients()
- cls.client = cls.floating_ips_client
+ max_microversion = '2.35'
- @classmethod
- def resource_setup(cls):
- super(FloatingIPsTestJSON, cls).resource_setup()
- cls.floating_ip_id = None
-
- # Server creation
- server = cls.create_test_server(wait_until='ACTIVE')
- cls.server_id = server['id']
- # Floating IP creation
- body = cls.client.create_floating_ip()['floating_ip']
- cls.floating_ip_id = body['id']
- cls.floating_ip = body['ip']
-
- @classmethod
- def resource_cleanup(cls):
- # Deleting the floating IP which is created in this method
- if cls.floating_ip_id:
- cls.client.delete_floating_ip(cls.floating_ip_id)
- super(FloatingIPsTestJSON, cls).resource_cleanup()
-
- def _try_delete_floating_ip(self, floating_ip_id):
- # delete floating ip, if it exists
- try:
- self.client.delete_floating_ip(floating_ip_id)
- # if not found, it depicts it was deleted in the test
- except lib_exc.NotFound:
- pass
-
- @test.idempotent_id('f7bfb946-297e-41b8-9e8c-aba8e9bb5194')
- @test.services('network')
+ @decorators.idempotent_id('f7bfb946-297e-41b8-9e8c-aba8e9bb5194')
def test_allocate_floating_ip(self):
# Positive test:Allocation of a new floating IP to a project
# should be successful
- body = self.client.create_floating_ip()['floating_ip']
+ body = self.client.create_floating_ip(
+ pool=CONF.network.floating_network_name)['floating_ip']
floating_ip_id_allocated = body['id']
self.addCleanup(self.client.delete_floating_ip,
floating_ip_id_allocated)
@@ -73,21 +43,42 @@
body = self.client.list_floating_ips()['floating_ips']
self.assertIn(floating_ip_details, body)
- @test.idempotent_id('de45e989-b5ca-4a9b-916b-04a52e7bbb8b')
- @test.services('network')
+ @decorators.idempotent_id('de45e989-b5ca-4a9b-916b-04a52e7bbb8b')
def test_delete_floating_ip(self):
# Positive test:Deletion of valid floating IP from project
# should be successful
# Creating the floating IP that is to be deleted in this method
- floating_ip_body = self.client.create_floating_ip()['floating_ip']
- self.addCleanup(self._try_delete_floating_ip, floating_ip_body['id'])
+ floating_ip_body = self.client.create_floating_ip(
+ pool=CONF.network.floating_network_name)['floating_ip']
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.client.delete_floating_ip, floating_ip_body['id'])
# Deleting the floating IP from the project
self.client.delete_floating_ip(floating_ip_body['id'])
# Check it was really deleted.
self.client.wait_for_resource_deletion(floating_ip_body['id'])
- @test.idempotent_id('307efa27-dc6f-48a0-8cd2-162ce3ef0b52')
- @test.services('network')
+
+class FloatingIPsAssociationTestJSON(base.BaseFloatingIPsTest):
+
+ max_microversion = '2.43'
+
+ @classmethod
+ def resource_setup(cls):
+ super(FloatingIPsAssociationTestJSON, cls).resource_setup()
+
+ # Server creation
+ cls.server = cls.create_test_server(wait_until='ACTIVE')
+ cls.server_id = cls.server['id']
+ # Floating IP creation
+ body = cls.client.create_floating_ip(
+ pool=CONF.network.floating_network_name)['floating_ip']
+ cls.addClassResourceCleanup(cls.client.delete_floating_ip, body['id'])
+ cls.floating_ip_id = body['id']
+ cls.floating_ip = body['ip']
+
+ @decorators.idempotent_id('307efa27-dc6f-48a0-8cd2-162ce3ef0b52')
+ @testtools.skipUnless(CONF.network.public_network_id,
+ 'The public_network_id option must be specified.')
def test_associate_disassociate_floating_ip(self):
# Positive test:Associate and disassociate the provided floating IP
# to a specific server should be successful
@@ -107,16 +98,14 @@
self.floating_ip,
self.server_id)
- @test.idempotent_id('6edef4b2-aaf1-4abc-bbe3-993e2561e0fe')
- @test.services('network')
+ @decorators.idempotent_id('6edef4b2-aaf1-4abc-bbe3-993e2561e0fe')
+ @testtools.skipUnless(CONF.network.public_network_id,
+ 'The public_network_id option must be specified.')
def test_associate_already_associated_floating_ip(self):
# positive test:Association of an already associated floating IP
# to specific server should change the association of the Floating IP
# Create server so as to use for Multiple association
- new_name = data_utils.rand_name('floating_server')
- body = self.create_test_server(name=new_name)
- waiters.wait_for_server_status(self.servers_client,
- body['id'], 'ACTIVE')
+ body = self.create_test_server(wait_until='ACTIVE')
self.new_server_id = body['id']
self.addCleanup(self.servers_client.delete_server, self.new_server_id)
diff --git a/tempest/api/compute/floating_ips/test_floating_ips_actions_negative.py b/tempest/api/compute/floating_ips/test_floating_ips_actions_negative.py
index 0223c0d..9257458 100644
--- a/tempest/api/compute/floating_ips/test_floating_ips_actions_negative.py
+++ b/tempest/api/compute/floating_ips/test_floating_ips_actions_negative.py
@@ -13,48 +13,38 @@
# License for the specific language governing permissions and limitations
# under the License.
-import uuid
-
-from tempest_lib import exceptions as lib_exc
+import testtools
from tempest.api.compute.floating_ips import base
-from tempest.common.utils import data_utils
from tempest import config
-from tempest import test
+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 FloatingIPsNegativeTestJSON(base.BaseFloatingIPsTest):
- server_id = None
- @classmethod
- def setup_clients(cls):
- super(FloatingIPsNegativeTestJSON, cls).setup_clients()
- cls.client = cls.floating_ips_client
+ max_microversion = '2.35'
@classmethod
def resource_setup(cls):
super(FloatingIPsNegativeTestJSON, cls).resource_setup()
- # Server creation
- server = cls.create_test_server(wait_until='ACTIVE')
- cls.server_id = server['id']
# Generating a nonexistent floatingIP id
- cls.floating_ip_ids = []
body = cls.client.list_floating_ips()['floating_ips']
- for i in range(len(body)):
- cls.floating_ip_ids.append(body[i]['id'])
+ floating_ip_ids = [floating_ip['id'] for floating_ip in body]
while True:
- cls.non_exist_id = data_utils.rand_int_id(start=999)
if CONF.service_available.neutron:
- cls.non_exist_id = str(uuid.uuid4())
- if cls.non_exist_id not in cls.floating_ip_ids:
+ cls.non_exist_id = data_utils.rand_uuid()
+ else:
+ cls.non_exist_id = data_utils.rand_int_id(start=999)
+ if cls.non_exist_id not in floating_ip_ids:
break
- @test.attr(type=['negative'])
- @test.idempotent_id('6e0f059b-e4dd-48fb-8207-06e3bba5b074')
- @test.services('network')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('6e0f059b-e4dd-48fb-8207-06e3bba5b074')
def test_allocate_floating_ip_from_nonexistent_pool(self):
# Negative test:Allocation of a new floating IP from a nonexistent_pool
# to a project should fail
@@ -62,9 +52,8 @@
self.client.create_floating_ip,
pool="non_exist_pool")
- @test.attr(type=['negative'])
- @test.idempotent_id('ae1c55a8-552b-44d4-bfb6-2a115a15d0ba')
- @test.services('network')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('ae1c55a8-552b-44d4-bfb6-2a115a15d0ba')
def test_delete_nonexistent_floating_ip(self):
# Negative test:Deletion of a nonexistent floating IP
# from project should fail
@@ -73,9 +62,19 @@
self.assertRaises(lib_exc.NotFound, self.client.delete_floating_ip,
self.non_exist_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('595fa616-1a71-4670-9614-46564ac49a4c')
- @test.services('network')
+
+class FloatingIPsAssociationNegativeTestJSON(base.BaseFloatingIPsTest):
+
+ max_microversion = '2.43'
+
+ @classmethod
+ def resource_setup(cls):
+ super(FloatingIPsAssociationNegativeTestJSON, cls).resource_setup()
+ cls.server = cls.create_test_server(wait_until='ACTIVE')
+ cls.server_id = cls.server['id']
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('595fa616-1a71-4670-9614-46564ac49a4c')
def test_associate_nonexistent_floating_ip(self):
# Negative test:Association of a non existent floating IP
# to specific server should fail
@@ -84,9 +83,8 @@
self.client.associate_floating_ip_to_server,
"0.0.0.0", self.server_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('0a081a66-e568-4e6b-aa62-9587a876dca8')
- @test.services('network')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('0a081a66-e568-4e6b-aa62-9587a876dca8')
def test_dissociate_nonexistent_floating_ip(self):
# Negative test:Dissociation of a non existent floating IP should fail
# Dissociating non existent floating IP
@@ -94,12 +92,34 @@
self.client.disassociate_floating_ip_from_server,
"0.0.0.0", self.server_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('804b4fcb-bbf5-412f-925d-896672b61eb3')
- @test.services('network')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('804b4fcb-bbf5-412f-925d-896672b61eb3')
def test_associate_ip_to_server_without_passing_floating_ip(self):
# Negative test:Association of empty floating IP to specific server
# should raise NotFound or BadRequest(In case of Nova V2.1) exception.
self.assertRaises((lib_exc.NotFound, lib_exc.BadRequest),
self.client.associate_floating_ip_to_server,
'', self.server_id)
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('58a80596-ffb2-11e6-9393-fa163e4fa634')
+ @testtools.skipUnless(CONF.network.public_network_id,
+ 'The public_network_id option must be specified.')
+ def test_associate_ip_to_server_with_floating_ip(self):
+ # The VM have one port
+ # Associate floating IP A to the VM
+ # Associate floating IP B which is from same pool with floating IP A
+ # to the VM, should raise BadRequest exception
+ body = self.client.create_floating_ip(
+ pool=CONF.network.public_network_id)['floating_ip']
+ self.addCleanup(self.client.delete_floating_ip, body['id'])
+ self.client.associate_floating_ip_to_server(body['ip'], self.server_id)
+ self.addCleanup(self.client.disassociate_floating_ip_from_server,
+ body['ip'], self.server_id)
+
+ body = self.client.create_floating_ip(
+ pool=CONF.network.public_network_id)['floating_ip']
+ self.addCleanup(self.client.delete_floating_ip, body['id'])
+ self.assertRaises(lib_exc.BadRequest,
+ self.client.associate_floating_ip_to_server,
+ body['ip'], self.server_id)
diff --git a/tempest/api/compute/floating_ips/test_list_floating_ips.py b/tempest/api/compute/floating_ips/test_list_floating_ips.py
index d003967..944f798 100644
--- a/tempest/api/compute/floating_ips/test_list_floating_ips.py
+++ b/tempest/api/compute/floating_ips/test_list_floating_ips.py
@@ -13,51 +13,44 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest.api.compute import base
-from tempest import test
+from tempest.api.compute.floating_ips import base
+from tempest import config
+from tempest.lib import decorators
+
+CONF = config.CONF
-class FloatingIPDetailsTestJSON(base.BaseV2ComputeTest):
+class FloatingIPDetailsTestJSON(base.BaseFloatingIPsTest):
- @classmethod
- def setup_clients(cls):
- super(FloatingIPDetailsTestJSON, cls).setup_clients()
- cls.client = cls.floating_ips_client
- cls.pools_client = cls.floating_ip_pools_client
+ max_microversion = '2.35'
@classmethod
def resource_setup(cls):
super(FloatingIPDetailsTestJSON, cls).resource_setup()
cls.floating_ip = []
- cls.floating_ip_id = []
- for i in range(3):
- body = cls.client.create_floating_ip()['floating_ip']
+ for _ in range(3):
+ body = cls.client.create_floating_ip(
+ pool=CONF.network.floating_network_name)['floating_ip']
+ cls.addClassResourceCleanup(cls.client.delete_floating_ip,
+ body['id'])
cls.floating_ip.append(body)
- cls.floating_ip_id.append(body['id'])
- @classmethod
- def resource_cleanup(cls):
- for i in range(3):
- cls.client.delete_floating_ip(cls.floating_ip_id[i])
- super(FloatingIPDetailsTestJSON, cls).resource_cleanup()
-
- @test.idempotent_id('16db31c3-fb85-40c9-bbe2-8cf7b67ff99f')
- @test.services('network')
+ @decorators.idempotent_id('16db31c3-fb85-40c9-bbe2-8cf7b67ff99f')
def test_list_floating_ips(self):
# Positive test:Should return the list of floating IPs
body = self.client.list_floating_ips()['floating_ips']
floating_ips = body
- self.assertNotEqual(0, len(floating_ips),
+ self.assertNotEmpty(floating_ips,
"Expected floating IPs. Got zero.")
for i in range(3):
self.assertIn(self.floating_ip[i], floating_ips)
- @test.idempotent_id('eef497e0-8ff7-43c8-85ef-558440574f84')
- @test.services('network')
+ @decorators.idempotent_id('eef497e0-8ff7-43c8-85ef-558440574f84')
def test_get_floating_ip_details(self):
# Positive test:Should be able to GET the details of floatingIP
# Creating a floating IP for which details are to be checked
- body = self.client.create_floating_ip()['floating_ip']
+ body = self.client.create_floating_ip(
+ pool=CONF.network.floating_network_name)['floating_ip']
floating_ip_id = body['id']
self.addCleanup(self.client.delete_floating_ip,
floating_ip_id)
@@ -73,10 +66,9 @@
body['fixed_ip'])
self.assertEqual(floating_ip_id, body['id'])
- @test.idempotent_id('df389fc8-56f5-43cc-b290-20eda39854d3')
- @test.services('network')
+ @decorators.idempotent_id('df389fc8-56f5-43cc-b290-20eda39854d3')
def test_list_floating_ip_pools(self):
# Positive test:Should return the list of floating IP Pools
floating_ip_pools = self.pools_client.list_floating_ip_pools()
- self.assertNotEqual(0, len(floating_ip_pools['floating_ip_pools']),
+ self.assertNotEmpty(floating_ip_pools['floating_ip_pools'],
"Expected floating IP Pools. Got zero.")
diff --git a/tempest/api/compute/floating_ips/test_list_floating_ips_negative.py b/tempest/api/compute/floating_ips/test_list_floating_ips_negative.py
index 75b6b55..d69248c 100644
--- a/tempest/api/compute/floating_ips/test_list_floating_ips_negative.py
+++ b/tempest/api/compute/floating_ips/test_list_floating_ips_negative.py
@@ -13,34 +13,27 @@
# License for the specific language governing permissions and limitations
# under the License.
-import uuid
-
-from tempest_lib import exceptions as lib_exc
-
-from tempest.api.compute import base
-from tempest.common.utils import data_utils
+from tempest.api.compute.floating_ips import base
from tempest import config
-from tempest import test
+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 FloatingIPDetailsNegativeTestJSON(base.BaseV2ComputeTest):
+class FloatingIPDetailsNegativeTestJSON(base.BaseFloatingIPsTest):
- @classmethod
- def setup_clients(cls):
- super(FloatingIPDetailsNegativeTestJSON, cls).setup_clients()
- cls.client = cls.floating_ips_client
+ max_microversion = '2.35'
- @test.attr(type=['negative'])
- @test.idempotent_id('7ab18834-4a4b-4f28-a2c5-440579866695')
- @test.services('network')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('7ab18834-4a4b-4f28-a2c5-440579866695')
def test_get_nonexistent_floating_ip_details(self):
# Negative test:Should not be able to GET the details
# of non-existent floating IP
# Creating a non-existent floatingIP id
if CONF.service_available.neutron:
- non_exist_id = str(uuid.uuid4())
+ non_exist_id = data_utils.rand_uuid()
else:
non_exist_id = data_utils.rand_int_id(start=999)
self.assertRaises(lib_exc.NotFound,
diff --git a/tempest/api/compute/images/test_image_metadata.py b/tempest/api/compute/images/test_image_metadata.py
index 975b850..1f3af5f 100644
--- a/tempest/api/compute/images/test_image_metadata.py
+++ b/tempest/api/compute/images/test_image_metadata.py
@@ -16,15 +16,19 @@
import six
from tempest.api.compute import base
-from tempest.common.utils import data_utils
+from tempest.common import image as common_image
from tempest.common import waiters
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions
CONF = config.CONF
class ImagesMetadataTestJSON(base.BaseV2ComputeTest):
+ max_microversion = '2.38'
@classmethod
def skip_checks(cls):
@@ -36,23 +40,46 @@
@classmethod
def setup_clients(cls):
super(ImagesMetadataTestJSON, cls).setup_clients()
- cls.glance_client = cls.os.image_client
- cls.client = cls.images_client
+ # Check if glance v1 is available to determine which client to use. We
+ # prefer glance v1 for the compute API tests since the compute image
+ # API proxy was written for glance v1.
+ if CONF.image_feature_enabled.api_v1:
+ cls.glance_client = cls.os_primary.image_client
+ elif CONF.image_feature_enabled.api_v2:
+ cls.glance_client = cls.os_primary.image_client_v2
+ else:
+ raise exceptions.InvalidConfiguration(
+ 'Either api_v1 or api_v2 must be True in '
+ '[image-feature-enabled].')
+ cls.client = cls.compute_images_client
@classmethod
def resource_setup(cls):
super(ImagesMetadataTestJSON, cls).resource_setup()
cls.image_id = None
- name = data_utils.rand_name('image')
- body = cls.glance_client.create_image(name=name,
- container_format='bare',
- disk_format='raw',
- is_public=False)['image']
+ params = {
+ 'name': data_utils.rand_name('image'),
+ 'container_format': 'bare',
+ 'disk_format': 'raw'
+ }
+ if CONF.image_feature_enabled.api_v1:
+ params.update({'is_public': False})
+ params = {'headers': common_image.image_meta_to_headers(**params)}
+ else:
+ params.update({'visibility': 'private'})
+
+ body = cls.glance_client.create_image(**params)
+ body = body['image'] if 'image' in body else body
cls.image_id = body['id']
- cls.images.append(cls.image_id)
- image_file = six.StringIO(('*' * 1024))
- cls.glance_client.update_image(cls.image_id, data=image_file)
+ cls.addClassResourceCleanup(test_utils.call_and_ignore_notfound_exc,
+ cls.glance_client.delete_image,
+ cls.image_id)
+ image_file = six.BytesIO((b'*' * 1024))
+ if CONF.image_feature_enabled.api_v1:
+ cls.glance_client.update_image(cls.image_id, data=image_file)
+ else:
+ cls.glance_client.store_image_file(cls.image_id, data=image_file)
waiters.wait_for_image_status(cls.client, cls.image_id, 'ACTIVE')
def setUp(self):
@@ -60,7 +87,7 @@
meta = {'os_version': 'value1', 'os_distro': 'value2'}
self.client.set_image_metadata(self.image_id, meta)
- @test.idempotent_id('37ec6edd-cf30-4c53-bd45-ae74db6b0531')
+ @decorators.idempotent_id('37ec6edd-cf30-4c53-bd45-ae74db6b0531')
def test_list_image_metadata(self):
# All metadata key/value pairs for an image should be returned
resp_metadata = self.client.list_image_metadata(self.image_id)
@@ -68,7 +95,7 @@
'os_version': 'value1', 'os_distro': 'value2'}}
self.assertEqual(expected, resp_metadata)
- @test.idempotent_id('ece7befc-d3ce-42a4-b4be-c3067a418c29')
+ @decorators.idempotent_id('ece7befc-d3ce-42a4-b4be-c3067a418c29')
def test_set_image_metadata(self):
# The metadata for the image should match the new values
req_metadata = {'os_version': 'value2', 'architecture': 'value3'}
@@ -79,7 +106,7 @@
['metadata'])
self.assertEqual(req_metadata, resp_metadata)
- @test.idempotent_id('7b491c11-a9d5-40fe-a696-7f7e03d3fea2')
+ @decorators.idempotent_id('7b491c11-a9d5-40fe-a696-7f7e03d3fea2')
def test_update_image_metadata(self):
# The metadata for the image should match the updated values
req_metadata = {'os_version': 'alt1', 'architecture': 'value3'}
@@ -93,14 +120,14 @@
'architecture': 'value3'}}
self.assertEqual(expected, resp_metadata)
- @test.idempotent_id('4f5db52f-6685-4c75-b848-f4bb363f9aa6')
+ @decorators.idempotent_id('4f5db52f-6685-4c75-b848-f4bb363f9aa6')
def test_get_image_metadata_item(self):
# The value for a specific metadata key should be returned
meta = self.client.show_image_metadata_item(self.image_id,
'os_distro')['meta']
self.assertEqual('value2', meta['os_distro'])
- @test.idempotent_id('f2de776a-4778-4d90-a5da-aae63aee64ae')
+ @decorators.idempotent_id('f2de776a-4778-4d90-a5da-aae63aee64ae')
def test_set_image_metadata_item(self):
# The value provided for the given meta item should be set for
# the image
@@ -111,7 +138,7 @@
expected = {'metadata': {'os_version': 'alt', 'os_distro': 'value2'}}
self.assertEqual(expected, resp_metadata)
- @test.idempotent_id('a013796c-ba37-4bb5-8602-d944511def14')
+ @decorators.idempotent_id('a013796c-ba37-4bb5-8602-d944511def14')
def test_delete_image_metadata_item(self):
# The metadata value/key pair should be deleted from the image
self.client.delete_image_metadata_item(self.image_id,
diff --git a/tempest/api/compute/images/test_image_metadata_negative.py b/tempest/api/compute/images/test_image_metadata_negative.py
index 0f02166..407fb08 100644
--- a/tempest/api/compute/images/test_image_metadata_negative.py
+++ b/tempest/api/compute/images/test_image_metadata_negative.py
@@ -13,30 +13,30 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.compute import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
-class ImagesMetadataTestJSON(base.BaseV2ComputeTest):
+class ImagesMetadataNegativeTestJSON(base.BaseV2ComputeTest):
+ max_microversion = '2.38'
@classmethod
def setup_clients(cls):
- super(ImagesMetadataTestJSON, cls).setup_clients()
- cls.client = cls.images_client
+ super(ImagesMetadataNegativeTestJSON, cls).setup_clients()
+ cls.client = cls.compute_images_client
- @test.attr(type=['negative'])
- @test.idempotent_id('94069db2-792f-4fa8-8bd3-2271a6e0c095')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('94069db2-792f-4fa8-8bd3-2271a6e0c095')
def test_list_nonexistent_image_metadata(self):
# Negative test: List on nonexistent image
# metadata should not happen
self.assertRaises(lib_exc.NotFound, self.client.list_image_metadata,
data_utils.rand_uuid())
- @test.attr(type=['negative'])
- @test.idempotent_id('a403ef9e-9f95-427c-b70a-3ce3388796f1')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('a403ef9e-9f95-427c-b70a-3ce3388796f1')
def test_update_nonexistent_image_metadata(self):
# Negative test:An update should not happen for a non-existent image
meta = {'os_distro': 'alt1', 'os_version': 'alt2'}
@@ -44,24 +44,24 @@
self.client.update_image_metadata,
data_utils.rand_uuid(), meta)
- @test.attr(type=['negative'])
- @test.idempotent_id('41ae052c-6ee6-405c-985e-5712393a620d')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('41ae052c-6ee6-405c-985e-5712393a620d')
def test_get_nonexistent_image_metadata_item(self):
# Negative test: Get on non-existent image should not happen
self.assertRaises(lib_exc.NotFound,
self.client.show_image_metadata_item,
data_utils.rand_uuid(), 'os_version')
- @test.attr(type=['negative'])
- @test.idempotent_id('dc64f2ce-77e8-45b0-88c8-e15041d08eaf')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('dc64f2ce-77e8-45b0-88c8-e15041d08eaf')
def test_set_nonexistent_image_metadata(self):
# Negative test: Metadata should not be set to a non-existent image
meta = {'os_distro': 'alt1', 'os_version': 'alt2'}
self.assertRaises(lib_exc.NotFound, self.client.set_image_metadata,
data_utils.rand_uuid(), meta)
- @test.attr(type=['negative'])
- @test.idempotent_id('2154fd03-ab54-457c-8874-e6e3eb56e9cf')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('2154fd03-ab54-457c-8874-e6e3eb56e9cf')
def test_set_nonexistent_image_metadata_item(self):
# Negative test: Metadata item should not be set to a
# nonexistent image
@@ -71,8 +71,8 @@
data_utils.rand_uuid(), 'os_distro',
meta)
- @test.attr(type=['negative'])
- @test.idempotent_id('848e157f-6bcf-4b2e-a5dd-5124025a8518')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('848e157f-6bcf-4b2e-a5dd-5124025a8518')
def test_delete_nonexistent_image_metadata_item(self):
# Negative test: Shouldn't be able to delete metadata
# item from non-existent image
diff --git a/tempest/api/compute/images/test_images.py b/tempest/api/compute/images/test_images.py
index dc62620..29bd6da 100644
--- a/tempest/api/compute/images/test_images.py
+++ b/tempest/api/compute/images/test_images.py
@@ -13,10 +13,11 @@
# under the License.
from tempest.api.compute import base
-from tempest.common.utils import data_utils
from tempest.common import waiters
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+import testtools
CONF = config.CONF
@@ -37,20 +38,20 @@
@classmethod
def setup_clients(cls):
super(ImagesTestJSON, cls).setup_clients()
- cls.client = cls.images_client
- cls.servers_client = cls.servers_client
+ cls.client = cls.compute_images_client
- @test.idempotent_id('aa06b52b-2db5-4807-b218-9441f75d74e3')
+ @decorators.idempotent_id('aa06b52b-2db5-4807-b218-9441f75d74e3')
def test_delete_saving_image(self):
- snapshot_name = data_utils.rand_name('test-snap')
server = self.create_test_server(wait_until='ACTIVE')
self.addCleanup(self.servers_client.delete_server, server['id'])
image = self.create_image_from_server(server['id'],
- name=snapshot_name,
wait_until='SAVING')
self.client.delete_image(image['id'])
+ msg = ('The image with ID {image_id} failed to be deleted'
+ .format(image_id=image['id']))
+ self.assertTrue(self.client.is_resource_deleted(image['id']), msg)
- @test.idempotent_id('aaacd1d0-55a2-4ce8-818a-b5439df8adc9')
+ @decorators.idempotent_id('aaacd1d0-55a2-4ce8-818a-b5439df8adc9')
def test_create_image_from_stopped_server(self):
server = self.create_test_server(wait_until='ACTIVE')
self.servers_client.stop_server(server['id'])
@@ -64,3 +65,39 @@
wait_for_server=False)
self.addCleanup(self.client.delete_image, image['id'])
self.assertEqual(snapshot_name, image['name'])
+
+ @decorators.idempotent_id('71bcb732-0261-11e7-9086-fa163e4fa634')
+ @testtools.skipUnless(CONF.compute_feature_enabled.pause,
+ 'Pause is not available.')
+ def test_create_image_from_paused_server(self):
+ server = self.create_test_server(wait_until='ACTIVE')
+ self.servers_client.pause_server(server['id'])
+ waiters.wait_for_server_status(self.servers_client,
+ server['id'], 'PAUSED')
+ self.addCleanup(self.servers_client.delete_server, server['id'])
+
+ snapshot_name = data_utils.rand_name('test-snap')
+ image = self.create_image_from_server(server['id'],
+ name=snapshot_name,
+ wait_until='ACTIVE',
+ wait_for_server=False)
+ self.addCleanup(self.client.delete_image, image['id'])
+ self.assertEqual(snapshot_name, image['name'])
+
+ @decorators.idempotent_id('8ca07fec-0262-11e7-907e-fa163e4fa634')
+ @testtools.skipUnless(CONF.compute_feature_enabled.suspend,
+ 'Suspend is not available.')
+ def test_create_image_from_suspended_server(self):
+ server = self.create_test_server(wait_until='ACTIVE')
+ self.servers_client.suspend_server(server['id'])
+ waiters.wait_for_server_status(self.servers_client,
+ server['id'], 'SUSPENDED')
+ self.addCleanup(self.servers_client.delete_server, server['id'])
+
+ snapshot_name = data_utils.rand_name('test-snap')
+ image = self.create_image_from_server(server['id'],
+ name=snapshot_name,
+ wait_until='ACTIVE',
+ wait_for_server=False)
+ self.addCleanup(self.client.delete_image, image['id'])
+ self.assertEqual(snapshot_name, image['name'])
diff --git a/tempest/api/compute/images/test_images_negative.py b/tempest/api/compute/images/test_images_negative.py
index 9197adf..e292389 100644
--- a/tempest/api/compute/images/test_images_negative.py
+++ b/tempest/api/compute/images/test_images_negative.py
@@ -12,13 +12,12 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.compute import base
-from tempest.common.utils import data_utils
from tempest.common import waiters
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
CONF = config.CONF
@@ -39,39 +38,34 @@
@classmethod
def setup_clients(cls):
super(ImagesNegativeTestJSON, cls).setup_clients()
- cls.client = cls.images_client
- cls.servers_client = cls.servers_client
+ cls.client = cls.compute_images_client
- @test.attr(type=['negative'])
- @test.idempotent_id('6cd5a89d-5b47-46a7-93bc-3916f0d84973')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('6cd5a89d-5b47-46a7-93bc-3916f0d84973')
def test_create_image_from_deleted_server(self):
# An image should not be created if the server instance is removed
server = self.create_test_server(wait_until='ACTIVE')
- # Delete server before trying to create server
+ # Delete server before trying to create image
self.servers_client.delete_server(server['id'])
waiters.wait_for_server_termination(self.servers_client, server['id'])
# Create a new image after server is deleted
- name = data_utils.rand_name('image')
meta = {'image_type': 'test'}
self.assertRaises(lib_exc.NotFound,
self.create_image_from_server,
- server['id'], name=name, meta=meta)
+ server['id'], metadata=meta)
- @test.attr(type=['negative'])
- @test.idempotent_id('82c5b0c4-9dbd-463c-872b-20c4755aae7f')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('82c5b0c4-9dbd-463c-872b-20c4755aae7f')
def test_create_image_from_invalid_server(self):
# An image should not be created with invalid server id
# Create a new image with invalid server id
- name = data_utils.rand_name('image')
meta = {'image_type': 'test'}
- resp = {}
- resp['status'] = None
self.assertRaises(lib_exc.NotFound, self.create_image_from_server,
- '!@#$%^&*()', name=name, meta=meta)
+ data_utils.rand_name('invalid'), metadata=meta)
- @test.attr(type=['negative'])
- @test.idempotent_id('ec176029-73dc-4037-8d72-2e4ff60cf538')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('ec176029-73dc-4037-8d72-2e4ff60cf538')
def test_create_image_specify_uuid_35_characters_or_less(self):
# Return an error if Image ID passed is 35 characters or less
snapshot_name = data_utils.rand_name('test-snap')
@@ -79,8 +73,8 @@
self.assertRaises(lib_exc.NotFound, self.client.create_image,
test_uuid, name=snapshot_name)
- @test.attr(type=['negative'])
- @test.idempotent_id('36741560-510e-4cc2-8641-55fe4dfb2437')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('36741560-510e-4cc2-8641-55fe4dfb2437')
def test_create_image_specify_uuid_37_characters_or_more(self):
# Return an error if Image ID passed is 37 characters or more
snapshot_name = data_utils.rand_name('test-snap')
@@ -88,45 +82,46 @@
self.assertRaises(lib_exc.NotFound, self.client.create_image,
test_uuid, name=snapshot_name)
- @test.attr(type=['negative'])
- @test.idempotent_id('381acb65-785a-4942-94ce-d8f8c84f1f0f')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('381acb65-785a-4942-94ce-d8f8c84f1f0f')
def test_delete_image_with_invalid_image_id(self):
# An image should not be deleted with invalid image id
self.assertRaises(lib_exc.NotFound, self.client.delete_image,
- '!@$^&*()')
+ data_utils.rand_name('invalid'))
- @test.attr(type=['negative'])
- @test.idempotent_id('137aef61-39f7-44a1-8ddf-0adf82511701')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('137aef61-39f7-44a1-8ddf-0adf82511701')
def test_delete_non_existent_image(self):
# Return an error while trying to delete a non-existent image
- non_existent_image_id = '11a22b9-12a9-5555-cc11-00ab112223fa'
+ non_existent_image_id = data_utils.rand_uuid()
self.assertRaises(lib_exc.NotFound, self.client.delete_image,
non_existent_image_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('e6e41425-af5c-4fe6-a4b5-7b7b963ffda5')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('e6e41425-af5c-4fe6-a4b5-7b7b963ffda5')
def test_delete_image_blank_id(self):
# Return an error while trying to delete an image with blank Id
self.assertRaises(lib_exc.NotFound, self.client.delete_image, '')
- @test.attr(type=['negative'])
- @test.idempotent_id('924540c3-f1f1-444c-8f58-718958b6724e')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('924540c3-f1f1-444c-8f58-718958b6724e')
def test_delete_image_non_hex_string_id(self):
# Return an error while trying to delete an image with non hex id
- image_id = '11a22b9-120q-5555-cc11-00ab112223gj'
+ invalid_image_id = data_utils.rand_uuid()[:-1] + "j"
self.assertRaises(lib_exc.NotFound, self.client.delete_image,
- image_id)
+ invalid_image_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('68e2c175-bd26-4407-ac0f-4ea9ce2139ea')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('68e2c175-bd26-4407-ac0f-4ea9ce2139ea')
def test_delete_image_negative_image_id(self):
# Return an error while trying to delete an image with negative id
self.assertRaises(lib_exc.NotFound, self.client.delete_image, -1)
- @test.attr(type=['negative'])
- @test.idempotent_id('b340030d-82cd-4066-a314-c72fb7c59277')
- def test_delete_image_id_is_over_35_character_limit(self):
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('b340030d-82cd-4066-a314-c72fb7c59277')
+ def test_delete_image_with_id_over_character_limit(self):
# Return an error while trying to delete image with id over limit
+ invalid_image_id = data_utils.rand_uuid() + "1"
self.assertRaises(lib_exc.NotFound, self.client.delete_image,
- '11a22b9-12a9-5555-cc11-00ab112223fa-3fac')
+ invalid_image_id)
diff --git a/tempest/api/compute/images/test_images_oneserver.py b/tempest/api/compute/images/test_images_oneserver.py
index 37c2bb6..058e7e6 100644
--- a/tempest/api/compute/images/test_images_oneserver.py
+++ b/tempest/api/compute/images/test_images_oneserver.py
@@ -13,39 +13,21 @@
# License for the specific language governing permissions and limitations
# under the License.
-from oslo_log import log as logging
-
from tempest.api.compute import base
-from tempest.common.utils import data_utils
-from tempest.common import waiters
from tempest import config
-from tempest import test
+from tempest.lib.common import api_version_utils
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
CONF = config.CONF
-LOG = logging.getLogger(__name__)
class ImagesOneServerTestJSON(base.BaseV2ComputeTest):
- def tearDown(self):
- """Terminate test instances created after a test is executed."""
- self.server_check_teardown()
- super(ImagesOneServerTestJSON, self).tearDown()
-
- def setUp(self):
- # NOTE(afazekas): Normally we use the same server with all test cases,
- # but if it has an issue, we build a new one
- super(ImagesOneServerTestJSON, self).setUp()
- # Check if the server is in a clean state after test
- try:
- waiters.wait_for_server_status(self.servers_client,
- self.server_id, 'ACTIVE')
- except Exception:
- LOG.exception('server %s timed out to become ACTIVE. rebuilding'
- % self.server_id)
- # Rebuild server if cannot reach the ACTIVE state
- # Usually it means the server had a serious accident
- self.__class__.server_id = self.rebuild_server(self.server_id)
+ @classmethod
+ def resource_setup(cls):
+ super(ImagesOneServerTestJSON, cls).resource_setup()
+ cls.server_id = cls.create_test_server(wait_until='ACTIVE')['id']
@classmethod
def skip_checks(cls):
@@ -62,31 +44,22 @@
@classmethod
def setup_clients(cls):
super(ImagesOneServerTestJSON, cls).setup_clients()
- cls.client = cls.images_client
-
- @classmethod
- def resource_setup(cls):
- super(ImagesOneServerTestJSON, cls).resource_setup()
- server = cls.create_test_server(wait_until='ACTIVE')
- cls.server_id = server['id']
+ cls.client = cls.compute_images_client
def _get_default_flavor_disk_size(self, flavor_id):
flavor = self.flavors_client.show_flavor(flavor_id)['flavor']
return flavor['disk']
- @test.idempotent_id('3731d080-d4c5-4872-b41a-64d0d0021314')
+ @decorators.idempotent_id('3731d080-d4c5-4872-b41a-64d0d0021314')
def test_create_delete_image(self):
-
# Create a new image
name = data_utils.rand_name('image')
meta = {'image_type': 'test'}
- body = self.client.create_image(self.server_id, name=name,
- metadata=meta)
- image_id = data_utils.parse_image_id(body.response['location'])
- waiters.wait_for_image_status(self.client, image_id, 'ACTIVE')
+ image = self.create_image_from_server(self.server_id, name=name,
+ metadata=meta,
+ wait_until='ACTIVE')
# Verify the image was created correctly
- image = self.client.show_image(image_id)['image']
self.assertEqual(name, image['name'])
self.assertEqual('test', image['metadata']['image_type'])
@@ -101,18 +74,22 @@
(str(original_image['minDisk']), str(flavor_disk_size)))
# Verify the image was deleted correctly
- self.client.delete_image(image_id)
- self.client.wait_for_resource_deletion(image_id)
+ self.client.delete_image(image['id'])
+ self.client.wait_for_resource_deletion(image['id'])
- @test.idempotent_id('3b7c6fe4-dfe7-477c-9243-b06359db51e6')
+ @decorators.idempotent_id('3b7c6fe4-dfe7-477c-9243-b06359db51e6')
def test_create_image_specify_multibyte_character_image_name(self):
# prefix character is:
- # http://www.fileformat.info/info/unicode/char/1F4A9/index.htm
+ # http://unicode.org/cldr/utility/character.jsp?a=20A1
- # We use a string with 3 byte utf-8 character due to bug
- # #1370954 in glance which will 500 if mysql is used as the
- # backend and it attempts to store a 4 byte utf-8 character
- utf8_name = data_utils.rand_name('\xe2\x82\xa1')
+ # We use a string with 3 byte utf-8 character due to nova/glance which
+ # will return 400(Bad Request) if we attempt to send a name which has
+ # 4 byte utf-8 character.
+ utf8_name = data_utils.rand_name(b'\xe2\x82\xa1'.decode('utf-8'))
body = self.client.create_image(self.server_id, name=utf8_name)
- image_id = data_utils.parse_image_id(body.response['location'])
+ if api_version_utils.compare_version_header_to_response(
+ "OpenStack-API-Version", "compute 2.45", body.response, "lt"):
+ image_id = body['image_id']
+ else:
+ image_id = data_utils.parse_image_id(body.response['location'])
self.addCleanup(self.client.delete_image, image_id)
diff --git a/tempest/api/compute/images/test_images_oneserver_negative.py b/tempest/api/compute/images/test_images_oneserver_negative.py
index 9ea62fb..a2e58c9 100644
--- a/tempest/api/compute/images/test_images_oneserver_negative.py
+++ b/tempest/api/compute/images/test_images_oneserver_negative.py
@@ -15,13 +15,14 @@
# under the License.
from oslo_log import log as logging
-from tempest_lib import exceptions as lib_exc
from tempest.api.compute import base
-from tempest.common.utils import data_utils
from tempest.common import waiters
from tempest import config
-from tempest import test
+from tempest.lib.common import api_version_utils
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
CONF = config.CONF
@@ -32,9 +33,6 @@
def tearDown(self):
"""Terminate test instances created after a test is executed."""
- for image_id in self.image_ids:
- self.client.delete_image(image_id)
- self.image_ids.remove(image_id)
self.server_check_teardown()
super(ImagesOneServerNegativeTestJSON, self).tearDown()
@@ -47,14 +45,14 @@
waiters.wait_for_server_status(self.servers_client, self.server_id,
'ACTIVE')
except Exception:
- LOG.exception('server %s timed out to become ACTIVE. rebuilding'
- % self.server_id)
+ LOG.exception('server %s timed out to become ACTIVE. rebuilding',
+ self.server_id)
# Rebuild server if cannot reach the ACTIVE state
# Usually it means the server had a serious accident
self._reset_server()
def _reset_server(self):
- self.__class__.server_id = self.rebuild_server(self.server_id)
+ self.__class__.server_id = self.recreate_server(self.server_id)
@classmethod
def skip_checks(cls):
@@ -69,14 +67,9 @@
raise cls.skipException(skip_msg)
@classmethod
- def setup_credentials(cls):
- cls.prepare_instance_network()
- super(ImagesOneServerNegativeTestJSON, cls).setup_credentials()
-
- @classmethod
def setup_clients(cls):
super(ImagesOneServerNegativeTestJSON, cls).setup_clients()
- cls.client = cls.images_client
+ cls.client = cls.compute_images_client
@classmethod
def resource_setup(cls):
@@ -84,65 +77,66 @@
server = cls.create_test_server(wait_until='ACTIVE')
cls.server_id = server['id']
- cls.image_ids = []
-
- @test.attr(type=['negative'])
- @test.idempotent_id('55d1d38c-dd66-4933-9c8e-7d92aeb60ddc')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('55d1d38c-dd66-4933-9c8e-7d92aeb60ddc')
def test_create_image_specify_invalid_metadata(self):
# Return an error when creating image with invalid metadata
- snapshot_name = data_utils.rand_name('test-snap')
meta = {'': ''}
- self.assertRaises(lib_exc.BadRequest, self.client.create_image,
- self.server_id, name=snapshot_name, metadata=meta)
+ self.assertRaises(lib_exc.BadRequest, self.create_image_from_server,
+ self.server_id, metadata=meta)
- @test.attr(type=['negative'])
- @test.idempotent_id('3d24d11f-5366-4536-bd28-cff32b748eca')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('3d24d11f-5366-4536-bd28-cff32b748eca')
def test_create_image_specify_metadata_over_limits(self):
- # Return an error when creating image with meta data over 256 chars
- snapshot_name = data_utils.rand_name('test-snap')
- meta = {'a' * 260: 'b' * 260}
- self.assertRaises(lib_exc.BadRequest, self.client.create_image,
- self.server_id, name=snapshot_name, metadata=meta)
+ # Return an error when creating image with meta data over 255 chars
+ meta = {'a' * 256: 'b' * 256}
+ self.assertRaises(lib_exc.BadRequest, self.create_image_from_server,
+ self.server_id, metadata=meta)
- @test.attr(type=['negative'])
- @test.idempotent_id('0460efcf-ee88-4f94-acef-1bf658695456')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('0460efcf-ee88-4f94-acef-1bf658695456')
def test_create_second_image_when_first_image_is_being_saved(self):
# Disallow creating another image when first image is being saved
# Create first snapshot
- snapshot_name = data_utils.rand_name('test-snap')
- body = self.client.create_image(self.server_id, name=snapshot_name)
- image_id = data_utils.parse_image_id(body.response['location'])
- self.image_ids.append(image_id)
+ image = self.create_image_from_server(self.server_id)
self.addCleanup(self._reset_server)
# Create second snapshot
- alt_snapshot_name = data_utils.rand_name('test-snap')
- self.assertRaises(lib_exc.Conflict, self.client.create_image,
- self.server_id, name=alt_snapshot_name)
+ self.assertRaises(lib_exc.Conflict, self.create_image_from_server,
+ self.server_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('084f0cbc-500a-4963-8a4e-312905862581')
- def test_create_image_specify_name_over_256_chars(self):
- # Return an error if snapshot name over 256 characters is passed
+ if api_version_utils.compare_version_header_to_response(
+ "OpenStack-API-Version", "compute 2.45", image.response, "lt"):
+ image_id = image['image_id']
+ else:
+ image_id = data_utils.parse_image_id(image.response['location'])
+ self.client.delete_image(image_id)
- snapshot_name = data_utils.rand_name('a' * 260)
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('084f0cbc-500a-4963-8a4e-312905862581')
+ def test_create_image_specify_name_over_character_limit(self):
+ # Return an error if snapshot name over 255 characters is passed
+
+ snapshot_name = ('a' * 256)
self.assertRaises(lib_exc.BadRequest, self.client.create_image,
self.server_id, name=snapshot_name)
- @test.attr(type=['negative'])
- @test.idempotent_id('0894954d-2db2-4195-a45b-ffec0bc0187e')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('0894954d-2db2-4195-a45b-ffec0bc0187e')
def test_delete_image_that_is_not_yet_active(self):
# Return an error while trying to delete an image what is creating
- snapshot_name = data_utils.rand_name('test-snap')
- body = self.client.create_image(self.server_id, name=snapshot_name)
- image_id = data_utils.parse_image_id(body.response['location'])
- self.image_ids.append(image_id)
+ image = self.create_image_from_server(self.server_id)
+ if api_version_utils.compare_version_header_to_response(
+ "OpenStack-API-Version", "compute 2.45", image.response, "lt"):
+ image_id = image['image_id']
+ else:
+ image_id = data_utils.parse_image_id(image.response['location'])
+
self.addCleanup(self._reset_server)
# Do not wait, attempt to delete the image, ensure it's successful
self.client.delete_image(image_id)
- self.image_ids.remove(image_id)
-
- self.assertRaises(lib_exc.NotFound, self.client.show_image, image_id)
+ self.assertRaises(lib_exc.NotFound,
+ self.client.show_image, image_id)
diff --git a/tempest/api/compute/images/test_list_image_filters.py b/tempest/api/compute/images/test_list_image_filters.py
index 9f3ba71..d83d8df 100644
--- a/tempest/api/compute/images/test_list_image_filters.py
+++ b/tempest/api/compute/images/test_list_image_filters.py
@@ -15,20 +15,20 @@
import time
-from oslo_log import log as logging
import six
import testtools
from tempest.api.compute import base
-from tempest.common.utils import data_utils
+from tempest.common import image as common_image
from tempest.common import waiters
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions
CONF = config.CONF
-LOG = logging.getLogger(__name__)
-
class ListImageFiltersTestJSON(base.BaseV2ComputeTest):
@@ -42,26 +42,51 @@
@classmethod
def setup_clients(cls):
super(ListImageFiltersTestJSON, cls).setup_clients()
- cls.client = cls.images_client
- cls.glance_client = cls.os.image_client
+ cls.client = cls.compute_images_client
+ # Check if glance v1 is available to determine which client to use. We
+ # prefer glance v1 for the compute API tests since the compute image
+ # API proxy was written for glance v1.
+ if CONF.image_feature_enabled.api_v1:
+ cls.glance_client = cls.os_primary.image_client
+ elif CONF.image_feature_enabled.api_v2:
+ cls.glance_client = cls.os_primary.image_client_v2
+ else:
+ raise exceptions.InvalidConfiguration(
+ 'Either api_v1 or api_v2 must be True in '
+ '[image-feature-enabled].')
@classmethod
def resource_setup(cls):
super(ListImageFiltersTestJSON, cls).resource_setup()
def _create_image():
- name = data_utils.rand_name('image')
- body = cls.glance_client.create_image(name=name,
- container_format='bare',
- disk_format='raw',
- is_public=False)['image']
+ params = {
+ 'name': data_utils.rand_name(cls.__name__ + '-image'),
+ 'container_format': 'bare',
+ 'disk_format': 'raw'
+ }
+ if CONF.image_feature_enabled.api_v1:
+ params.update({'is_public': False})
+ params = {'headers':
+ common_image.image_meta_to_headers(**params)}
+ else:
+ params.update({'visibility': 'private'})
+
+ body = cls.glance_client.create_image(**params)
+ body = body['image'] if 'image' in body else body
image_id = body['id']
- cls.images.append(image_id)
+ cls.addClassResourceCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ cls.compute_images_client.delete_image,
+ image_id)
# Wait 1 second between creation and upload to ensure a delta
# between created_at and updated_at.
time.sleep(1)
- image_file = six.StringIO(('*' * 1024))
- cls.glance_client.update_image(image_id, data=image_file)
+ image_file = six.BytesIO((b'*' * 1024))
+ if CONF.image_feature_enabled.api_v1:
+ cls.glance_client.update_image(image_id, data=image_file)
+ else:
+ cls.glance_client.store_image_file(image_id, data=image_file)
waiters.wait_for_image_status(cls.client, image_id, 'ACTIVE')
body = cls.client.show_image(image_id)['image']
return body
@@ -101,29 +126,29 @@
cls.server1['id'], wait_until='ACTIVE')
cls.snapshot2_id = cls.snapshot2['id']
- @test.idempotent_id('a3f5b513-aeb3-42a9-b18e-f091ef73254d')
+ @decorators.idempotent_id('a3f5b513-aeb3-42a9-b18e-f091ef73254d')
def test_list_images_filter_by_status(self):
# The list of images should contain only images with the
# provided status
params = {'status': 'ACTIVE'}
images = self.client.list_images(**params)['images']
- self.assertTrue(any([i for i in images if i['id'] == self.image1_id]))
- self.assertTrue(any([i for i in images if i['id'] == self.image2_id]))
- self.assertTrue(any([i for i in images if i['id'] == self.image3_id]))
+ self.assertNotEmpty([i for i in images if i['id'] == self.image1_id])
+ self.assertNotEmpty([i for i in images if i['id'] == self.image2_id])
+ self.assertNotEmpty([i for i in images if i['id'] == self.image3_id])
- @test.idempotent_id('33163b73-79f5-4d07-a7ea-9213bcc468ff')
+ @decorators.idempotent_id('33163b73-79f5-4d07-a7ea-9213bcc468ff')
def test_list_images_filter_by_name(self):
# List of all images should contain the expected images filtered
# by name
params = {'name': self.image1['name']}
images = self.client.list_images(**params)['images']
- self.assertTrue(any([i for i in images if i['id'] == self.image1_id]))
- self.assertFalse(any([i for i in images if i['id'] == self.image2_id]))
- self.assertFalse(any([i for i in images if i['id'] == self.image3_id]))
+ self.assertNotEmpty([i for i in images if i['id'] == self.image1_id])
+ self.assertEmpty([i for i in images if i['id'] == self.image2_id])
+ self.assertEmpty([i for i in images if i['id'] == self.image3_id])
- @test.idempotent_id('9f238683-c763-45aa-b848-232ec3ce3105')
+ @decorators.idempotent_id('9f238683-c763-45aa-b848-232ec3ce3105')
@testtools.skipUnless(CONF.compute_feature_enabled.snapshot,
'Snapshotting is not available.')
def test_list_images_filter_by_server_id(self):
@@ -131,16 +156,15 @@
params = {'server': self.server1['id']}
images = self.client.list_images(**params)['images']
- self.assertTrue(any([i for i in images
- if i['id'] == self.snapshot1_id]),
- "Failed to find image %s in images. Got images %s" %
- (self.image1_id, images))
- self.assertTrue(any([i for i in images
- if i['id'] == self.snapshot2_id]))
- self.assertFalse(any([i for i in images
- if i['id'] == self.snapshot3_id]))
+ self.assertNotEmpty([i for i in images
+ if i['id'] == self.snapshot1_id],
+ "Failed to find image %s in images. "
+ "Got images %s" % (self.image1_id, images))
+ self.assertNotEmpty([i for i in images
+ if i['id'] == self.snapshot2_id])
+ self.assertEmpty([i for i in images if i['id'] == self.snapshot3_id])
- @test.idempotent_id('05a377b8-28cf-4734-a1e6-2ab5c38bf606')
+ @decorators.idempotent_id('05a377b8-28cf-4734-a1e6-2ab5c38bf606')
@testtools.skipUnless(CONF.compute_feature_enabled.snapshot,
'Snapshotting is not available.')
def test_list_images_filter_by_server_ref(self):
@@ -152,14 +176,14 @@
params = {'server': link['href']}
images = self.client.list_images(**params)['images']
- self.assertFalse(any([i for i in images
- if i['id'] == self.snapshot1_id]))
- self.assertFalse(any([i for i in images
- if i['id'] == self.snapshot2_id]))
- self.assertTrue(any([i for i in images
- if i['id'] == self.snapshot3_id]))
+ self.assertEmpty([i for i in images
+ if i['id'] == self.snapshot1_id])
+ self.assertEmpty([i for i in images
+ if i['id'] == self.snapshot2_id])
+ self.assertNotEmpty([i for i in images
+ if i['id'] == self.snapshot3_id])
- @test.idempotent_id('e3356918-4d3e-4756-81d5-abc4524ba29f')
+ @decorators.idempotent_id('e3356918-4d3e-4756-81d5-abc4524ba29f')
@testtools.skipUnless(CONF.compute_feature_enabled.snapshot,
'Snapshotting is not available.')
def test_list_images_filter_by_type(self):
@@ -167,23 +191,22 @@
params = {'type': 'snapshot'}
images = self.client.list_images(**params)['images']
- self.assertTrue(any([i for i in images
- if i['id'] == self.snapshot1_id]))
- self.assertTrue(any([i for i in images
- if i['id'] == self.snapshot2_id]))
- self.assertTrue(any([i for i in images
- if i['id'] == self.snapshot3_id]))
- self.assertFalse(any([i for i in images
- if i['id'] == self.image_ref]))
+ self.assertNotEmpty([i for i in images
+ if i['id'] == self.snapshot1_id])
+ self.assertNotEmpty([i for i in images
+ if i['id'] == self.snapshot2_id])
+ self.assertNotEmpty([i for i in images
+ if i['id'] == self.snapshot3_id])
+ self.assertEmpty([i for i in images if i['id'] == self.image_ref])
- @test.idempotent_id('3a484ca9-67ba-451e-b494-7fcf28d32d62')
+ @decorators.idempotent_id('3a484ca9-67ba-451e-b494-7fcf28d32d62')
def test_list_images_limit_results(self):
# Verify only the expected number of results are returned
params = {'limit': '1'}
images = self.client.list_images(**params)['images']
self.assertEqual(1, len([x for x in images if 'id' in x]))
- @test.idempotent_id('18bac3ae-da27-436c-92a9-b22474d13aab')
+ @decorators.idempotent_id('18bac3ae-da27-436c-92a9-b22474d13aab')
def test_list_images_filter_by_changes_since(self):
# Verify only updated images are returned in the detailed list
@@ -191,32 +214,32 @@
# Filter by the image's created time
params = {'changes-since': self.image3['created']}
images = self.client.list_images(**params)['images']
- found = any([i for i in images if i['id'] == self.image3_id])
- self.assertTrue(found)
+ found = [i for i in images if i['id'] == self.image3_id]
+ self.assertNotEmpty(found)
- @test.idempotent_id('9b0ea018-6185-4f71-948a-a123a107988e')
+ @decorators.idempotent_id('9b0ea018-6185-4f71-948a-a123a107988e')
def test_list_images_with_detail_filter_by_status(self):
# Detailed list of all images should only contain images
# with the provided status
params = {'status': 'ACTIVE'}
images = self.client.list_images(detail=True, **params)['images']
- self.assertTrue(any([i for i in images if i['id'] == self.image1_id]))
- self.assertTrue(any([i for i in images if i['id'] == self.image2_id]))
- self.assertTrue(any([i for i in images if i['id'] == self.image3_id]))
+ self.assertNotEmpty([i for i in images if i['id'] == self.image1_id])
+ self.assertNotEmpty([i for i in images if i['id'] == self.image2_id])
+ self.assertNotEmpty([i for i in images if i['id'] == self.image3_id])
- @test.idempotent_id('644ea267-9bd9-4f3b-af9f-dffa02396a17')
+ @decorators.idempotent_id('644ea267-9bd9-4f3b-af9f-dffa02396a17')
def test_list_images_with_detail_filter_by_name(self):
# Detailed list of all images should contain the expected
# images filtered by name
params = {'name': self.image1['name']}
images = self.client.list_images(detail=True, **params)['images']
- self.assertTrue(any([i for i in images if i['id'] == self.image1_id]))
- self.assertFalse(any([i for i in images if i['id'] == self.image2_id]))
- self.assertFalse(any([i for i in images if i['id'] == self.image3_id]))
+ self.assertNotEmpty([i for i in images if i['id'] == self.image1_id])
+ self.assertEmpty([i for i in images if i['id'] == self.image2_id])
+ self.assertEmpty([i for i in images if i['id'] == self.image3_id])
- @test.idempotent_id('ba2fa9a9-b672-47cc-b354-3b4c0600e2cb')
+ @decorators.idempotent_id('ba2fa9a9-b672-47cc-b354-3b4c0600e2cb')
def test_list_images_with_detail_limit_results(self):
# Verify only the expected number of results (with full details)
# are returned
@@ -224,7 +247,7 @@
images = self.client.list_images(detail=True, **params)['images']
self.assertEqual(1, len(images))
- @test.idempotent_id('8c78f822-203b-4bf6-8bba-56ebd551cf84')
+ @decorators.idempotent_id('8c78f822-203b-4bf6-8bba-56ebd551cf84')
@testtools.skipUnless(CONF.compute_feature_enabled.snapshot,
'Snapshotting is not available.')
def test_list_images_with_detail_filter_by_server_ref(self):
@@ -236,14 +259,14 @@
params = {'server': link['href']}
images = self.client.list_images(detail=True, **params)['images']
- self.assertFalse(any([i for i in images
- if i['id'] == self.snapshot1_id]))
- self.assertFalse(any([i for i in images
- if i['id'] == self.snapshot2_id]))
- self.assertTrue(any([i for i in images
- if i['id'] == self.snapshot3_id]))
+ self.assertEmpty([i for i in images
+ if i['id'] == self.snapshot1_id])
+ self.assertEmpty([i for i in images
+ if i['id'] == self.snapshot2_id])
+ self.assertNotEmpty([i for i in images
+ if i['id'] == self.snapshot3_id])
- @test.idempotent_id('888c0cc0-7223-43c5-9db0-b125fd0a393b')
+ @decorators.idempotent_id('888c0cc0-7223-43c5-9db0-b125fd0a393b')
@testtools.skipUnless(CONF.compute_feature_enabled.snapshot,
'Snapshotting is not available.')
def test_list_images_with_detail_filter_by_type(self):
@@ -252,16 +275,15 @@
images = self.client.list_images(detail=True, **params)['images']
self.client.show_image(self.image_ref)
- self.assertTrue(any([i for i in images
- if i['id'] == self.snapshot1_id]))
- self.assertTrue(any([i for i in images
- if i['id'] == self.snapshot2_id]))
- self.assertTrue(any([i for i in images
- if i['id'] == self.snapshot3_id]))
- self.assertFalse(any([i for i in images
- if i['id'] == self.image_ref]))
+ self.assertNotEmpty([i for i in images
+ if i['id'] == self.snapshot1_id])
+ self.assertNotEmpty([i for i in images
+ if i['id'] == self.snapshot2_id])
+ self.assertNotEmpty([i for i in images
+ if i['id'] == self.snapshot3_id])
+ self.assertEmpty([i for i in images if i['id'] == self.image_ref])
- @test.idempotent_id('7d439e18-ac2e-4827-b049-7e18004712c4')
+ @decorators.idempotent_id('7d439e18-ac2e-4827-b049-7e18004712c4')
def test_list_images_with_detail_filter_by_changes_since(self):
# Verify an update image is returned
@@ -269,4 +291,4 @@
# Filter by the image's created time
params = {'changes-since': self.image1['created']}
images = self.client.list_images(detail=True, **params)['images']
- self.assertTrue(any([i for i in images if i['id'] == self.image1_id]))
+ self.assertNotEmpty([i for i in images if i['id'] == self.image1_id])
diff --git a/tempest/api/compute/images/test_list_image_filters_negative.py b/tempest/api/compute/images/test_list_image_filters_negative.py
index 82062bd..d37f8fc 100644
--- a/tempest/api/compute/images/test_list_image_filters_negative.py
+++ b/tempest/api/compute/images/test_list_image_filters_negative.py
@@ -12,12 +12,11 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.compute import base
-from tempest.common.utils import data_utils
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
CONF = config.CONF
@@ -34,10 +33,10 @@
@classmethod
def setup_clients(cls):
super(ListImageFiltersNegativeTestJSON, cls).setup_clients()
- cls.client = cls.images_client
+ cls.client = cls.compute_images_client
- @test.attr(type=['negative'])
- @test.idempotent_id('391b0440-432c-4d4b-b5da-c5096aa247eb')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('391b0440-432c-4d4b-b5da-c5096aa247eb')
def test_get_nonexistent_image(self):
# Check raises a NotFound
nonexistent_image = data_utils.rand_uuid()
diff --git a/tempest/api/compute/images/test_list_images.py b/tempest/api/compute/images/test_list_images.py
index 6ca15d6..e2dbd72 100644
--- a/tempest/api/compute/images/test_list_images.py
+++ b/tempest/api/compute/images/test_list_images.py
@@ -15,7 +15,7 @@
from tempest.api.compute import base
from tempest import config
-from tempest import test
+from tempest.lib import decorators
CONF = config.CONF
@@ -32,24 +32,24 @@
@classmethod
def setup_clients(cls):
super(ListImagesTestJSON, cls).setup_clients()
- cls.client = cls.images_client
+ cls.client = cls.compute_images_client
- @test.idempotent_id('490d0898-e12a-463f-aef0-c50156b9f789')
+ @decorators.idempotent_id('490d0898-e12a-463f-aef0-c50156b9f789')
def test_get_image(self):
# Returns the correct details for a single image
image = self.client.show_image(self.image_ref)['image']
self.assertEqual(self.image_ref, image['id'])
- @test.idempotent_id('fd51b7f4-d4a3-4331-9885-866658112a6f')
+ @decorators.idempotent_id('fd51b7f4-d4a3-4331-9885-866658112a6f')
def test_list_images(self):
# The list of all images should contain the image
images = self.client.list_images()['images']
- found = any([i for i in images if i['id'] == self.image_ref])
- self.assertTrue(found)
+ found = [i for i in images if i['id'] == self.image_ref]
+ self.assertNotEmpty(found)
- @test.idempotent_id('9f94cb6b-7f10-48c5-b911-a0b84d7d4cd6')
+ @decorators.idempotent_id('9f94cb6b-7f10-48c5-b911-a0b84d7d4cd6')
def test_list_images_with_detail(self):
# Detailed list of all images should contain the expected images
images = self.client.list_images(detail=True)['images']
- found = any([i for i in images if i['id'] == self.image_ref])
- self.assertTrue(found)
+ found = [i for i in images if i['id'] == self.image_ref]
+ self.assertNotEmpty(found)
diff --git a/tempest/api/compute/keypairs/base.py b/tempest/api/compute/keypairs/base.py
index 15f231b..44da88c 100644
--- a/tempest/api/compute/keypairs/base.py
+++ b/tempest/api/compute/keypairs/base.py
@@ -14,23 +14,35 @@
# under the License.
from tempest.api.compute import base
+from tempest.lib.common.utils import data_utils
class BaseKeypairTest(base.BaseV2ComputeTest):
"""Base test case class for all keypair API tests."""
- @classmethod
- def setup_clients(cls):
- super(BaseKeypairTest, cls).setup_clients()
- cls.client = cls.keypairs_client
+ def _delete_keypair(self, keypair_name, client=None, **params):
+ if not client:
+ client = self.keypairs_client
+ client.delete_keypair(keypair_name, **params)
- def _delete_keypair(self, keypair_name):
- self.client.delete_keypair(keypair_name)
-
- def _create_keypair(self, keypair_name, pub_key=None):
+ def create_keypair(self, keypair_name=None,
+ pub_key=None, keypair_type=None,
+ user_id=None, client=None):
+ if not client:
+ client = self.keypairs_client
+ if keypair_name is None:
+ keypair_name = data_utils.rand_name(
+ self.__class__.__name__ + '-keypair')
kwargs = {'name': keypair_name}
+ delete_params = {}
if pub_key:
kwargs.update({'public_key': pub_key})
- body = self.client.create_keypair(**kwargs)['keypair']
- self.addCleanup(self._delete_keypair, keypair_name)
+ if keypair_type:
+ kwargs.update({'type': keypair_type})
+ if user_id:
+ kwargs.update({'user_id': user_id})
+ delete_params['user_id'] = user_id
+ body = client.create_keypair(**kwargs)['keypair']
+ self.addCleanup(self._delete_keypair, keypair_name,
+ client, **delete_params)
return body
diff --git a/tempest/api/compute/keypairs/test_keypairs.py b/tempest/api/compute/keypairs/test_keypairs.py
index d10bf14..66abb21 100644
--- a/tempest/api/compute/keypairs/test_keypairs.py
+++ b/tempest/api/compute/keypairs/test_keypairs.py
@@ -14,19 +14,20 @@
# under the License.
from tempest.api.compute.keypairs import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
class KeyPairsV2TestJSON(base.BaseKeypairTest):
- @test.idempotent_id('1d1dbedb-d7a0-432a-9d09-83f543c3c19b')
+ max_microversion = '2.1'
+
+ @decorators.idempotent_id('1d1dbedb-d7a0-432a-9d09-83f543c3c19b')
def test_keypairs_create_list_delete(self):
# Keypairs created should be available in the response list
# Create 3 keypairs
key_list = list()
- for i in range(3):
- k_name = data_utils.rand_name('keypair')
- keypair = self._create_keypair(k_name)
+ for _ in range(3):
+ keypair = self.create_keypair()
# Need to pop these keys so that our compare doesn't fail later,
# as the keypair dicts from list API doesn't have them.
keypair.pop('private_key')
@@ -34,7 +35,7 @@
key_list.append(keypair)
# Fetch all keypairs and verify the list
# has all created keypairs
- fetched_list = self.client.list_keypairs()['keypairs']
+ fetched_list = self.keypairs_client.list_keypairs()['keypairs']
new_list = list()
for keypair in fetched_list:
new_list.append(keypair['keypair'])
@@ -45,35 +46,27 @@
"Failed to find keypairs %s in fetched list"
% ', '.join(m_key['name'] for m_key in missing_kps))
- @test.idempotent_id('6c1d3123-4519-4742-9194-622cb1714b7d')
+ @decorators.idempotent_id('6c1d3123-4519-4742-9194-622cb1714b7d')
def test_keypair_create_delete(self):
# Keypair should be created, verified and deleted
k_name = data_utils.rand_name('keypair')
- keypair = self._create_keypair(k_name)
- private_key = keypair['private_key']
+ keypair = self.create_keypair(k_name)
key_name = keypair['name']
self.assertEqual(key_name, k_name,
"The created keypair name is not equal "
"to the requested name")
- self.assertTrue(private_key is not None,
- "Field private_key is empty or not found.")
- @test.idempotent_id('a4233d5d-52d8-47cc-9a25-e1864527e3df')
+ @decorators.idempotent_id('a4233d5d-52d8-47cc-9a25-e1864527e3df')
def test_get_keypair_detail(self):
# Keypair should be created, Got details by name and deleted
k_name = data_utils.rand_name('keypair')
- self._create_keypair(k_name)
- keypair_detail = self.client.show_keypair(k_name)['keypair']
- self.assertIn('name', keypair_detail)
- self.assertIn('public_key', keypair_detail)
+ self.create_keypair(k_name)
+ keypair_detail = self.keypairs_client.show_keypair(k_name)['keypair']
self.assertEqual(keypair_detail['name'], k_name,
"The created keypair name is not equal "
"to requested name")
- public_key = keypair_detail['public_key']
- self.assertTrue(public_key is not None,
- "Field public_key is empty or not found.")
- @test.idempotent_id('39c90c6a-304a-49dd-95ec-2366129def05')
+ @decorators.idempotent_id('39c90c6a-304a-49dd-95ec-2366129def05')
def test_keypair_create_with_pub_key(self):
# Keypair should be created with a given public key
k_name = data_utils.rand_name('keypair')
@@ -86,8 +79,8 @@
"LOeB1kYMOBaiUPLQTWXR3JpckqFIQwhIH0zoHlJvZE8hh90"
"XcPojYN56tI0OlrGqojbediJYD0rUsJu4weZpbn8vilb3JuDY+jws"
"snSA8wzBx3A/8y9Pp1B nova@ubuntu")
- keypair = self._create_keypair(k_name, pub_key)
- self.assertFalse('private_key' in keypair,
+ keypair = self.create_keypair(k_name, pub_key)
+ self.assertNotIn('private_key', keypair,
"Field private_key is not empty!")
key_name = keypair['name']
self.assertEqual(key_name, k_name,
diff --git a/tempest/api/compute/keypairs/test_keypairs_negative.py b/tempest/api/compute/keypairs/test_keypairs_negative.py
index 0ab78fb..f9050a8 100644
--- a/tempest/api/compute/keypairs/test_keypairs_negative.py
+++ b/tempest/api/compute/keypairs/test_keypairs_negative.py
@@ -14,79 +14,76 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.compute.keypairs import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class KeyPairsNegativeTestJSON(base.BaseKeypairTest):
- @test.attr(type=['negative'])
- @test.idempotent_id('29cca892-46ae-4d48-bc32-8fe7e731eb81')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('29cca892-46ae-4d48-bc32-8fe7e731eb81')
def test_keypair_create_with_invalid_pub_key(self):
# Keypair should not be created with a non RSA public key
- k_name = data_utils.rand_name('keypair')
pub_key = "ssh-rsa JUNK nova@ubuntu"
self.assertRaises(lib_exc.BadRequest,
- self._create_keypair, k_name, pub_key)
+ self.create_keypair, pub_key=pub_key)
- @test.attr(type=['negative'])
- @test.idempotent_id('7cc32e47-4c42-489d-9623-c5e2cb5a2fa5')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('7cc32e47-4c42-489d-9623-c5e2cb5a2fa5')
def test_keypair_delete_nonexistent_key(self):
# Non-existent key deletion should throw a proper error
k_name = data_utils.rand_name("keypair-non-existent")
- self.assertRaises(lib_exc.NotFound, self.client.delete_keypair,
+ self.assertRaises(lib_exc.NotFound,
+ self.keypairs_client.delete_keypair,
k_name)
- @test.attr(type=['negative'])
- @test.idempotent_id('dade320e-69ca-42a9-ba4a-345300f127e0')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('dade320e-69ca-42a9-ba4a-345300f127e0')
def test_create_keypair_with_empty_public_key(self):
# Keypair should not be created with an empty public key
- k_name = data_utils.rand_name("keypair")
pub_key = ' '
- self.assertRaises(lib_exc.BadRequest, self._create_keypair,
- k_name, pub_key)
+ self.assertRaises(lib_exc.BadRequest, self.create_keypair,
+ pub_key=pub_key)
- @test.attr(type=['negative'])
- @test.idempotent_id('fc100c19-2926-4b9c-8fdc-d0589ee2f9ff')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('fc100c19-2926-4b9c-8fdc-d0589ee2f9ff')
def test_create_keypair_when_public_key_bits_exceeds_maximum(self):
# Keypair should not be created when public key bits are too long
- k_name = data_utils.rand_name("keypair")
pub_key = 'ssh-rsa ' + 'A' * 2048 + ' openstack@ubuntu'
- self.assertRaises(lib_exc.BadRequest, self._create_keypair,
- k_name, pub_key)
+ self.assertRaises(lib_exc.BadRequest, self.create_keypair,
+ pub_key=pub_key)
- @test.attr(type=['negative'])
- @test.idempotent_id('0359a7f1-f002-4682-8073-0c91e4011b7c')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('0359a7f1-f002-4682-8073-0c91e4011b7c')
def test_create_keypair_with_duplicate_name(self):
# Keypairs with duplicate names should not be created
k_name = data_utils.rand_name('keypair')
- self.client.create_keypair(name=k_name)
+ self.keypairs_client.create_keypair(name=k_name)
# Now try the same keyname to create another key
- self.assertRaises(lib_exc.Conflict, self._create_keypair,
+ self.assertRaises(lib_exc.Conflict, self.create_keypair,
k_name)
- self.client.delete_keypair(k_name)
+ self.keypairs_client.delete_keypair(k_name)
- @test.attr(type=['negative'])
- @test.idempotent_id('1398abe1-4a84-45fb-9294-89f514daff00')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('1398abe1-4a84-45fb-9294-89f514daff00')
def test_create_keypair_with_empty_name_string(self):
# Keypairs with name being an empty string should not be created
- self.assertRaises(lib_exc.BadRequest, self._create_keypair,
+ self.assertRaises(lib_exc.BadRequest, self.create_keypair,
'')
- @test.attr(type=['negative'])
- @test.idempotent_id('3faa916f-779f-4103-aca7-dc3538eee1b7')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('3faa916f-779f-4103-aca7-dc3538eee1b7')
def test_create_keypair_with_long_keynames(self):
# Keypairs with name longer than 255 chars should not be created
k_name = 'keypair-'.ljust(260, '0')
- self.assertRaises(lib_exc.BadRequest, self._create_keypair,
+ self.assertRaises(lib_exc.BadRequest, self.create_keypair,
k_name)
- @test.attr(type=['negative'])
- @test.idempotent_id('45fbe5e0-acb5-49aa-837a-ff8d0719db91')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('45fbe5e0-acb5-49aa-837a-ff8d0719db91')
def test_create_keypair_invalid_name(self):
# Keypairs with name being an invalid name should not be created
k_name = 'key_/.\@:'
- self.assertRaises(lib_exc.BadRequest, self._create_keypair,
+ self.assertRaises(lib_exc.BadRequest, self.create_keypair,
k_name)
diff --git a/tempest/api/compute/keypairs/test_keypairs_v22.py b/tempest/api/compute/keypairs/test_keypairs_v22.py
new file mode 100644
index 0000000..1aff262
--- /dev/null
+++ b/tempest/api/compute/keypairs/test_keypairs_v22.py
@@ -0,0 +1,51 @@
+# Copyright 2016 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.keypairs import test_keypairs
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+
+
+class KeyPairsV22TestJSON(test_keypairs.KeyPairsV2TestJSON):
+ min_microversion = '2.2'
+ max_microversion = 'latest'
+
+ def _check_keypair_type(self, keypair, keypair_type):
+ if keypair_type is None:
+ keypair_type = 'ssh'
+ self.assertEqual(keypair_type, keypair['type'])
+
+ def _test_keypairs_create_list_show(self, keypair_type=None):
+ k_name = data_utils.rand_name('keypair')
+ keypair = self.create_keypair(k_name, keypair_type=keypair_type)
+ # Verify whether 'type' is present in keypair create response of
+ # version 2.2 and it is with default value 'ssh'.
+ self._check_keypair_type(keypair, keypair_type)
+ keypair_detail = self.keypairs_client.show_keypair(k_name)['keypair']
+ self._check_keypair_type(keypair_detail, keypair_type)
+ fetched_list = self.keypairs_client.list_keypairs()['keypairs']
+ for keypair in fetched_list:
+ # Verify whether 'type' is present in keypair list response of
+ # version 2.2 and it is with default value 'ssh'.
+ if keypair['keypair']['name'] == k_name:
+ self._check_keypair_type(keypair['keypair'], keypair_type)
+
+ @decorators.idempotent_id('8726fa85-7f98-4b20-af9e-f710a4f3391c')
+ def test_keypairsv22_create_list_show(self):
+ self._test_keypairs_create_list_show()
+
+ @decorators.idempotent_id('89d59d43-f735-441a-abcf-0601727f47b6')
+ def test_keypairsv22_create_list_show_with_type(self):
+ keypair_type = 'x509'
+ self._test_keypairs_create_list_show(keypair_type=keypair_type)
diff --git a/tempest/api/compute/limits/test_absolute_limits.py b/tempest/api/compute/limits/test_absolute_limits.py
index 69811f4..0585fec 100644
--- a/tempest/api/compute/limits/test_absolute_limits.py
+++ b/tempest/api/compute/limits/test_absolute_limits.py
@@ -14,7 +14,7 @@
# under the License.
from tempest.api.compute import base
-from tempest import test
+from tempest.lib import decorators
class AbsoluteLimitsTestJSON(base.BaseV2ComputeTest):
@@ -24,7 +24,7 @@
super(AbsoluteLimitsTestJSON, cls).setup_clients()
cls.client = cls.limits_client
- @test.idempotent_id('b54c66af-6ab6-4cf0-a9e5-a0cb58d75e0b')
+ @decorators.idempotent_id('b54c66af-6ab6-4cf0-a9e5-a0cb58d75e0b')
def test_absLimits_get(self):
# To check if all limits are present in the response
limits = self.client.show_limits()['limits']
@@ -35,12 +35,13 @@
'maxTotalFloatingIps', 'maxSecurityGroups',
'maxSecurityGroupRules', 'maxTotalInstances',
'maxTotalKeypairs', 'maxTotalRAMSize',
+ 'maxServerGroups', 'maxServerGroupMembers',
'totalCoresUsed', 'totalFloatingIpsUsed',
'totalSecurityGroupsUsed', 'totalInstancesUsed',
- 'totalRAMUsed']
+ 'totalRAMUsed', 'totalServerGroupsUsed']
# check whether all expected elements exist
missing_elements =\
[ele for ele in expected_elements if ele not in absolute_limits]
- self.assertEqual(0, len(missing_elements),
+ self.assertEmpty(missing_elements,
"Failed to find element %s in absolute limits list"
% ', '.join(ele for ele in missing_elements))
diff --git a/tempest/api/compute/limits/test_absolute_limits_negative.py b/tempest/api/compute/limits/test_absolute_limits_negative.py
index 73e852f..bef4eb5 100644
--- a/tempest/api/compute/limits/test_absolute_limits_negative.py
+++ b/tempest/api/compute/limits/test_absolute_limits_negative.py
@@ -13,11 +13,10 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.compute import base
from tempest.common import tempest_fixtures as fixtures
-from tempest import test
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class AbsoluteLimitsNegativeTestJSON(base.BaseV2ComputeTest):
@@ -31,10 +30,9 @@
def setup_clients(cls):
super(AbsoluteLimitsNegativeTestJSON, cls).setup_clients()
cls.client = cls.limits_client
- cls.server_client = cls.servers_client
- @test.attr(type=['negative'])
- @test.idempotent_id('215cd465-d8ae-49c9-bf33-9c911913a5c8')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('215cd465-d8ae-49c9-bf33-9c911913a5c8')
def test_max_image_meta_exceed_limit(self):
# We should not create vm with image meta over maxImageMeta limit
# Get max limit value
@@ -42,11 +40,11 @@
max_meta = limits['absolute']['maxImageMeta']
# No point in running this test if there is no limit.
- if int(max_meta) == -1:
+ if max_meta == -1:
raise self.skipException('no limit for maxImageMeta')
# Create server should fail, since we are passing > metadata Limit!
- max_meta_data = int(max_meta) + 1
+ max_meta_data = max_meta + 1
meta_data = {}
for xx in range(max_meta_data):
diff --git a/tempest/api/compute/security_groups/base.py b/tempest/api/compute/security_groups/base.py
index f70f6d3..49125d1 100644
--- a/tempest/api/compute/security_groups/base.py
+++ b/tempest/api/compute/security_groups/base.py
@@ -14,12 +14,32 @@
# under the License.
from tempest.api.compute import base
+from tempest.common import utils
+from tempest import config
+from tempest.lib.common.utils import data_utils
+
+CONF = config.CONF
class BaseSecurityGroupsTest(base.BaseV2ComputeTest):
+ max_microversion = '2.35'
+
+ @classmethod
+ def skip_checks(cls):
+ super(BaseSecurityGroupsTest, cls).skip_checks()
+ if not utils.get_service_list()['network']:
+ raise cls.skipException("network service not enabled.")
@classmethod
def setup_credentials(cls):
# A network and a subnet will be created for these tests
cls.set_network_resources(network=True, subnet=True)
super(BaseSecurityGroupsTest, cls).setup_credentials()
+
+ @staticmethod
+ def generate_random_security_group_id():
+ if (CONF.service_available.neutron and
+ utils.is_extension_enabled('security-group', 'network')):
+ return data_utils.rand_uuid()
+ else:
+ return data_utils.rand_int_id(start=999)
diff --git a/tempest/api/compute/security_groups/test_security_group_rules.py b/tempest/api/compute/security_groups/test_security_group_rules.py
index 9aa59f7..4c99ea6 100644
--- a/tempest/api/compute/security_groups/test_security_group_rules.py
+++ b/tempest/api/compute/security_groups/test_security_group_rules.py
@@ -14,10 +14,7 @@
# under the License.
from tempest.api.compute.security_groups import base
-from tempest import config
-from tempest import test
-
-CONF = config.CONF
+from tempest.lib import decorators
class SecurityGroupRulesTestJSON(base.BaseSecurityGroupsTest):
@@ -30,7 +27,6 @@
@classmethod
def resource_setup(cls):
super(SecurityGroupRulesTestJSON, cls).resource_setup()
- cls.neutron_available = CONF.service_available.neutron
cls.ip_protocol = 'tcp'
cls.from_port = 22
cls.to_port = 22
@@ -43,7 +39,6 @@
group = {}
ip_range = {}
cls.expected = {
- 'id': None,
'parent_group_id': None,
'ip_protocol': cls.ip_protocol,
'from_port': from_port,
@@ -54,14 +49,11 @@
def _check_expected_response(self, actual_rule):
for key in self.expected:
- if key == 'id':
- continue
self.assertEqual(self.expected[key], actual_rule[key],
"Miss-matched key is %s" % key)
- @test.attr(type='smoke')
- @test.idempotent_id('850795d7-d4d3-4e55-b527-a774c0123d3a')
- @test.services('network')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('850795d7-d4d3-4e55-b527-a774c0123d3a')
def test_security_group_rules_create(self):
# Positive test: Creation of Security Group rule
# should be successful
@@ -78,8 +70,7 @@
self.expected['ip_range'] = {'cidr': '0.0.0.0/0'}
self._check_expected_response(rule)
- @test.idempotent_id('7a01873e-3c38-4f30-80be-31a043cfe2fd')
- @test.services('network')
+ @decorators.idempotent_id('7a01873e-3c38-4f30-80be-31a043cfe2fd')
def test_security_group_rules_create_with_optional_cidr(self):
# Positive test: Creation of Security Group rule
# with optional argument cidr
@@ -101,8 +92,7 @@
self.expected['ip_range'] = {'cidr': cidr}
self._check_expected_response(rule)
- @test.idempotent_id('7f5d2899-7705-4d4b-8458-4505188ffab6')
- @test.services('network')
+ @decorators.idempotent_id('7f5d2899-7705-4d4b-8458-4505188ffab6')
def test_security_group_rules_create_with_optional_group_id(self):
# Positive test: Creation of Security Group rule
# with optional argument group_id
@@ -129,9 +119,8 @@
'name': group_name}
self._check_expected_response(rule)
- @test.attr(type='smoke')
- @test.idempotent_id('a6154130-5a55-4850-8be4-5e9e796dbf17')
- @test.services('network')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('a6154130-5a55-4850-8be4-5e9e796dbf17')
def test_security_group_rules_list(self):
# Positive test: Created Security Group rules should be
# in the list of all rules
@@ -158,16 +147,17 @@
to_port=to_port2)['security_group_rule']
rule2_id = rule['id']
# Delete the Security Group rule2 at the end of this method
- self.addCleanup(self.client.delete_security_group_rule, rule2_id)
+ self.addCleanup(
+ self.security_group_rules_client.delete_security_group_rule,
+ rule2_id)
# Get rules of the created Security Group
rules = self.security_groups_client.show_security_group(
securitygroup_id)['security_group']['rules']
- self.assertTrue(any([i for i in rules if i['id'] == rule1_id]))
- self.assertTrue(any([i for i in rules if i['id'] == rule2_id]))
+ self.assertNotEmpty([i for i in rules if i['id'] == rule1_id])
+ self.assertNotEmpty([i for i in rules if i['id'] == rule2_id])
- @test.idempotent_id('fc5c5acf-2091-43a6-a6ae-e42760e9ffaf')
- @test.services('network')
+ @decorators.idempotent_id('fc5c5acf-2091-43a6-a6ae-e42760e9ffaf')
def test_security_group_rules_delete_when_peer_group_deleted(self):
# Positive test:rule will delete when peer group deleting
# Creating a Security Group to add rules to it
@@ -182,7 +172,7 @@
ip_protocol=self.ip_protocol,
from_port=self.from_port,
to_port=self.to_port,
- group_id=sg2_id)['security_group_rule']
+ group_id=sg2_id)
# Delete group2
self.security_groups_client.delete_security_group(sg2_id)
@@ -190,4 +180,4 @@
rules = (self.security_groups_client.show_security_group(sg1_id)
['security_group']['rules'])
# The group1 has no rules because group2 has deleted
- self.assertEqual(0, len(rules))
+ self.assertEmpty(rules)
diff --git a/tempest/api/compute/security_groups/test_security_group_rules_negative.py b/tempest/api/compute/security_groups/test_security_group_rules_negative.py
index 816038a..8283aae 100644
--- a/tempest/api/compute/security_groups/test_security_group_rules_negative.py
+++ b/tempest/api/compute/security_groups/test_security_group_rules_negative.py
@@ -13,21 +13,10 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.compute.security_groups import base
-from tempest.common.utils import data_utils
-from tempest import config
-from tempest import test
-
-CONF = config.CONF
-
-
-def not_existing_id():
- if CONF.service_available.neutron:
- return data_utils.rand_uuid()
- else:
- return data_utils.rand_int_id(start=999)
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class SecurityGroupRulesNegativeTestJSON(base.BaseSecurityGroupsTest):
@@ -35,17 +24,15 @@
@classmethod
def setup_clients(cls):
super(SecurityGroupRulesNegativeTestJSON, cls).setup_clients()
- cls.client = cls.security_groups_client
cls.rules_client = cls.security_group_rules_client
- @test.attr(type=['negative'])
- @test.idempotent_id('1d507e98-7951-469b-82c3-23f1e6b8c254')
- @test.services('network')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('1d507e98-7951-469b-82c3-23f1e6b8c254')
def test_create_security_group_rule_with_non_existent_id(self):
# Negative test: Creation of Security Group rule should FAIL
# with non existent Parent group id
# Adding rules to the non existent Security Group id
- parent_group_id = not_existing_id()
+ parent_group_id = self.generate_random_security_group_id()
ip_protocol = 'tcp'
from_port = 22
to_port = 22
@@ -55,9 +42,8 @@
ip_protocol=ip_protocol, from_port=from_port,
to_port=to_port)
- @test.attr(type=['negative'])
- @test.idempotent_id('2244d7e4-adb7-4ecb-9930-2d77e123ce4f')
- @test.services('network')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('2244d7e4-adb7-4ecb-9930-2d77e123ce4f')
def test_create_security_group_rule_with_invalid_id(self):
# Negative test: Creation of Security Group rule should FAIL
# with Parent group id which is not integer
@@ -72,9 +58,8 @@
ip_protocol=ip_protocol, from_port=from_port,
to_port=to_port)
- @test.attr(type=['negative'])
- @test.idempotent_id('8bd56d02-3ffa-4d67-9933-b6b9a01d6089')
- @test.services('network')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('8bd56d02-3ffa-4d67-9933-b6b9a01d6089')
def test_create_security_group_rule_duplicate(self):
# Negative test: Create Security Group rule duplicate should fail
# Creating a Security Group to add rule to it
@@ -97,9 +82,8 @@
ip_protocol=ip_protocol, from_port=from_port,
to_port=to_port)
- @test.attr(type=['negative'])
- @test.idempotent_id('84c81249-9f6e-439c-9bbf-cbb0d2cddbdf')
- @test.services('network')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('84c81249-9f6e-439c-9bbf-cbb0d2cddbdf')
def test_create_security_group_rule_with_invalid_ip_protocol(self):
# Negative test: Creation of Security Group rule should FAIL
# with invalid ip_protocol
@@ -117,9 +101,8 @@
ip_protocol=ip_protocol, from_port=from_port,
to_port=to_port)
- @test.attr(type=['negative'])
- @test.idempotent_id('12bbc875-1045-4f7a-be46-751277baedb9')
- @test.services('network')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('12bbc875-1045-4f7a-be46-751277baedb9')
def test_create_security_group_rule_with_invalid_from_port(self):
# Negative test: Creation of Security Group rule should FAIL
# with invalid from_port
@@ -136,9 +119,8 @@
ip_protocol=ip_protocol, from_port=from_port,
to_port=to_port)
- @test.attr(type=['negative'])
- @test.idempotent_id('ff88804d-144f-45d1-bf59-dd155838a43a')
- @test.services('network')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('ff88804d-144f-45d1-bf59-dd155838a43a')
def test_create_security_group_rule_with_invalid_to_port(self):
# Negative test: Creation of Security Group rule should FAIL
# with invalid to_port
@@ -155,9 +137,8 @@
ip_protocol=ip_protocol, from_port=from_port,
to_port=to_port)
- @test.attr(type=['negative'])
- @test.idempotent_id('00296fa9-0576-496a-ae15-fbab843189e0')
- @test.services('network')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('00296fa9-0576-496a-ae15-fbab843189e0')
def test_create_security_group_rule_with_invalid_port_range(self):
# Negative test: Creation of Security Group rule should FAIL
# with invalid port range.
@@ -174,13 +155,12 @@
ip_protocol=ip_protocol, from_port=from_port,
to_port=to_port)
- @test.attr(type=['negative'])
- @test.idempotent_id('56fddcca-dbb8-4494-a0db-96e9f869527c')
- @test.services('network')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('56fddcca-dbb8-4494-a0db-96e9f869527c')
def test_delete_security_group_rule_with_non_existent_id(self):
# Negative test: Deletion of Security Group rule should be FAIL
# with non existent id
- non_existent_rule_id = not_existing_id()
+ non_existent_rule_id = self.generate_random_security_group_id()
self.assertRaises(lib_exc.NotFound,
self.rules_client.delete_security_group_rule,
non_existent_rule_id)
diff --git a/tempest/api/compute/security_groups/test_security_groups.py b/tempest/api/compute/security_groups/test_security_groups.py
index 81a02be..62d5bea 100644
--- a/tempest/api/compute/security_groups/test_security_groups.py
+++ b/tempest/api/compute/security_groups/test_security_groups.py
@@ -13,12 +13,11 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.compute.security_groups import base
-from tempest.common.utils import data_utils
from tempest.common import waiters
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class SecurityGroupsTestJSON(base.BaseSecurityGroupsTest):
@@ -28,14 +27,13 @@
super(SecurityGroupsTestJSON, cls).setup_clients()
cls.client = cls.security_groups_client
- @test.attr(type='smoke')
- @test.idempotent_id('eb2b087d-633d-4d0d-a7bd-9e6ba35b32de')
- @test.services('network')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('eb2b087d-633d-4d0d-a7bd-9e6ba35b32de')
def test_security_groups_create_list_delete(self):
# Positive test:Should return the list of Security Groups
# Create 3 Security Groups
security_group_list = []
- for i in range(3):
+ for _ in range(3):
body = self.create_security_group()
security_group_list.append(body)
# Fetch all Security Groups and verify the list
@@ -54,22 +52,19 @@
self.client.wait_for_resource_deletion(sg['id'])
# Now check if all the created Security Groups are deleted
fetched_list = self.client.list_security_groups()['security_groups']
- deleted_sgs = \
- [sg for sg in security_group_list if sg in fetched_list]
+ deleted_sgs = [sg for sg in security_group_list if sg in fetched_list]
self.assertFalse(deleted_sgs,
"Failed to delete Security Group %s "
"list" % ', '.join(m_group['name']
for m_group in deleted_sgs))
- @test.idempotent_id('ecc0da4a-2117-48af-91af-993cca39a615')
- @test.services('network')
+ @decorators.idempotent_id('ecc0da4a-2117-48af-91af-993cca39a615')
def test_security_group_create_get_delete(self):
# Security Group should be created, fetched and deleted
# with char space between name along with
# leading and trailing spaces
s_name = ' %s ' % data_utils.rand_name('securitygroup ')
securitygroup = self.create_security_group(name=s_name)
- self.assertIn('name', securitygroup)
securitygroup_name = securitygroup['name']
self.assertEqual(securitygroup_name, s_name,
"The created Security Group name is "
@@ -83,8 +78,7 @@
self.client.delete_security_group(securitygroup['id'])
self.client.wait_for_resource_deletion(securitygroup['id'])
- @test.idempotent_id('fe4abc0d-83f5-4c50-ad11-57a1127297a2')
- @test.services('network')
+ @decorators.idempotent_id('fe4abc0d-83f5-4c50-ad11-57a1127297a2')
def test_server_security_groups(self):
# Checks that security groups may be added and linked to a server
# and not deleted if the server is active.
@@ -95,11 +89,8 @@
# Create server and add the security group created
# above to the server we just created
- server_name = data_utils.rand_name('server')
- server = self.create_test_server(name=server_name)
+ server = self.create_test_server(wait_until='ACTIVE')
server_id = server['id']
- waiters.wait_for_server_status(self.servers_client, server_id,
- 'ACTIVE')
self.servers_client.add_security_group(server_id, name=sg['name'])
# Check that we are not able to delete the security
@@ -128,13 +119,11 @@
self.client.delete_security_group(sg['id'])
self.client.delete_security_group(sg2['id'])
- @test.idempotent_id('7d4e1d3c-3209-4d6d-b020-986304ebad1f')
- @test.services('network')
+ @decorators.idempotent_id('7d4e1d3c-3209-4d6d-b020-986304ebad1f')
def test_update_security_groups(self):
# Update security group name and description
# Create a security group
securitygroup = self.create_security_group()
- self.assertIn('id', securitygroup)
securitygroup_id = securitygroup['id']
# Update the name and description
s_new_name = data_utils.rand_name('sg-hth')
@@ -147,3 +136,30 @@
['security_group'])
self.assertEqual(s_new_name, fetched_group['name'])
self.assertEqual(s_new_des, fetched_group['description'])
+
+ @decorators.idempotent_id('79517d60-535a-438f-af3d-e6feab1cbea7')
+ def test_list_security_groups_by_server(self):
+ # Create a couple security groups that we will use
+ # for the server resource this test creates
+ sg = self.create_security_group()
+ sg2 = self.create_security_group()
+ assigned_security_groups_ids = [sg['id'], sg2['id']]
+ # Create server and add the security group created
+ # above to the server we just created
+ server_id = self.create_test_server(wait_until='ACTIVE')['id']
+ # add security groups to server
+ self.servers_client.add_security_group(server_id, name=sg['name'])
+ self.servers_client.add_security_group(server_id, name=sg2['name'])
+
+ # list security groups for a server
+ fetched_groups = (
+ self.servers_client.list_security_groups_by_server(
+ server_id)['security_groups'])
+ fetched_security_groups_ids = [i['id'] for i in fetched_groups]
+ # verifying the security groups ids in list
+ missing_security_groups =\
+ [p for p in assigned_security_groups_ids
+ if p not in fetched_security_groups_ids]
+ self.assertEmpty(missing_security_groups,
+ "Failed to find security_groups %s in fetched list" %
+ ', '.join(missing_security_groups))
diff --git a/tempest/api/compute/security_groups/test_security_groups_negative.py b/tempest/api/compute/security_groups/test_security_groups_negative.py
index 120d327..9c44bb2 100644
--- a/tempest/api/compute/security_groups/test_security_groups_negative.py
+++ b/tempest/api/compute/security_groups/test_security_groups_negative.py
@@ -13,14 +13,13 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import decorators
-from tempest_lib import exceptions as lib_exc
import testtools
from tempest.api.compute.security_groups import base
-from tempest.common.utils import data_utils
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
CONF = config.CONF
@@ -32,40 +31,19 @@
super(SecurityGroupsNegativeTestJSON, cls).setup_clients()
cls.client = cls.security_groups_client
- @classmethod
- def resource_setup(cls):
- super(SecurityGroupsNegativeTestJSON, cls).resource_setup()
- cls.neutron_available = CONF.service_available.neutron
-
- def _generate_a_non_existent_security_group_id(self):
- security_group_id = []
- body = self.client.list_security_groups()['security_groups']
- for i in range(len(body)):
- security_group_id.append(body[i]['id'])
- # Generate a non-existent security group id
- while True:
- non_exist_id = data_utils.rand_int_id(start=999)
- if self.neutron_available:
- non_exist_id = data_utils.rand_uuid()
- if non_exist_id not in security_group_id:
- break
- return non_exist_id
-
- @test.attr(type=['negative'])
- @test.idempotent_id('673eaec1-9b3e-48ed-bdf1-2786c1b9661c')
- @test.services('network')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('673eaec1-9b3e-48ed-bdf1-2786c1b9661c')
def test_security_group_get_nonexistent_group(self):
# Negative test:Should not be able to GET the details
# of non-existent Security Group
- non_exist_id = self._generate_a_non_existent_security_group_id()
+ non_exist_id = self.generate_random_security_group_id()
self.assertRaises(lib_exc.NotFound, self.client.show_security_group,
non_exist_id)
@decorators.skip_because(bug="1161411",
condition=CONF.service_available.neutron)
- @test.attr(type=['negative'])
- @test.idempotent_id('1759c3cb-b0fc-44b7-86ce-c99236be911d')
- @test.services('network')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('1759c3cb-b0fc-44b7-86ce-c99236be911d')
def test_security_group_create_with_invalid_group_name(self):
# Negative test: Security Group should not be created with group name
# as an empty string/with white spaces/chars more than 255
@@ -86,9 +64,8 @@
@decorators.skip_because(bug="1161411",
condition=CONF.service_available.neutron)
- @test.attr(type=['negative'])
- @test.idempotent_id('777b6f14-aca9-4758-9e84-38783cfa58bc')
- @test.services('network')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('777b6f14-aca9-4758-9e84-38783cfa58bc')
def test_security_group_create_with_invalid_group_description(self):
# Negative test: Security Group should not be created with description
# longer than 255 chars. Empty description is allowed by the API
@@ -100,11 +77,10 @@
self.client.create_security_group,
name=s_name, description=s_description)
- @test.idempotent_id('9fdb4abc-6b66-4b27-b89c-eb215a956168')
+ @decorators.idempotent_id('9fdb4abc-6b66-4b27-b89c-eb215a956168')
@testtools.skipIf(CONF.service_available.neutron,
"Neutron allows duplicate names for security groups")
- @test.attr(type=['negative'])
- @test.services('network')
+ @decorators.attr(type=['negative'])
def test_security_group_create_with_duplicate_name(self):
# Negative test:Security Group with duplicate name should not
# be created
@@ -116,9 +92,8 @@
self.client.create_security_group,
name=s_name, description=s_description)
- @test.attr(type=['negative'])
- @test.idempotent_id('36a1629f-c6da-4a26-b8b8-55e7e5d5cd58')
- @test.services('network')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('36a1629f-c6da-4a26-b8b8-55e7e5d5cd58')
def test_delete_the_default_security_group(self):
# Negative test:Deletion of the "default" Security Group should Fail
default_security_group_id = None
@@ -132,29 +107,26 @@
self.client.delete_security_group,
default_security_group_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('6727c00b-214c-4f9e-9a52-017ac3e98411')
- @test.services('network')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('6727c00b-214c-4f9e-9a52-017ac3e98411')
def test_delete_nonexistent_security_group(self):
# Negative test:Deletion of a non-existent Security Group should fail
- non_exist_id = self._generate_a_non_existent_security_group_id()
+ non_exist_id = self.generate_random_security_group_id()
self.assertRaises(lib_exc.NotFound,
self.client.delete_security_group, non_exist_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('1438f330-8fa4-4aeb-8a94-37c250106d7f')
- @test.services('network')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('1438f330-8fa4-4aeb-8a94-37c250106d7f')
def test_delete_security_group_without_passing_id(self):
# Negative test:Deletion of a Security Group with out passing ID
# should Fail
self.assertRaises(lib_exc.NotFound,
self.client.delete_security_group, '')
- @test.idempotent_id('00579617-fe04-4e1c-9d08-ca7467d2e34b')
+ @decorators.idempotent_id('00579617-fe04-4e1c-9d08-ca7467d2e34b')
@testtools.skipIf(CONF.service_available.neutron,
"Neutron does not check the security group ID")
- @test.attr(type=['negative'])
- @test.services('network')
+ @decorators.attr(type=['negative'])
def test_update_security_group_with_invalid_sg_id(self):
# Update security_group with invalid sg_id should fail
s_name = data_utils.rand_name('sg')
@@ -165,15 +137,13 @@
self.client.update_security_group, sg_id_invalid,
name=s_name, description=s_description)
- @test.idempotent_id('cda8d8b4-59f8-4087-821d-20cf5a03b3b1')
+ @decorators.idempotent_id('cda8d8b4-59f8-4087-821d-20cf5a03b3b1')
@testtools.skipIf(CONF.service_available.neutron,
"Neutron does not check the security group name")
- @test.attr(type=['negative'])
- @test.services('network')
+ @decorators.attr(type=['negative'])
def test_update_security_group_with_invalid_sg_name(self):
# Update security_group with invalid sg_name should fail
securitygroup = self.create_security_group()
- self.assertIn('id', securitygroup)
securitygroup_id = securitygroup['id']
# Update Security Group with group name longer than 255 chars
s_new_name = 'securitygroup-'.ljust(260, '0')
@@ -181,15 +151,13 @@
self.client.update_security_group,
securitygroup_id, name=s_new_name)
- @test.idempotent_id('97d12b1c-a610-4194-93f1-ba859e718b45')
+ @decorators.idempotent_id('97d12b1c-a610-4194-93f1-ba859e718b45')
@testtools.skipIf(CONF.service_available.neutron,
"Neutron does not check the security group description")
- @test.attr(type=['negative'])
- @test.services('network')
+ @decorators.attr(type=['negative'])
def test_update_security_group_with_invalid_sg_des(self):
# Update security_group with invalid sg_des should fail
securitygroup = self.create_security_group()
- self.assertIn('id', securitygroup)
securitygroup_id = securitygroup['id']
# Update Security Group with group description longer than 255 chars
s_new_des = 'des-'.ljust(260, '0')
@@ -197,12 +165,11 @@
self.client.update_security_group,
securitygroup_id, description=s_new_des)
- @test.attr(type=['negative'])
- @test.idempotent_id('27edee9c-873d-4da6-a68a-3c256efebe8f')
- @test.services('network')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('27edee9c-873d-4da6-a68a-3c256efebe8f')
def test_update_non_existent_security_group(self):
# Update a non-existent Security Group should Fail
- non_exist_id = self._generate_a_non_existent_security_group_id()
+ non_exist_id = self.generate_random_security_group_id()
s_name = data_utils.rand_name('sg')
s_description = data_utils.rand_name('description')
self.assertRaises(lib_exc.NotFound,
diff --git a/tempest/api/compute/servers/test_attach_interfaces.py b/tempest/api/compute/servers/test_attach_interfaces.py
index 24d503f..7509ac6 100644
--- a/tempest/api/compute/servers/test_attach_interfaces.py
+++ b/tempest/api/compute/servers/test_attach_interfaces.py
@@ -15,12 +15,16 @@
import time
-from tempest_lib import exceptions as lib_exc
+import six
from tempest.api.compute import base
+from tempest.common import compute
+from tempest.common import utils
+from tempest.common.utils import net_utils
+from tempest.common import waiters
from tempest import config
-from tempest import exceptions
-from tempest import test
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
CONF = config.CONF
@@ -44,35 +48,41 @@
@classmethod
def setup_clients(cls):
super(AttachInterfacesTestJSON, cls).setup_clients()
- cls.client = cls.os.interfaces_client
+ cls.subnets_client = cls.os_primary.subnets_client
+ cls.ports_client = cls.os_primary.ports_client
- def wait_for_interface_status(self, server, port_id, status):
- """Waits for a interface to reach a given status."""
- body = (self.client.show_interface(server, port_id)
- ['interfaceAttachment'])
- interface_status = body['port_state']
+ def wait_for_port_detach(self, port_id):
+ """Waits for the port's device_id to be unset.
+
+ :param port_id: The id of the port being detached.
+ :returns: The final port dict from the show_port response.
+ """
+ port = self.ports_client.show_port(port_id)['port']
+ device_id = port['device_id']
start = int(time.time())
- while(interface_status != status):
+ # NOTE(mriedem): Nova updates the port's device_id to '' rather than
+ # None, but it's not contractual so handle Falsey either way.
+ while device_id:
time.sleep(self.build_interval)
- body = (self.client.show_interface(server, port_id)
- ['interfaceAttachment'])
- interface_status = body['port_state']
+ port = self.ports_client.show_port(port_id)['port']
+ device_id = port['device_id']
timed_out = int(time.time()) - start >= self.build_timeout
- if interface_status != status and timed_out:
- message = ('Interface %s failed to reach %s status '
- '(current %s) within the required time (%s s).' %
- (port_id, status, interface_status,
- self.build_timeout))
- raise exceptions.TimeoutException(message)
+ if device_id and timed_out:
+ message = ('Port %s failed to detach (device_id %s) within '
+ 'the required time (%s s).' %
+ (port_id, device_id, self.build_timeout))
+ raise lib_exc.TimeoutException(message)
- return body
+ return port
- def _check_interface(self, iface, port_id=None, network_id=None,
- fixed_ip=None, mac_addr=None):
- self.assertIn('port_state', iface)
+ def _check_interface(self, iface, server_id=None, port_id=None,
+ network_id=None, fixed_ip=None, mac_addr=None):
+ if server_id:
+ iface = waiters.wait_for_interface_status(
+ self.interfaces_client, server_id, iface['port_id'], 'ACTIVE')
if port_id:
self.assertEqual(iface['port_id'], port_id)
if network_id:
@@ -84,33 +94,60 @@
def _create_server_get_interfaces(self):
server = self.create_test_server(wait_until='ACTIVE')
- ifs = (self.client.list_interfaces(server['id'])
+ ifs = (self.interfaces_client.list_interfaces(server['id'])
['interfaceAttachments'])
- body = self.wait_for_interface_status(
- server['id'], ifs[0]['port_id'], 'ACTIVE')
+ body = waiters.wait_for_interface_status(
+ self.interfaces_client, server['id'], ifs[0]['port_id'], 'ACTIVE')
ifs[0]['port_state'] = body['port_state']
return server, ifs
def _test_create_interface(self, server):
- iface = (self.client.create_interface(server['id'])
+ iface = (self.interfaces_client.create_interface(server['id'])
['interfaceAttachment'])
- iface = self.wait_for_interface_status(
- server['id'], iface['port_id'], 'ACTIVE')
- self._check_interface(iface)
+ iface = waiters.wait_for_interface_status(
+ self.interfaces_client, server['id'], iface['port_id'], 'ACTIVE')
return iface
def _test_create_interface_by_network_id(self, server, ifs):
network_id = ifs[0]['net_id']
- iface = self.client.create_interface(
+ iface = self.interfaces_client.create_interface(
server['id'], net_id=network_id)['interfaceAttachment']
- iface = self.wait_for_interface_status(
- server['id'], iface['port_id'], 'ACTIVE')
- self._check_interface(iface, network_id=network_id)
+ self._check_interface(iface, server_id=server['id'],
+ network_id=network_id)
+ return iface
+
+ def _test_create_interface_by_port_id(self, server, ifs):
+ network_id = ifs[0]['net_id']
+ port = self.ports_client.create_port(network_id=network_id)
+ port_id = port['port']['id']
+ self.addCleanup(self.ports_client.delete_port, port_id)
+ iface = self.interfaces_client.create_interface(
+ server['id'], port_id=port_id)['interfaceAttachment']
+ self._check_interface(iface, server_id=server['id'], port_id=port_id,
+ network_id=network_id)
+ return iface
+
+ def _test_create_interface_by_fixed_ips(self, server, ifs):
+ network_id = ifs[0]['net_id']
+ subnet_id = ifs[0]['fixed_ips'][0]['subnet_id']
+ ip_list = net_utils.get_unused_ip_addresses(self.ports_client,
+ self.subnets_client,
+ network_id,
+ subnet_id,
+ 1)
+
+ fixed_ips = [{'ip_address': ip_list[0]}]
+ iface = self.interfaces_client.create_interface(
+ server['id'], net_id=network_id,
+ fixed_ips=fixed_ips)['interfaceAttachment']
+ self.addCleanup(self.ports_client.delete_port, iface['port_id'])
+ self._check_interface(iface, server_id=server['id'],
+ fixed_ip=ip_list[0])
return iface
def _test_show_interface(self, server, ifs):
iface = ifs[0]
- _iface = self.client.show_interface(
+ _iface = self.interfaces_client.show_interface(
server['id'], iface['port_id'])['interfaceAttachment']
self._check_interface(iface, port_id=_iface['port_id'],
network_id=_iface['net_id'],
@@ -120,20 +157,20 @@
def _test_delete_interface(self, server, ifs):
# NOTE(danms): delete not the first or last, but one in the middle
iface = ifs[1]
- self.client.delete_interface(server['id'], iface['port_id'])
- _ifs = (self.client.list_interfaces(server['id'])
+ self.interfaces_client.delete_interface(server['id'], iface['port_id'])
+ _ifs = (self.interfaces_client.list_interfaces(server['id'])
['interfaceAttachments'])
start = int(time.time())
while len(ifs) == len(_ifs):
time.sleep(self.build_interval)
- _ifs = (self.client.list_interfaces(server['id'])
+ _ifs = (self.interfaces_client.list_interfaces(server['id'])
['interfaceAttachments'])
timed_out = int(time.time()) - start >= self.build_timeout
if len(ifs) == len(_ifs) and timed_out:
message = ('Failed to delete interface within '
'the required time: %s sec.' % self.build_timeout)
- raise exceptions.TimeoutException(message)
+ raise lib_exc.TimeoutException(message)
self.assertNotIn(iface['port_id'], [i['port_id'] for i in _ifs])
return _ifs
@@ -146,20 +183,19 @@
self.assertEqual(sorted(list1), sorted(list2))
- @test.idempotent_id('73fe8f02-590d-4bf1-b184-e9ca81065051')
- @test.services('network')
- def test_create_list_show_delete_interfaces(self):
+ @decorators.idempotent_id('73fe8f02-590d-4bf1-b184-e9ca81065051')
+ @utils.services('network')
+ def test_create_list_show_delete_interfaces_by_network_port(self):
server, ifs = self._create_server_get_interfaces()
interface_count = len(ifs)
- self.assertTrue(interface_count > 0)
- self._check_interface(ifs[0])
+ self.assertGreater(interface_count, 0)
try:
iface = self._test_create_interface(server)
except lib_exc.BadRequest as e:
msg = ('Multiple possible networks found, use a Network ID to be '
'more specific.')
- if not CONF.compute.fixed_network_name and e.message == msg:
+ if not CONF.compute.fixed_network_name and six.text_type(e) == msg:
raise
else:
ifs.append(iface)
@@ -167,7 +203,10 @@
iface = self._test_create_interface_by_network_id(server, ifs)
ifs.append(iface)
- _ifs = (self.client.list_interfaces(server['id'])
+ iface = self._test_create_interface_by_port_id(server, ifs)
+ ifs.append(iface)
+
+ _ifs = (self.interfaces_client.list_interfaces(server['id'])
['interfaceAttachments'])
self._compare_iface_list(ifs, _ifs)
@@ -176,19 +215,57 @@
_ifs = self._test_delete_interface(server, ifs)
self.assertEqual(len(ifs) - 1, len(_ifs))
- @test.attr(type='smoke')
- @test.idempotent_id('c7e0e60b-ee45-43d0-abeb-8596fd42a2f9')
- @test.services('network')
+ @decorators.idempotent_id('d290c06c-f5b3-11e7-8ec8-002293781009')
+ @utils.services('network')
+ def test_create_list_show_delete_interfaces_by_fixed_ip(self):
+ # NOTE(zhufl) By default only project that is admin or network owner
+ # or project with role advsvc is authorised to create interfaces with
+ # fixed-ip, so if we don't create network for each project, do not
+ # test _test_create_interface_by_fixed_ips.
+ if not (CONF.auth.use_dynamic_credentials and
+ CONF.auth.create_isolated_networks and
+ not CONF.network.shared_physical_network):
+ raise self.skipException("Only owner network supports "
+ "creating interface by fixed ip.")
+
+ server, ifs = self._create_server_get_interfaces()
+ interface_count = len(ifs)
+ self.assertGreater(interface_count, 0)
+
+ try:
+ iface = self._test_create_interface(server)
+ except lib_exc.BadRequest as e:
+ msg = ('Multiple possible networks found, use a Network ID to be '
+ 'more specific.')
+ if not CONF.compute.fixed_network_name and six.text_type(e) == msg:
+ raise
+ else:
+ ifs.append(iface)
+
+ iface = self._test_create_interface_by_fixed_ips(server, ifs)
+ ifs.append(iface)
+
+ _ifs = (self.interfaces_client.list_interfaces(server['id'])
+ ['interfaceAttachments'])
+ self._compare_iface_list(ifs, _ifs)
+
+ self._test_show_interface(server, ifs)
+
+ _ifs = self._test_delete_interface(server, ifs)
+ self.assertEqual(len(ifs) - 1, len(_ifs))
+
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('c7e0e60b-ee45-43d0-abeb-8596fd42a2f9')
+ @utils.services('network')
def test_add_remove_fixed_ip(self):
# Add and Remove the fixed IP to server.
server, ifs = self._create_server_get_interfaces()
interface_count = len(ifs)
- self.assertTrue(interface_count > 0)
- self._check_interface(ifs[0])
+ self.assertGreater(interface_count, 0)
network_id = ifs[0]['net_id']
self.servers_client.add_fixed_ip(server['id'], networkId=network_id)
# Remove the fixed IP from server.
- server_detail = self.os.servers_client.show_server(
+ server_detail = self.os_primary.servers_client.show_server(
server['id'])['server']
# Get the Fixed IP from server.
fixed_ip = None
@@ -200,3 +277,40 @@
if fixed_ip is not None:
break
self.servers_client.remove_fixed_ip(server['id'], address=fixed_ip)
+
+ @decorators.idempotent_id('2f3a0127-95c7-4977-92d2-bc5aec602fb4')
+ def test_reassign_port_between_servers(self):
+ """Tests the following:
+
+ 1. Create a port in Neutron.
+ 2. Create two servers in Nova.
+ 3. Attach the port to the first server.
+ 4. Detach the port from the first server.
+ 5. Attach the port to the second server.
+ 6. Detach the port from the second server.
+ """
+ network = self.get_tenant_network()
+ network_id = network['id']
+ port = self.ports_client.create_port(network_id=network_id)
+ port_id = port['port']['id']
+ self.addCleanup(self.ports_client.delete_port, port_id)
+
+ # create two servers
+ _, servers = compute.create_test_server(
+ self.os_primary, tenant_network=network,
+ wait_until='ACTIVE', min_count=2)
+ # add our cleanups for the servers since we bypassed the base class
+ for server in servers:
+ self.addCleanup(self.delete_server, server['id'])
+
+ for server in servers:
+ # attach the port to the server
+ iface = self.interfaces_client.create_interface(
+ server['id'], port_id=port_id)['interfaceAttachment']
+ self._check_interface(iface, server_id=server['id'],
+ port_id=port_id)
+
+ # detach the port from the server; this is a cast in the compute
+ # API so we have to poll the port until the device_id is unset.
+ self.interfaces_client.delete_interface(server['id'], port_id)
+ self.wait_for_port_detach(port_id)
diff --git a/tempest/api/compute/servers/test_availability_zone.py b/tempest/api/compute/servers/test_availability_zone.py
index 76da317..36828d6 100644
--- a/tempest/api/compute/servers/test_availability_zone.py
+++ b/tempest/api/compute/servers/test_availability_zone.py
@@ -14,7 +14,7 @@
# under the License.
from tempest.api.compute import base
-from tempest import test
+from tempest.lib import decorators
class AZV2TestJSON(base.BaseV2ComputeTest):
@@ -25,8 +25,8 @@
super(AZV2TestJSON, cls).setup_clients()
cls.client = cls.availability_zone_client
- @test.idempotent_id('a8333aa2-205c-449f-a828-d38c2489bf25')
+ @decorators.idempotent_id('a8333aa2-205c-449f-a828-d38c2489bf25')
def test_get_availability_zone_list_with_non_admin_user(self):
# List of availability zone with non-administrator user
availability_zone = self.client.list_availability_zones()
- self.assertTrue(len(availability_zone['availabilityZoneInfo']) > 0)
+ self.assertNotEmpty(availability_zone['availabilityZoneInfo'])
diff --git a/tempest/api/compute/servers/test_create_server.py b/tempest/api/compute/servers/test_create_server.py
index f51c2db..122c4f5 100644
--- a/tempest/api/compute/servers/test_create_server.py
+++ b/tempest/api/compute/servers/test_create_server.py
@@ -17,17 +17,18 @@
import testtools
from tempest.api.compute import base
-from tempest.common.utils import data_utils
+from tempest.common import utils
from tempest.common.utils.linux import remote_client
-from tempest.common import waiters
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
CONF = config.CONF
class ServersTestJSON(base.BaseV2ComputeTest):
disk_config = 'AUTO'
+ volume_backed = False
@classmethod
def setup_credentials(cls):
@@ -38,47 +39,34 @@
def setup_clients(cls):
super(ServersTestJSON, cls).setup_clients()
cls.client = cls.servers_client
- cls.network_client = cls.os.network_client
- cls.networks_client = cls.os.networks_client
- cls.subnets_client = cls.os.subnets_client
@classmethod
def resource_setup(cls):
- cls.set_validation_resources()
super(ServersTestJSON, cls).resource_setup()
+ validation_resources = cls.get_class_validation_resources(
+ cls.os_primary)
cls.meta = {'hello': 'world'}
cls.accessIPv4 = '1.1.1.1'
cls.accessIPv6 = '0000:0000:0000:0000:0000:babe:220.12.22.2'
- cls.name = data_utils.rand_name('server')
+ cls.name = data_utils.rand_name(cls.__name__ + '-server')
+ cls.password = data_utils.rand_password()
disk_config = cls.disk_config
- cls.server_initial = cls.create_test_server(
+ server_initial = cls.create_test_server(
validatable=True,
+ validation_resources=validation_resources,
wait_until='ACTIVE',
name=cls.name,
metadata=cls.meta,
accessIPv4=cls.accessIPv4,
accessIPv6=cls.accessIPv6,
- disk_config=disk_config)
- cls.password = cls.server_initial['adminPass']
- cls.server = (cls.client.show_server(cls.server_initial['id'])
+ disk_config=disk_config,
+ adminPass=cls.password,
+ volume_backed=cls.volume_backed)
+ cls.server = (cls.client.show_server(server_initial['id'])
['server'])
- def _create_net_subnet_ret_net_from_cidr(self, cidr):
- name_net = data_utils.rand_name(self.__class__.__name__)
- net = self.networks_client.create_network(name=name_net)
- self.addCleanup(self.networks_client.delete_network,
- net['network']['id'])
-
- subnet = self.subnets_client.create_subnet(
- network_id=net['network']['id'],
- cidr=cidr,
- ip_version=4)
- self.addCleanup(self.subnets_client.delete_subnet,
- subnet['subnet']['id'])
- return net
-
- @test.attr(type='smoke')
- @test.idempotent_id('5de47127-9977-400a-936f-abcfbec1218f')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('5de47127-9977-400a-936f-abcfbec1218f')
def test_verify_server_details(self):
# Verify the specified server attributes are set correctly
self.assertEqual(self.accessIPv4, self.server['accessIPv4'])
@@ -87,259 +75,73 @@
self.assertEqual(self.server['accessIPv6'],
str(netaddr.IPAddress(self.accessIPv6)))
self.assertEqual(self.name, self.server['name'])
- self.assertEqual(self.image_ref, self.server['image']['id'])
+ if self.volume_backed:
+ # Image is an empty string as per documentation
+ self.assertEqual("", self.server['image'])
+ else:
+ self.assertEqual(self.image_ref, self.server['image']['id'])
self.assertEqual(self.flavor_ref, self.server['flavor']['id'])
self.assertEqual(self.meta, self.server['metadata'])
- @test.attr(type='smoke')
- @test.idempotent_id('9a438d88-10c6-4bcd-8b5b-5b6e25e1346f')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('9a438d88-10c6-4bcd-8b5b-5b6e25e1346f')
def test_list_servers(self):
# The created server should be in the list of all servers
body = self.client.list_servers()
servers = body['servers']
- found = any([i for i in servers if i['id'] == self.server['id']])
- self.assertTrue(found)
+ found = [i for i in servers if i['id'] == self.server['id']]
+ self.assertNotEmpty(found)
- @test.idempotent_id('585e934c-448e-43c4-acbf-d06a9b899997')
+ @decorators.idempotent_id('585e934c-448e-43c4-acbf-d06a9b899997')
def test_list_servers_with_detail(self):
# The created server should be in the detailed list of all servers
body = self.client.list_servers(detail=True)
servers = body['servers']
- found = any([i for i in servers if i['id'] == self.server['id']])
- self.assertTrue(found)
+ found = [i for i in servers if i['id'] == self.server['id']]
+ self.assertNotEmpty(found)
- @test.idempotent_id('cbc0f52f-05aa-492b-bdc1-84b575ca294b')
+ @decorators.idempotent_id('cbc0f52f-05aa-492b-bdc1-84b575ca294b')
@testtools.skipUnless(CONF.validation.run_validation,
'Instance validation tests are disabled.')
def test_verify_created_server_vcpus(self):
# Verify that the number of vcpus reported by the instance matches
# the amount stated by the flavor
flavor = self.flavors_client.show_flavor(self.flavor_ref)['flavor']
+ validation_resources = self.get_class_validation_resources(
+ self.os_primary)
linux_client = remote_client.RemoteClient(
- self.get_server_ip(self.server),
+ self.get_server_ip(self.server, validation_resources),
self.ssh_user,
self.password,
- self.validation_resources['keypair']['private_key'])
- self.assertEqual(flavor['vcpus'], linux_client.get_number_of_vcpus())
+ validation_resources['keypair']['private_key'],
+ server=self.server,
+ servers_client=self.client)
+ output = linux_client.exec_command('grep -c ^processor /proc/cpuinfo')
+ self.assertEqual(flavor['vcpus'], int(output))
- @test.idempotent_id('ac1ad47f-984b-4441-9274-c9079b7a0666')
+ @decorators.idempotent_id('ac1ad47f-984b-4441-9274-c9079b7a0666')
@testtools.skipUnless(CONF.validation.run_validation,
'Instance validation tests are disabled.')
def test_host_name_is_same_as_server_name(self):
# Verify the instance host name is the same as the server name
+ validation_resources = self.get_class_validation_resources(
+ self.os_primary)
linux_client = remote_client.RemoteClient(
- self.get_server_ip(self.server),
+ self.get_server_ip(self.server, validation_resources),
self.ssh_user,
self.password,
- self.validation_resources['keypair']['private_key'])
- self.assertTrue(linux_client.hostname_equals_servername(self.name))
-
- @test.idempotent_id('ed20d3fb-9d1f-4329-b160-543fbd5d9811')
- def test_create_server_with_scheduler_hint_group(self):
- # Create a server with the scheduler hint "group".
- name = data_utils.rand_name('server_group')
- policies = ['affinity']
- body = self.server_groups_client.create_server_group(
- name=name, policies=policies)['server_group']
- group_id = body['id']
- self.addCleanup(self.server_groups_client.delete_server_group,
- group_id)
-
- hints = {'group': group_id}
- server = self.create_test_server(scheduler_hints=hints,
- wait_until='ACTIVE')
-
- # Check a server is in the group
- server_group = (self.server_groups_client.show_server_group(group_id)
- ['server_group'])
- self.assertIn(server['id'], server_group['members'])
-
- @test.idempotent_id('0578d144-ed74-43f8-8e57-ab10dbf9b3c2')
- @testtools.skipUnless(CONF.service_available.neutron,
- 'Neutron service must be available.')
- def test_verify_multiple_nics_order(self):
- # Verify that the networks order given at the server creation is
- # preserved within the server.
- net1 = self._create_net_subnet_ret_net_from_cidr('19.80.0.0/24')
- net2 = self._create_net_subnet_ret_net_from_cidr('19.86.0.0/24')
-
- networks = [{'uuid': net1['network']['id']},
- {'uuid': net2['network']['id']}]
-
- server_multi_nics = self.create_test_server(
- networks=networks, wait_until='ACTIVE')
-
- # Cleanup server; this is needed in the test case because with the LIFO
- # nature of the cleanups, if we don't delete the server first, the port
- # will still be part of the subnet and we'll get a 409 from Neutron
- # when trying to delete the subnet. The tear down in the base class
- # will try to delete the server and get a 404 but it's ignored so
- # we're OK.
- def cleanup_server():
- self.client.delete_server(server_multi_nics['id'])
- waiters.wait_for_server_termination(self.client,
- server_multi_nics['id'])
-
- self.addCleanup(cleanup_server)
-
- addresses = (self.client.list_addresses(server_multi_nics['id'])
- ['addresses'])
-
- # We can't predict the ip addresses assigned to the server on networks.
- # Sometimes the assigned addresses are ['19.80.0.2', '19.86.0.2'], at
- # other times ['19.80.0.3', '19.86.0.3']. So we check if the first
- # address is in first network, similarly second address is in second
- # network.
- addr = [addresses[net1['network']['name']][0]['addr'],
- addresses[net2['network']['name']][0]['addr']]
- networks = [netaddr.IPNetwork('19.80.0.0/24'),
- netaddr.IPNetwork('19.86.0.0/24')]
- for address, network in zip(addr, networks):
- self.assertIn(address, network)
-
- @test.idempotent_id('1678d144-ed74-43f8-8e57-ab10dbf9b3c2')
- @testtools.skipUnless(CONF.service_available.neutron,
- 'Neutron service must be available.')
- # The below skipUnless should be removed once Kilo-eol happens.
- @testtools.skipUnless(CONF.compute_feature_enabled.
- allow_duplicate_networks,
- 'Duplicate networks must be allowed')
- def test_verify_duplicate_network_nics(self):
- # Verify that server creation does not fail when more than one nic
- # is created on the same network.
- net1 = self._create_net_subnet_ret_net_from_cidr('19.80.0.0/24')
- net2 = self._create_net_subnet_ret_net_from_cidr('19.86.0.0/24')
-
- networks = [{'uuid': net1['network']['id']},
- {'uuid': net2['network']['id']},
- {'uuid': net1['network']['id']}]
-
- server_multi_nics = self.create_test_server(
- networks=networks, wait_until='ACTIVE')
-
- def cleanup_server():
- self.client.delete_server(server_multi_nics['id'])
- waiters.wait_for_server_termination(self.client,
- server_multi_nics['id'])
-
- self.addCleanup(cleanup_server)
-
- addresses = (self.client.list_addresses(server_multi_nics['id'])
- ['addresses'])
-
- addr = [addresses[net1['network']['name']][0]['addr'],
- addresses[net2['network']['name']][0]['addr'],
- addresses[net1['network']['name']][1]['addr']]
- networks = [netaddr.IPNetwork('19.80.0.0/24'),
- netaddr.IPNetwork('19.86.0.0/24'),
- netaddr.IPNetwork('19.80.0.0/24')]
- for address, network in zip(addr, networks):
- self.assertIn(address, network)
-
-
-class ServersWithSpecificFlavorTestJSON(base.BaseV2ComputeAdminTest):
- disk_config = 'AUTO'
-
- @classmethod
- def setup_credentials(cls):
- cls.prepare_instance_network()
- super(ServersWithSpecificFlavorTestJSON, cls).setup_credentials()
-
- @classmethod
- def setup_clients(cls):
- super(ServersWithSpecificFlavorTestJSON, cls).setup_clients()
- cls.flavor_client = cls.os_adm.flavors_client
- cls.client = cls.servers_client
-
- @classmethod
- def resource_setup(cls):
- cls.set_validation_resources()
-
- super(ServersWithSpecificFlavorTestJSON, cls).resource_setup()
-
- @test.idempotent_id('b3c7bcfc-bb5b-4e22-b517-c7f686b802ca')
- @testtools.skipUnless(CONF.validation.run_validation,
- 'Instance validation tests are disabled.')
- def test_verify_created_server_ephemeral_disk(self):
- # Verify that the ephemeral disk is created when creating server
-
- def create_flavor_with_extra_specs():
- flavor_with_eph_disk_name = data_utils.rand_name('eph_flavor')
- flavor_with_eph_disk_id = data_utils.rand_int_id(start=1000)
- ram = 64
- vcpus = 1
- disk = 0
-
- # Create a flavor with extra specs
- flavor = (self.flavor_client.
- create_flavor(name=flavor_with_eph_disk_name,
- ram=ram, vcpus=vcpus, disk=disk,
- id=flavor_with_eph_disk_id,
- ephemeral=1))['flavor']
- self.addCleanup(flavor_clean_up, flavor['id'])
-
- return flavor['id']
-
- def create_flavor_without_extra_specs():
- flavor_no_eph_disk_name = data_utils.rand_name('no_eph_flavor')
- flavor_no_eph_disk_id = data_utils.rand_int_id(start=1000)
-
- ram = 64
- vcpus = 1
- disk = 0
-
- # Create a flavor without extra specs
- flavor = (self.flavor_client.
- create_flavor(name=flavor_no_eph_disk_name,
- ram=ram, vcpus=vcpus, disk=disk,
- id=flavor_no_eph_disk_id))['flavor']
- self.addCleanup(flavor_clean_up, flavor['id'])
-
- return flavor['id']
-
- def flavor_clean_up(flavor_id):
- self.flavor_client.delete_flavor(flavor_id)
- self.flavor_client.wait_for_resource_deletion(flavor_id)
-
- flavor_with_eph_disk_id = create_flavor_with_extra_specs()
- flavor_no_eph_disk_id = create_flavor_without_extra_specs()
-
- admin_pass = self.image_ssh_password
-
- server_no_eph_disk = self.create_test_server(
- validatable=True,
- wait_until='ACTIVE',
- adminPass=admin_pass,
- flavor=flavor_no_eph_disk_id)
-
- # Get partition number of server without extra specs.
- server_no_eph_disk = self.client.show_server(
- server_no_eph_disk['id'])['server']
- linux_client = remote_client.RemoteClient(
- self.get_server_ip(server_no_eph_disk),
- self.ssh_user,
- admin_pass,
- self.validation_resources['keypair']['private_key'])
- partition_num = len(linux_client.get_partitions().split('\n'))
-
- # Explicit server deletion necessary for Juno compatibility
- self.client.delete_server(server_no_eph_disk['id'])
-
- server_with_eph_disk = self.create_test_server(
- validatable=True,
- wait_until='ACTIVE',
- adminPass=admin_pass,
- flavor=flavor_with_eph_disk_id)
-
- server_with_eph_disk = self.client.show_server(
- server_with_eph_disk['id'])['server']
- linux_client = remote_client.RemoteClient(
- self.get_server_ip(server_with_eph_disk),
- self.ssh_user,
- admin_pass,
- self.validation_resources['keypair']['private_key'])
- partition_num_emph = len(linux_client.get_partitions().split('\n'))
- self.assertEqual(partition_num + 1, partition_num_emph)
+ validation_resources['keypair']['private_key'],
+ server=self.server,
+ 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.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):
@@ -351,3 +153,15 @@
if not CONF.compute_feature_enabled.disk_config:
msg = "DiskConfig extension not enabled."
raise cls.skipException(msg)
+
+
+class ServersTestBootFromVolume(ServersTestJSON):
+ """Run the `ServersTestJSON` tests with a volume backed VM"""
+ volume_backed = True
+
+ @classmethod
+ def skip_checks(cls):
+ super(ServersTestBootFromVolume, cls).skip_checks()
+ if not utils.get_service_list()['volume']:
+ msg = "Volume service not enabled."
+ raise cls.skipException(msg)
diff --git a/tempest/api/compute/servers/test_create_server_multi_nic.py b/tempest/api/compute/servers/test_create_server_multi_nic.py
new file mode 100644
index 0000000..3447d85
--- /dev/null
+++ b/tempest/api/compute/servers/test_create_server_multi_nic.py
@@ -0,0 +1,120 @@
+# 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.
+
+import netaddr
+import testtools
+
+from tempest.api.compute import base
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+
+CONF = config.CONF
+
+
+class ServersTestMultiNic(base.BaseV2ComputeTest):
+
+ @classmethod
+ def setup_credentials(cls):
+ cls.prepare_instance_network()
+ super(ServersTestMultiNic, cls).setup_credentials()
+
+ @classmethod
+ def setup_clients(cls):
+ super(ServersTestMultiNic, cls).setup_clients()
+ cls.client = cls.servers_client
+ cls.networks_client = cls.os_primary.networks_client
+ cls.subnets_client = cls.os_primary.subnets_client
+
+ def _create_net_subnet_ret_net_from_cidr(self, cidr):
+ name_net = data_utils.rand_name(self.__class__.__name__)
+ net = self.networks_client.create_network(name=name_net)
+ self.addCleanup(self.networks_client.delete_network,
+ net['network']['id'])
+
+ subnet = self.subnets_client.create_subnet(
+ network_id=net['network']['id'],
+ cidr=cidr,
+ ip_version=4)
+ self.addCleanup(self.subnets_client.delete_subnet,
+ subnet['subnet']['id'])
+ return net
+
+ @decorators.idempotent_id('0578d144-ed74-43f8-8e57-ab10dbf9b3c2')
+ @testtools.skipUnless(CONF.service_available.neutron,
+ 'Neutron service must be available.')
+ def test_verify_multiple_nics_order(self):
+ # Verify that the networks order given at the server creation is
+ # preserved within the server.
+ net1 = self._create_net_subnet_ret_net_from_cidr('19.80.0.0/24')
+ net2 = self._create_net_subnet_ret_net_from_cidr('19.86.0.0/24')
+
+ networks = [{'uuid': net1['network']['id']},
+ {'uuid': net2['network']['id']}]
+
+ server_multi_nics = self.create_test_server(
+ networks=networks, wait_until='ACTIVE')
+
+ # Cleanup server; this is needed in the test case because with the LIFO
+ # nature of the cleanups, if we don't delete the server first, the port
+ # will still be part of the subnet and we'll get a 409 from Neutron
+ # when trying to delete the subnet. The tear down in the base class
+ # will try to delete the server and get a 404 but it's ignored so
+ # we're OK.
+ self.addCleanup(self.delete_server, server_multi_nics['id'])
+
+ addresses = (self.client.list_addresses(server_multi_nics['id'])
+ ['addresses'])
+
+ # We can't predict the ip addresses assigned to the server on networks.
+ # Sometimes the assigned addresses are ['19.80.0.2', '19.86.0.2'], at
+ # other times ['19.80.0.3', '19.86.0.3']. So we check if the first
+ # address is in first network, similarly second address is in second
+ # network.
+ addr = [addresses[net1['network']['name']][0]['addr'],
+ addresses[net2['network']['name']][0]['addr']]
+ networks = [netaddr.IPNetwork('19.80.0.0/24'),
+ netaddr.IPNetwork('19.86.0.0/24')]
+ for address, network in zip(addr, networks):
+ self.assertIn(address, network)
+
+ @decorators.idempotent_id('1678d144-ed74-43f8-8e57-ab10dbf9b3c2')
+ @testtools.skipUnless(CONF.service_available.neutron,
+ 'Neutron service must be available.')
+ def test_verify_duplicate_network_nics(self):
+ # Verify that server creation does not fail when more than one nic
+ # is created on the same network.
+ net1 = self._create_net_subnet_ret_net_from_cidr('19.80.0.0/24')
+ net2 = self._create_net_subnet_ret_net_from_cidr('19.86.0.0/24')
+
+ networks = [{'uuid': net1['network']['id']},
+ {'uuid': net2['network']['id']},
+ {'uuid': net1['network']['id']}]
+
+ server_multi_nics = self.create_test_server(
+ networks=networks, wait_until='ACTIVE')
+ self.addCleanup(self.delete_server, server_multi_nics['id'])
+
+ addresses = (self.client.list_addresses(server_multi_nics['id'])
+ ['addresses'])
+
+ addr = [addresses[net1['network']['name']][0]['addr'],
+ addresses[net2['network']['name']][0]['addr'],
+ addresses[net1['network']['name']][1]['addr']]
+ networks = [netaddr.IPNetwork('19.80.0.0/24'),
+ netaddr.IPNetwork('19.86.0.0/24'),
+ netaddr.IPNetwork('19.80.0.0/24')]
+ for address, network in zip(addr, networks):
+ self.assertIn(address, network)
diff --git a/tempest/api/compute/servers/test_delete_server.py b/tempest/api/compute/servers/test_delete_server.py
index 6796bb5..0093752 100644
--- a/tempest/api/compute/servers/test_delete_server.py
+++ b/tempest/api/compute/servers/test_delete_server.py
@@ -16,9 +16,11 @@
import testtools
from tempest.api.compute import base
+from tempest.common import compute
+from tempest.common import utils
from tempest.common import waiters
from tempest import config
-from tempest import test
+from tempest.lib import decorators
CONF = config.CONF
@@ -33,21 +35,21 @@
super(DeleteServersTestJSON, cls).setup_clients()
cls.client = cls.servers_client
- @test.idempotent_id('9e6e0c87-3352-42f7-9faf-5d6210dbd159')
+ @decorators.idempotent_id('9e6e0c87-3352-42f7-9faf-5d6210dbd159')
def test_delete_server_while_in_building_state(self):
# Delete a server while it's VM state is Building
server = self.create_test_server(wait_until='BUILD')
self.client.delete_server(server['id'])
waiters.wait_for_server_termination(self.client, server['id'])
- @test.idempotent_id('925fdfb4-5b13-47ea-ac8a-c36ae6fddb05')
+ @decorators.idempotent_id('925fdfb4-5b13-47ea-ac8a-c36ae6fddb05')
def test_delete_active_server(self):
# Delete a server while it's VM state is Active
server = self.create_test_server(wait_until='ACTIVE')
self.client.delete_server(server['id'])
waiters.wait_for_server_termination(self.client, server['id'])
- @test.idempotent_id('546d368c-bb6c-4645-979a-83ed16f3a6be')
+ @decorators.idempotent_id('546d368c-bb6c-4645-979a-83ed16f3a6be')
def test_delete_server_while_in_shutoff_state(self):
# Delete a server while it's VM state is Shutoff
server = self.create_test_server(wait_until='ACTIVE')
@@ -56,7 +58,7 @@
self.client.delete_server(server['id'])
waiters.wait_for_server_termination(self.client, server['id'])
- @test.idempotent_id('943bd6e8-4d7a-4904-be83-7a6cc2d4213b')
+ @decorators.idempotent_id('943bd6e8-4d7a-4904-be83-7a6cc2d4213b')
@testtools.skipUnless(CONF.compute_feature_enabled.pause,
'Pause is not available.')
def test_delete_server_while_in_pause_state(self):
@@ -67,7 +69,7 @@
self.client.delete_server(server['id'])
waiters.wait_for_server_termination(self.client, server['id'])
- @test.idempotent_id('1f82ebd3-8253-4f4e-b93f-de9b7df56d8b')
+ @decorators.idempotent_id('1f82ebd3-8253-4f4e-b93f-de9b7df56d8b')
@testtools.skipUnless(CONF.compute_feature_enabled.suspend,
'Suspend is not available.')
def test_delete_server_while_in_suspended_state(self):
@@ -78,26 +80,18 @@
self.client.delete_server(server['id'])
waiters.wait_for_server_termination(self.client, server['id'])
- @test.idempotent_id('bb0cb402-09dd-4947-b6e5-5e7e1cfa61ad')
+ @decorators.idempotent_id('bb0cb402-09dd-4947-b6e5-5e7e1cfa61ad')
@testtools.skipUnless(CONF.compute_feature_enabled.shelve,
'Shelve is not available.')
def test_delete_server_while_in_shelved_state(self):
# Delete a server while it's VM state is Shelved
server = self.create_test_server(wait_until='ACTIVE')
- self.client.shelve_server(server['id'])
+ compute.shelve_server(self.client, server['id'])
- offload_time = CONF.compute.shelved_offload_time
- if offload_time >= 0:
- waiters.wait_for_server_status(self.client, server['id'],
- 'SHELVED_OFFLOADED',
- extra_timeout=offload_time)
- else:
- waiters.wait_for_server_status(self.client, server['id'],
- 'SHELVED')
self.client.delete_server(server['id'])
waiters.wait_for_server_termination(self.client, server['id'])
- @test.idempotent_id('ab0c38b4-cdd8-49d3-9b92-0cb898723c01')
+ @decorators.idempotent_id('ab0c38b4-cdd8-49d3-9b92-0cb898723c01')
@testtools.skipIf(not CONF.compute_feature_enabled.resize,
'Resize not available.')
def test_delete_server_while_in_verify_resize_state(self):
@@ -109,57 +103,17 @@
self.client.delete_server(server['id'])
waiters.wait_for_server_termination(self.client, server['id'])
- @test.idempotent_id('d0f3f0d6-d9b6-4a32-8da4-23015dcab23c')
- @test.services('volume')
+ @decorators.idempotent_id('d0f3f0d6-d9b6-4a32-8da4-23015dcab23c')
+ @utils.services('volume')
def test_delete_server_while_in_attached_volume(self):
# Delete a server while a volume is attached to it
- volumes_client = self.volumes_extensions_client
device = '/dev/%s' % CONF.compute.volume_device_name
server = self.create_test_server(wait_until='ACTIVE')
- volume = (volumes_client.create_volume(size=CONF.volume.volume_size)
- ['volume'])
- self.addCleanup(volumes_client.delete_volume, volume['id'])
- waiters.wait_for_volume_status(volumes_client,
- volume['id'], 'available')
- self.client.attach_volume(server['id'],
- volumeId=volume['id'],
- device=device)
- waiters.wait_for_volume_status(volumes_client,
- volume['id'], 'in-use')
+ volume = self.create_volume()
+ self.attach_volume(server, volume, device=device)
self.client.delete_server(server['id'])
waiters.wait_for_server_termination(self.client, server['id'])
- waiters.wait_for_volume_status(volumes_client,
- volume['id'], 'available')
-
-
-class DeleteServersAdminTestJSON(base.BaseV2ComputeAdminTest):
- # NOTE: Server creations of each test class should be under 10
- # for preventing "Quota exceeded for instances".
-
- @classmethod
- def setup_clients(cls):
- super(DeleteServersAdminTestJSON, cls).setup_clients()
- cls.non_admin_client = cls.servers_client
- cls.admin_client = cls.os_adm.servers_client
-
- @test.idempotent_id('99774678-e072-49d1-9d2a-49a59bc56063')
- def test_delete_server_while_in_error_state(self):
- # Delete a server while it's VM state is error
- server = self.create_test_server(wait_until='ACTIVE')
- self.admin_client.reset_state(server['id'], state='error')
- # Verify server's state
- server = self.non_admin_client.show_server(server['id'])['server']
- self.assertEqual(server['status'], 'ERROR')
- self.non_admin_client.delete_server(server['id'])
- waiters.wait_for_server_termination(self.servers_client,
- server['id'],
- ignore_error=True)
-
- @test.idempotent_id('73177903-6737-4f27-a60c-379e8ae8cf48')
- def test_admin_delete_servers_of_others(self):
- # Administrator can delete servers of others
- server = self.create_test_server(wait_until='ACTIVE')
- self.admin_client.delete_server(server['id'])
- waiters.wait_for_server_termination(self.servers_client, server['id'])
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume['id'], 'available')
diff --git a/tempest/api/compute/servers/test_device_tagging.py b/tempest/api/compute/servers/test_device_tagging.py
new file mode 100644
index 0000000..ff8ed61
--- /dev/null
+++ b/tempest/api/compute/servers/test_device_tagging.py
@@ -0,0 +1,384 @@
+# Copyright (C) 2016, Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import json
+
+from oslo_log import log as logging
+
+from tempest.api.compute import base
+from tempest.common import utils
+from tempest.common.utils.linux import remote_client
+from tempest.common import waiters
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions
+
+
+CONF = config.CONF
+
+LOG = logging.getLogger(__name__)
+
+
+class DeviceTaggingBase(base.BaseV2ComputeTest):
+
+ @classmethod
+ def skip_checks(cls):
+ super(DeviceTaggingBase, cls).skip_checks()
+ if not CONF.service_available.neutron:
+ 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):
+ raise cls.skipException('One of metadata or config drive must be '
+ 'enabled')
+
+ @classmethod
+ def setup_clients(cls):
+ super(DeviceTaggingBase, cls).setup_clients()
+ cls.networks_client = cls.os_primary.networks_client
+ cls.ports_client = cls.os_primary.ports_client
+ cls.subnets_client = cls.os_primary.subnets_client
+ cls.interfaces_client = cls.os_primary.interfaces_client
+
+ @classmethod
+ def setup_credentials(cls):
+ cls.set_network_resources(network=True, subnet=True, router=True,
+ dhcp=True)
+ super(DeviceTaggingBase, cls).setup_credentials()
+
+ def verify_metadata_from_api(self, server, ssh_client, verify_method):
+ md_url = 'http://169.254.169.254/openstack/latest/meta_data.json'
+ LOG.info('Attempting to verify tagged devices in server %s via '
+ 'the metadata service: %s', server['id'], md_url)
+
+ def get_and_verify_metadata():
+ try:
+ ssh_client.exec_command('curl -V')
+ except exceptions.SSHExecCommandFailed:
+ if not CONF.compute_feature_enabled.config_drive:
+ raise self.skipException('curl not found in guest '
+ 'and config drive is '
+ 'disabled')
+ LOG.warning('curl was not found in the guest, device '
+ 'tagging metadata was not checked in the '
+ 'metadata API')
+ return True
+ cmd = 'curl %s' % md_url
+ md_json = ssh_client.exec_command(cmd)
+ return verify_method(md_json)
+ # NOTE(gmann) Keep refreshing the metadata info until the metadata
+ # cache is refreshed. For safer side, we will go with wait loop of
+ # build_interval till build_timeout. verify_method() above will return
+ # True if all metadata verification is done as expected.
+ if not test_utils.call_until_true(get_and_verify_metadata,
+ CONF.compute.build_timeout,
+ CONF.compute.build_interval):
+ raise exceptions.TimeoutException('Timeout while verifying '
+ 'metadata on server.')
+
+ def verify_metadata_on_config_drive(self, server, ssh_client,
+ verify_method):
+ LOG.info('Attempting to verify tagged devices in server %s via '
+ 'the config drive.', server['id'])
+ ssh_client.mount_config_drive()
+ cmd_md = 'sudo cat /mnt/openstack/latest/meta_data.json'
+ md_json = ssh_client.exec_command(cmd_md)
+ verify_method(md_json)
+ ssh_client.unmount_config_drive()
+
+
+class TaggedBootDevicesTest(DeviceTaggingBase):
+
+ min_microversion = '2.32'
+ # NOTE(mriedem): max_version looks odd but it's actually correct. Due to a
+ # bug in the 2.32 microversion, tags on block devices only worked with the
+ # 2.32 microversion specifically. And tags on networks only worked between
+ # 2.32 and 2.36 inclusive; the 2.37 microversion broke tags for networks.
+ max_microversion = '2.32'
+
+ def verify_device_metadata(self, md_json):
+ md_dict = json.loads(md_json)
+ for d in md_dict['devices']:
+ if d['type'] == 'nic':
+ if d['mac'] == self.port1['mac_address']:
+ self.assertEqual(d['tags'], ['port-1'])
+ if d['mac'] == self.port2['mac_address']:
+ self.assertEqual(d['tags'], ['port-2'])
+ if d['mac'] == self.net_2_100_mac:
+ self.assertEqual(d['tags'], ['net-2-100'])
+ if d['mac'] == self.net_2_200_mac:
+ self.assertEqual(d['tags'], ['net-2-200'])
+
+ # A hypervisor may present multiple paths to a tagged disk, so
+ # there may be duplicated tags in the metadata, use set() to
+ # remove duplicated tags.
+ # Some hypervisors might report devices with no tags as well.
+ found_devices = [d['tags'][0] for d in md_dict['devices']
+ if d.get('tags')]
+ try:
+ self.assertEqual(set(found_devices), set(['port-1', 'port-2',
+ 'net-1', 'net-2-100',
+ 'net-2-200', 'boot',
+ 'other']))
+ return True
+ except Exception:
+ return False
+
+ @decorators.idempotent_id('a2e65a6c-66f1-4442-aaa8-498c31778d96')
+ @utils.services('network', 'volume', 'image')
+ def test_tagged_boot_devices(self):
+ # Create volumes
+ # The create_volume methods waits for the volumes to be available and
+ # the base class will clean them up on tearDown.
+ boot_volume = self.create_volume(CONF.compute.image_ref)
+ other_volume = self.create_volume()
+ untagged_volume = self.create_volume()
+
+ # Create networks
+ net1 = self.networks_client.create_network(
+ name=data_utils.rand_name('device-tagging-net1'))['network']
+ self.addCleanup(self.networks_client.delete_network, net1['id'])
+
+ net2 = self.networks_client.create_network(
+ name=data_utils.rand_name('device-tagging-net2'))['network']
+ self.addCleanup(self.networks_client.delete_network, net2['id'])
+
+ # Create subnets
+ subnet1 = self.subnets_client.create_subnet(
+ network_id=net1['id'],
+ cidr='10.1.1.0/24',
+ ip_version=4)['subnet']
+ self.addCleanup(self.subnets_client.delete_subnet, subnet1['id'])
+
+ subnet2 = self.subnets_client.create_subnet(
+ network_id=net2['id'],
+ cidr='10.2.2.0/24',
+ ip_version=4)['subnet']
+ self.addCleanup(self.subnets_client.delete_subnet, subnet2['id'])
+
+ # Create ports
+ self.port1 = self.ports_client.create_port(
+ network_id=net1['id'],
+ fixed_ips=[{'subnet_id': subnet1['id']}])['port']
+ self.addCleanup(self.ports_client.delete_port, self.port1['id'])
+
+ self.port2 = self.ports_client.create_port(
+ network_id=net1['id'],
+ fixed_ips=[{'subnet_id': subnet1['id']}])['port']
+ self.addCleanup(self.ports_client.delete_port, self.port2['id'])
+
+ # Create server
+ config_drive_enabled = CONF.compute_feature_enabled.config_drive
+ validation_resources = self.get_test_validation_resources(
+ self.os_primary)
+
+ server = self.create_test_server(
+ validatable=True,
+ wait_until='ACTIVE',
+ validation_resources=validation_resources,
+ config_drive=config_drive_enabled,
+ name=data_utils.rand_name('device-tagging-server'),
+ networks=[
+ # Validation network for ssh
+ {
+ 'uuid': self.get_tenant_network()['id']
+ },
+ # Different tags for different ports
+ {
+ 'port': self.port1['id'],
+ 'tag': 'port-1'
+ },
+ {
+ 'port': self.port2['id'],
+ 'tag': 'port-2'
+ },
+ # Two nics on same net, one tagged one not
+ {
+ 'uuid': net1['id'],
+ 'tag': 'net-1'
+ },
+ {
+ 'uuid': net1['id']
+ },
+ # Two nics on same net, different IP
+ {
+ 'uuid': net2['id'],
+ 'fixed_ip': '10.2.2.100',
+ 'tag': 'net-2-100'
+ },
+ {
+ 'uuid': net2['id'],
+ 'fixed_ip': '10.2.2.200',
+ 'tag': 'net-2-200'
+ }
+ ],
+ block_device_mapping_v2=[
+ # Boot volume
+ {
+ 'uuid': boot_volume['id'],
+ 'source_type': 'volume',
+ 'destination_type': 'volume',
+ 'boot_index': 0,
+ 'tag': 'boot'
+ },
+ # Other volume
+ {
+ 'uuid': other_volume['id'],
+ 'source_type': 'volume',
+ 'destination_type': 'volume',
+ 'boot_index': 1,
+ 'tag': 'other'
+ },
+ # Untagged volume
+ {
+ 'uuid': untagged_volume['id'],
+ 'source_type': 'volume',
+ 'destination_type': 'volume',
+ 'boot_index': 2
+ }
+ ])
+
+ self.addCleanup(self.delete_server, server['id'])
+
+ server = self.servers_client.show_server(server['id'])['server']
+ ssh_client = remote_client.RemoteClient(
+ self.get_server_ip(server, validation_resources),
+ CONF.validation.image_ssh_user,
+ pkey=validation_resources['keypair']['private_key'],
+ server=server,
+ servers_client=self.servers_client)
+
+ # Find the MAC addresses of our fixed IPs
+ self.net_2_100_mac = None
+ self.net_2_200_mac = None
+ ifaces = self.interfaces_client.list_interfaces(server['id'])
+ for iface in ifaces['interfaceAttachments']:
+ if 'fixed_ips' in iface:
+ for ip in iface['fixed_ips']:
+ if ip['ip_address'] == '10.2.2.100':
+ self.net_2_100_mac = iface['mac_addr']
+ if ip['ip_address'] == '10.2.2.200':
+ self.net_2_200_mac = iface['mac_addr']
+ # Make sure we have the MACs we need, there's no reason for some to be
+ # missing
+ self.assertTrue(self.net_2_100_mac)
+ self.assertTrue(self.net_2_200_mac)
+
+ # Verify metadata from metadata API
+ if CONF.compute_feature_enabled.metadata_service:
+ self.verify_metadata_from_api(server, ssh_client,
+ self.verify_device_metadata)
+
+ # Verify metadata on config drive
+ if CONF.compute_feature_enabled.config_drive:
+ self.verify_metadata_on_config_drive(server, ssh_client,
+ self.verify_device_metadata)
+
+
+class TaggedBootDevicesTest_v242(TaggedBootDevicesTest):
+ min_microversion = '2.42'
+ max_microversion = 'latest'
+
+
+class TaggedAttachmentsTest(DeviceTaggingBase):
+
+ min_microversion = '2.49'
+ max_microversion = 'latest'
+
+ @classmethod
+ def skip_checks(cls):
+ super(TaggedAttachmentsTest, cls).skip_checks()
+ if not CONF.compute_feature_enabled.metadata_service:
+ raise cls.skipException('Metadata API must be enabled')
+
+ def verify_device_metadata(self, md_json):
+ md_dict = json.loads(md_json)
+ found_devices = [d['tags'][0] for d in md_dict['devices']
+ if d.get('tags')]
+ try:
+ self.assertItemsEqual(found_devices, ['nic-tag', 'volume-tag'])
+ return True
+ except Exception:
+ return False
+
+ def verify_empty_devices(self, md_json):
+ md_dict = json.loads(md_json)
+ try:
+ self.assertEmpty(md_dict['devices'])
+ return True
+ except Exception:
+ return False
+
+ @decorators.idempotent_id('3e41c782-2a89-4922-a9d2-9a188c4e7c7c')
+ @utils.services('network', 'volume', 'image')
+ def test_tagged_attachment(self):
+ # Create network
+ net = self.networks_client.create_network(
+ name=data_utils.rand_name(
+ 'tagged-attachments-test-net'))['network']
+ self.addCleanup(self.networks_client.delete_network, net['id'])
+
+ # Create subnet
+ subnet = self.subnets_client.create_subnet(
+ network_id=net['id'],
+ cidr='10.10.10.0/24',
+ ip_version=4)['subnet']
+ self.addCleanup(self.subnets_client.delete_subnet, subnet['id'])
+
+ # Create volume
+ volume = self.create_volume()
+
+ # Boot test server
+ config_drive_enabled = CONF.compute_feature_enabled.config_drive
+ validation_resources = self.get_test_validation_resources(
+ self.os_primary)
+
+ server = self.create_test_server(
+ validatable=True,
+ validation_resources=validation_resources,
+ config_drive=config_drive_enabled,
+ name=data_utils.rand_name('device-tagging-server'),
+ networks=[{'uuid': self.get_tenant_network()['id']}])
+ self.addCleanup(self.delete_server, server['id'])
+
+ # Attach tagged nic and volume
+ interface = self.interfaces_client.create_interface(
+ server['id'], net_id=net['id'],
+ tag='nic-tag')['interfaceAttachment']
+ self.attach_volume(server, volume, tag='volume-tag')
+
+ ssh_client = remote_client.RemoteClient(
+ self.get_server_ip(server, validation_resources),
+ CONF.validation.image_ssh_user,
+ pkey=validation_resources['keypair']['private_key'],
+ server=server,
+ servers_client=self.servers_client)
+
+ self.verify_metadata_from_api(server, ssh_client,
+ self.verify_device_metadata)
+
+ # Detach tagged nic and volume
+ self.servers_client.detach_volume(server['id'], volume['id'])
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume['id'], 'available')
+ self.interfaces_client.delete_interface(server['id'],
+ interface['port_id'])
+ waiters.wait_for_interface_detach(self.interfaces_client,
+ server['id'],
+ interface['port_id'])
+ self.verify_metadata_from_api(server, ssh_client,
+ self.verify_empty_devices)
diff --git a/tempest/api/compute/servers/test_disk_config.py b/tempest/api/compute/servers/test_disk_config.py
index 617cdd5..bc48069 100644
--- a/tempest/api/compute/servers/test_disk_config.py
+++ b/tempest/api/compute/servers/test_disk_config.py
@@ -18,7 +18,7 @@
from tempest.api.compute import base
from tempest.common import waiters
from tempest import config
-from tempest import test
+from tempest.lib import decorators
CONF = config.CONF
@@ -35,29 +35,26 @@
@classmethod
def setup_clients(cls):
super(ServerDiskConfigTestJSON, cls).setup_clients()
- cls.client = cls.os.servers_client
+ cls.client = cls.os_primary.servers_client
- @classmethod
- def resource_setup(cls):
- super(ServerDiskConfigTestJSON, cls).resource_setup()
- server = cls.create_test_server(wait_until='ACTIVE')
- cls.server_id = server['id']
-
- def _update_server_with_disk_config(self, disk_config):
- server = self.client.show_server(self.server_id)['server']
+ def _update_server_with_disk_config(self, server_id, disk_config):
+ server = self.client.show_server(server_id)['server']
if disk_config != server['OS-DCF:diskConfig']:
server = self.client.update_server(
- self.server_id, disk_config=disk_config)['server']
+ server_id, disk_config=disk_config)['server']
waiters.wait_for_server_status(self.client, server['id'], 'ACTIVE')
server = self.client.show_server(server['id'])['server']
self.assertEqual(disk_config, server['OS-DCF:diskConfig'])
- @test.idempotent_id('bef56b09-2e8c-4883-a370-4950812f430e')
+ @decorators.idempotent_id('bef56b09-2e8c-4883-a370-4950812f430e')
def test_rebuild_server_with_manual_disk_config(self):
# A server should be rebuilt using the manual disk config option
- self._update_server_with_disk_config(disk_config='AUTO')
+ server = self.create_test_server(wait_until='ACTIVE')
+ self.addCleanup(self.client.delete_server, server['id'])
+ self._update_server_with_disk_config(server['id'],
+ disk_config='AUTO')
- server = self.client.rebuild_server(self.server_id,
+ server = self.client.rebuild_server(server['id'],
self.image_ref_alt,
disk_config='MANUAL')['server']
@@ -68,12 +65,15 @@
server = self.client.show_server(server['id'])['server']
self.assertEqual('MANUAL', server['OS-DCF:diskConfig'])
- @test.idempotent_id('9c9fae77-4feb-402f-8450-bf1c8b609713')
+ @decorators.idempotent_id('9c9fae77-4feb-402f-8450-bf1c8b609713')
def test_rebuild_server_with_auto_disk_config(self):
# A server should be rebuilt using the auto disk config option
- self._update_server_with_disk_config(disk_config='MANUAL')
+ server = self.create_test_server(wait_until='ACTIVE')
+ self.addCleanup(self.client.delete_server, server['id'])
+ self._update_server_with_disk_config(server['id'],
+ disk_config='MANUAL')
- server = self.client.rebuild_server(self.server_id,
+ server = self.client.rebuild_server(server['id'],
self.image_ref_alt,
disk_config='AUTO')['server']
@@ -84,59 +84,48 @@
server = self.client.show_server(server['id'])['server']
self.assertEqual('AUTO', server['OS-DCF:diskConfig'])
- def _get_alternative_flavor(self):
- server = self.client.show_server(self.server_id)['server']
-
- if server['flavor']['id'] == self.flavor_ref:
- return self.flavor_ref_alt
- else:
- return self.flavor_ref
-
- @test.idempotent_id('414e7e93-45b5-44bc-8e03-55159c6bfc97')
+ @decorators.idempotent_id('414e7e93-45b5-44bc-8e03-55159c6bfc97')
@testtools.skipUnless(CONF.compute_feature_enabled.resize,
'Resize not available.')
def test_resize_server_from_manual_to_auto(self):
# A server should be resized from manual to auto disk config
- self._update_server_with_disk_config(disk_config='MANUAL')
-
+ server = self.create_test_server(wait_until='ACTIVE')
+ self.addCleanup(self.client.delete_server, server['id'])
+ self._update_server_with_disk_config(server['id'],
+ disk_config='MANUAL')
# Resize with auto option
- flavor_id = self._get_alternative_flavor()
- self.client.resize_server(self.server_id, flavor_id,
- disk_config='AUTO')
- waiters.wait_for_server_status(self.client, self.server_id,
- 'VERIFY_RESIZE')
- self.client.confirm_resize_server(self.server_id)
- waiters.wait_for_server_status(self.client, self.server_id, 'ACTIVE')
+ self.resize_server(server['id'], self.flavor_ref_alt,
+ disk_config='AUTO')
- server = self.client.show_server(self.server_id)['server']
+ server = self.client.show_server(server['id'])['server']
self.assertEqual('AUTO', server['OS-DCF:diskConfig'])
- @test.idempotent_id('693d16f3-556c-489a-8bac-3d0ca2490bad')
+ @decorators.idempotent_id('693d16f3-556c-489a-8bac-3d0ca2490bad')
@testtools.skipUnless(CONF.compute_feature_enabled.resize,
'Resize not available.')
def test_resize_server_from_auto_to_manual(self):
# A server should be resized from auto to manual disk config
- self._update_server_with_disk_config(disk_config='AUTO')
-
+ server = self.create_test_server(wait_until='ACTIVE')
+ self.addCleanup(self.client.delete_server, server['id'])
+ self._update_server_with_disk_config(server['id'],
+ disk_config='AUTO')
# Resize with manual option
- flavor_id = self._get_alternative_flavor()
- self.client.resize_server(self.server_id, flavor_id,
- disk_config='MANUAL')
- waiters.wait_for_server_status(self.client, self.server_id,
- 'VERIFY_RESIZE')
- self.client.confirm_resize_server(self.server_id)
- waiters.wait_for_server_status(self.client, self.server_id, 'ACTIVE')
+ self.resize_server(server['id'], self.flavor_ref_alt,
+ disk_config='MANUAL')
- server = self.client.show_server(self.server_id)['server']
+ server = self.client.show_server(server['id'])['server']
self.assertEqual('MANUAL', server['OS-DCF:diskConfig'])
- @test.idempotent_id('5ef18867-358d-4de9-b3c9-94d4ba35742f')
+ @decorators.idempotent_id('5ef18867-358d-4de9-b3c9-94d4ba35742f')
def test_update_server_from_auto_to_manual(self):
# A server should be updated from auto to manual disk config
- self._update_server_with_disk_config(disk_config='AUTO')
+ server = self.create_test_server(wait_until='ACTIVE')
+ self.addCleanup(self.client.delete_server, server['id'])
+ self._update_server_with_disk_config(server['id'],
+ disk_config='AUTO')
# Update the disk_config attribute to manual
- server = self.client.update_server(self.server_id,
+ server = self.client.update_server(server['id'],
disk_config='MANUAL')['server']
waiters.wait_for_server_status(self.client, server['id'], 'ACTIVE')
diff --git a/tempest/api/compute/servers/test_instance_actions.py b/tempest/api/compute/servers/test_instance_actions.py
index 1367629..b916a42 100644
--- a/tempest/api/compute/servers/test_instance_actions.py
+++ b/tempest/api/compute/servers/test_instance_actions.py
@@ -15,7 +15,7 @@
from tempest.api.compute import base
from tempest.common import waiters
-from tempest import test
+from tempest.lib import decorators
class InstanceActionsTestJSON(base.BaseV2ComputeTest):
@@ -28,26 +28,50 @@
@classmethod
def resource_setup(cls):
super(InstanceActionsTestJSON, cls).resource_setup()
- server = cls.create_test_server(wait_until='ACTIVE')
- cls.request_id = server.response['x-compute-request-id']
- cls.server_id = server['id']
+ cls.server = cls.create_test_server(wait_until='ACTIVE')
+ cls.request_id = cls.server.response['x-compute-request-id']
- @test.idempotent_id('77ca5cc5-9990-45e0-ab98-1de8fead201a')
+ @decorators.idempotent_id('77ca5cc5-9990-45e0-ab98-1de8fead201a')
def test_list_instance_actions(self):
# List actions of the provided server
- self.client.reboot_server(self.server_id, type='HARD')
- waiters.wait_for_server_status(self.client, self.server_id, 'ACTIVE')
+ self.client.reboot_server(self.server['id'], type='HARD')
+ waiters.wait_for_server_status(self.client,
+ self.server['id'], 'ACTIVE')
- body = (self.client.list_instance_actions(self.server_id)
+ body = (self.client.list_instance_actions(self.server['id'])
['instanceActions'])
- self.assertTrue(len(body) == 2, str(body))
- self.assertTrue(any([i for i in body if i['action'] == 'create']))
- self.assertTrue(any([i for i in body if i['action'] == 'reboot']))
+ self.assertEqual(len(body), 2, str(body))
+ self.assertEqual(sorted([i['action'] for i in body]),
+ ['create', 'reboot'])
- @test.idempotent_id('aacc71ca-1d70-4aa5-bbf6-0ff71470e43c')
+ @decorators.idempotent_id('aacc71ca-1d70-4aa5-bbf6-0ff71470e43c')
def test_get_instance_action(self):
# Get the action details of the provided server
body = self.client.show_instance_action(
- self.server_id, self.request_id)['instanceAction']
- self.assertEqual(self.server_id, body['instance_uuid'])
+ self.server['id'], self.request_id)['instanceAction']
+ self.assertEqual(self.server['id'], body['instance_uuid'])
self.assertEqual('create', body['action'])
+
+
+class InstanceActionsV221TestJSON(base.BaseV2ComputeTest):
+
+ min_microversion = '2.21'
+ max_microversion = 'latest'
+
+ @classmethod
+ def setup_clients(cls):
+ super(InstanceActionsV221TestJSON, cls).setup_clients()
+ cls.client = cls.servers_client
+
+ @decorators.idempotent_id('0a0f85d4-10fa-41f6-bf80-a54fb4aa2ae1')
+ def test_get_list_deleted_instance_actions(self):
+
+ # List actions of the deleted server
+ server = self.create_test_server(wait_until='ACTIVE')
+ self.client.delete_server(server['id'])
+ waiters.wait_for_server_termination(self.client, server['id'])
+ body = (self.client.list_instance_actions(server['id'])
+ ['instanceActions'])
+ self.assertEqual(len(body), 2, str(body))
+ self.assertEqual(sorted([i['action'] for i in body]),
+ ['create', 'delete'])
diff --git a/tempest/api/compute/servers/test_instance_actions_negative.py b/tempest/api/compute/servers/test_instance_actions_negative.py
index ac66d05..1d3a790 100644
--- a/tempest/api/compute/servers/test_instance_actions_negative.py
+++ b/tempest/api/compute/servers/test_instance_actions_negative.py
@@ -13,11 +13,10 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.compute import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class InstanceActionsNegativeTestJSON(base.BaseV2ComputeTest):
@@ -30,11 +29,10 @@
@classmethod
def resource_setup(cls):
super(InstanceActionsNegativeTestJSON, cls).resource_setup()
- server = cls.create_test_server(wait_until='ACTIVE')
- cls.server_id = server['id']
+ cls.server = cls.create_test_server(wait_until='ACTIVE')
- @test.attr(type=['negative'])
- @test.idempotent_id('67e1fce6-7ec2-45c6-92d4-0a8f1a632910')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('67e1fce6-7ec2-45c6-92d4-0a8f1a632910')
def test_list_instance_actions_non_existent_server(self):
# List actions of the non-existent server id
non_existent_server_id = data_utils.rand_uuid()
@@ -42,9 +40,9 @@
self.client.list_instance_actions,
non_existent_server_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('0269f40a-6f18-456c-b336-c03623c897f1')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('0269f40a-6f18-456c-b336-c03623c897f1')
def test_get_instance_action_invalid_request(self):
# Get the action details of the provided server with invalid request
self.assertRaises(lib_exc.NotFound, self.client.show_instance_action,
- self.server_id, '999')
+ self.server['id'], '999')
diff --git a/tempest/api/compute/servers/test_list_server_filters.py b/tempest/api/compute/servers/test_list_server_filters.py
index 3acff98..14aecfd 100644
--- a/tempest/api/compute/servers/test_list_server_filters.py
+++ b/tempest/api/compute/servers/test_list_server_filters.py
@@ -12,16 +12,15 @@
# 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 import exceptions as lib_exc
+import testtools
from tempest.api.compute import base
-from tempest.api import utils
-from tempest.common import fixed_network
-from tempest.common.utils import data_utils
from tempest.common import waiters
from tempest import config
-from tempest import test
+from tempest.lib.common import fixed_network
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+
CONF = config.CONF
@@ -42,31 +41,6 @@
def resource_setup(cls):
super(ListServerFiltersTestJSON, cls).resource_setup()
- # Check to see if the alternate image ref actually exists...
- images_client = cls.images_client
- images = images_client.list_images()['images']
-
- if cls.image_ref != cls.image_ref_alt and \
- any([image for image in images
- if image['id'] == cls.image_ref_alt]):
- cls.multiple_images = True
- else:
- cls.image_ref_alt = cls.image_ref
-
- # Do some sanity checks here. If one of the images does
- # not exist, fail early since the tests won't work...
- try:
- cls.images_client.show_image(cls.image_ref)
- except lib_exc.NotFound:
- raise RuntimeError("Image %s (image_ref) was not found!" %
- cls.image_ref)
-
- try:
- cls.images_client.show_image(cls.image_ref_alt)
- except lib_exc.NotFound:
- raise RuntimeError("Image %s (image_ref_alt) was not found!" %
- cls.image_ref_alt)
-
network = cls.get_tenant_network()
if network:
cls.fixed_network_name = network.get('name')
@@ -74,22 +48,28 @@
cls.fixed_network_name = None
network_kwargs = fixed_network.set_networks_kwarg(network)
cls.s1_name = data_utils.rand_name(cls.__name__ + '-instance')
- cls.s1 = cls.create_test_server(name=cls.s1_name,
- wait_until='ACTIVE',
- **network_kwargs)
+ cls.s1 = cls.create_test_server(name=cls.s1_name, **network_kwargs)
cls.s2_name = data_utils.rand_name(cls.__name__ + '-instance')
- cls.s2 = cls.create_test_server(name=cls.s2_name,
- image_id=cls.image_ref_alt,
- wait_until='ACTIVE')
+ # If image_ref_alt is "" or None then we still want to boot a server
+ # but we rely on `testtools.skipUnless` decorator to actually skip
+ # the irrelevant tests.
+ cls.s2 = cls.create_test_server(
+ name=cls.s2_name, image_id=cls.image_ref_alt or cls.image_ref)
cls.s3_name = data_utils.rand_name(cls.__name__ + '-instance')
cls.s3 = cls.create_test_server(name=cls.s3_name,
flavor=cls.flavor_ref_alt,
wait_until='ACTIVE')
- @test.idempotent_id('05e8a8e7-9659-459a-989d-92c2f501f4ba')
- @utils.skip_unless_attr('multiple_images', 'Only one image found')
+ waiters.wait_for_server_status(cls.client, cls.s1['id'],
+ 'ACTIVE')
+ waiters.wait_for_server_status(cls.client, cls.s2['id'],
+ 'ACTIVE')
+
+ @decorators.idempotent_id('05e8a8e7-9659-459a-989d-92c2f501f4ba')
+ @testtools.skipUnless(CONF.compute.image_ref != CONF.compute.image_ref_alt,
+ "Need distinct images to run this test")
def test_list_servers_filter_by_image(self):
# Filter the list of servers by image
params = {'image': self.image_ref}
@@ -100,7 +80,7 @@
self.assertNotIn(self.s2['id'], map(lambda x: x['id'], servers))
self.assertIn(self.s3['id'], map(lambda x: x['id'], servers))
- @test.idempotent_id('573637f5-7325-47bb-9144-3476d0416908')
+ @decorators.idempotent_id('573637f5-7325-47bb-9144-3476d0416908')
def test_list_servers_filter_by_flavor(self):
# Filter the list of servers by flavor
params = {'flavor': self.flavor_ref_alt}
@@ -111,7 +91,7 @@
self.assertNotIn(self.s2['id'], map(lambda x: x['id'], servers))
self.assertIn(self.s3['id'], map(lambda x: x['id'], servers))
- @test.idempotent_id('9b067a7b-7fee-4f6a-b29c-be43fe18fc5a')
+ @decorators.idempotent_id('9b067a7b-7fee-4f6a-b29c-be43fe18fc5a')
def test_list_servers_filter_by_server_name(self):
# Filter the list of servers by server name
params = {'name': self.s1_name}
@@ -122,9 +102,9 @@
self.assertNotIn(self.s2_name, map(lambda x: x['name'], servers))
self.assertNotIn(self.s3_name, map(lambda x: x['name'], servers))
- @test.idempotent_id('ca78e20e-fddb-4ce6-b7f7-bcbf8605e66e')
- def test_list_servers_filter_by_server_status(self):
- # Filter the list of servers by server status
+ @decorators.idempotent_id('ca78e20e-fddb-4ce6-b7f7-bcbf8605e66e')
+ def test_list_servers_filter_by_active_status(self):
+ # Filter the list of servers by server active status
params = {'status': 'active'}
body = self.client.list_servers(**params)
servers = body['servers']
@@ -133,7 +113,7 @@
self.assertIn(self.s2['id'], map(lambda x: x['id'], servers))
self.assertIn(self.s3['id'], map(lambda x: x['id'], servers))
- @test.idempotent_id('451dbbb2-f330-4a9f-b0e1-5f5d2cb0f34c')
+ @decorators.idempotent_id('451dbbb2-f330-4a9f-b0e1-5f5d2cb0f34c')
def test_list_servers_filter_by_shutoff_status(self):
# Filter the list of servers by server shutoff status
params = {'status': 'shutoff'}
@@ -150,21 +130,21 @@
self.assertNotIn(self.s2['id'], map(lambda x: x['id'], servers))
self.assertNotIn(self.s3['id'], map(lambda x: x['id'], servers))
- @test.idempotent_id('614cdfc1-d557-4bac-915b-3e67b48eee76')
+ @decorators.idempotent_id('614cdfc1-d557-4bac-915b-3e67b48eee76')
def test_list_servers_filter_by_limit(self):
# Verify only the expected number of servers are returned
params = {'limit': 1}
servers = self.client.list_servers(**params)
self.assertEqual(1, len([x for x in servers['servers'] if 'id' in x]))
- @test.idempotent_id('b1495414-2d93-414c-8019-849afe8d319e')
+ @decorators.idempotent_id('b1495414-2d93-414c-8019-849afe8d319e')
def test_list_servers_filter_by_zero_limit(self):
# Verify only the expected number of servers are returned
params = {'limit': 0}
servers = self.client.list_servers(**params)
- self.assertEqual(0, len(servers['servers']))
+ self.assertEmpty(servers['servers'])
- @test.idempotent_id('37791bbd-90c0-4de0-831e-5f38cba9c6b3')
+ @decorators.idempotent_id('37791bbd-90c0-4de0-831e-5f38cba9c6b3')
def test_list_servers_filter_by_exceed_limit(self):
# Verify only the expected number of servers are returned
params = {'limit': 100000}
@@ -173,8 +153,9 @@
self.assertEqual(len([x for x in all_servers['servers'] if 'id' in x]),
len([x for x in servers['servers'] if 'id' in x]))
- @test.idempotent_id('b3304c3b-97df-46d2-8cd3-e2b6659724e7')
- @utils.skip_unless_attr('multiple_images', 'Only one image found')
+ @decorators.idempotent_id('b3304c3b-97df-46d2-8cd3-e2b6659724e7')
+ @testtools.skipUnless(CONF.compute.image_ref != CONF.compute.image_ref_alt,
+ "Need distinct images to run this test")
def test_list_servers_detailed_filter_by_image(self):
# Filter the detailed list of servers by image
params = {'image': self.image_ref}
@@ -185,7 +166,7 @@
self.assertNotIn(self.s2['id'], map(lambda x: x['id'], servers))
self.assertIn(self.s3['id'], map(lambda x: x['id'], servers))
- @test.idempotent_id('80c574cc-0925-44ba-8602-299028357dd9')
+ @decorators.idempotent_id('80c574cc-0925-44ba-8602-299028357dd9')
def test_list_servers_detailed_filter_by_flavor(self):
# Filter the detailed list of servers by flavor
params = {'flavor': self.flavor_ref_alt}
@@ -196,7 +177,7 @@
self.assertNotIn(self.s2['id'], map(lambda x: x['id'], servers))
self.assertIn(self.s3['id'], map(lambda x: x['id'], servers))
- @test.idempotent_id('f9eb2b70-735f-416c-b260-9914ac6181e4')
+ @decorators.idempotent_id('f9eb2b70-735f-416c-b260-9914ac6181e4')
def test_list_servers_detailed_filter_by_server_name(self):
# Filter the detailed list of servers by server name
params = {'name': self.s1_name}
@@ -207,7 +188,7 @@
self.assertNotIn(self.s2_name, map(lambda x: x['name'], servers))
self.assertNotIn(self.s3_name, map(lambda x: x['name'], servers))
- @test.idempotent_id('de2612ab-b7dd-4044-b0b1-d2539601911f')
+ @decorators.idempotent_id('de2612ab-b7dd-4044-b0b1-d2539601911f')
def test_list_servers_detailed_filter_by_server_status(self):
# Filter the detailed list of servers by server status
params = {'status': 'active'}
@@ -221,7 +202,7 @@
self.assertEqual(['ACTIVE'] * 3, [x['status'] for x in servers
if x['id'] in test_ids])
- @test.idempotent_id('e9f624ee-92af-4562-8bec-437945a18dcb')
+ @decorators.idempotent_id('e9f624ee-92af-4562-8bec-437945a18dcb')
def test_list_servers_filtered_by_name_wildcard(self):
# List all servers that contains '-instance' in name
params = {'name': '-instance'}
@@ -243,7 +224,7 @@
self.assertNotIn(self.s2_name, map(lambda x: x['name'], servers))
self.assertNotIn(self.s3_name, map(lambda x: x['name'], servers))
- @test.idempotent_id('24a89b0c-0d55-4a28-847f-45075f19b27b')
+ @decorators.idempotent_id('24a89b0c-0d55-4a28-847f-45075f19b27b')
def test_list_servers_filtered_by_name_regex(self):
# list of regex that should match s1, s2 and s3
regexes = ['^.*\-instance\-[0-9]+$', '^.*\-instance\-.*$']
@@ -267,30 +248,44 @@
self.assertNotIn(self.s2_name, map(lambda x: x['name'], servers))
self.assertNotIn(self.s3_name, map(lambda x: x['name'], servers))
- @test.idempotent_id('43a1242e-7b31-48d1-88f2-3f72aa9f2077')
+ @decorators.idempotent_id('43a1242e-7b31-48d1-88f2-3f72aa9f2077')
def test_list_servers_filtered_by_ip(self):
# Filter servers by ip
# Here should be listed 1 server
if not self.fixed_network_name:
msg = 'fixed_network_name needs to be configured to run this test'
raise self.skipException(msg)
+
+ # list servers filter by ip is something "regexp match", i.e,
+ # filter by "10.1.1.1" will return both "10.1.1.1" and "10.1.1.10".
+ # so here look for the longest server ip, and filter by that ip,
+ # so as to ensure only one server is returned.
+ ip_list = {}
self.s1 = self.client.show_server(self.s1['id'])['server']
- for addr_spec in self.s1['addresses'][self.fixed_network_name]:
- ip = addr_spec['addr']
- if addr_spec['version'] == 4:
- params = {'ip': ip}
- break
- else:
- msg = "Skipped until bug 1450859 is resolved"
- raise self.skipException(msg)
+ # Get first ip address in spite of v4 or v6
+ ip_addr = self.s1['addresses'][self.fixed_network_name][0]['addr']
+ ip_list[ip_addr] = self.s1['id']
+
+ self.s2 = self.client.show_server(self.s2['id'])['server']
+ ip_addr = self.s2['addresses'][self.fixed_network_name][0]['addr']
+ ip_list[ip_addr] = self.s2['id']
+
+ self.s3 = self.client.show_server(self.s3['id'])['server']
+ ip_addr = self.s3['addresses'][self.fixed_network_name][0]['addr']
+ ip_list[ip_addr] = self.s3['id']
+
+ longest_ip = max([[len(ip), ip] for ip in ip_list])[1]
+ params = {'ip': longest_ip}
body = self.client.list_servers(**params)
servers = body['servers']
- self.assertIn(self.s1_name, map(lambda x: x['name'], servers))
- self.assertNotIn(self.s2_name, map(lambda x: x['name'], servers))
- self.assertNotIn(self.s3_name, map(lambda x: x['name'], servers))
+ self.assertIn(ip_list[longest_ip], map(lambda x: x['id'], servers))
+ del ip_list[longest_ip]
+ for ip in ip_list:
+ self.assertNotIn(ip_list[ip], map(lambda x: x['id'], servers))
- @test.idempotent_id('a905e287-c35e-42f2-b132-d02b09f3654a')
+ @decorators.skip_because(bug="1540645")
+ @decorators.idempotent_id('a905e287-c35e-42f2-b132-d02b09f3654a')
def test_list_servers_filtered_by_ip_regex(self):
# Filter servers by regex ip
# List all servers filtered by part of ip address.
@@ -320,7 +315,7 @@
"%s not found in %s, all servers %s" %
(self.s3_name, servers, all_servers))
- @test.idempotent_id('67aec2d0-35fe-4503-9f92-f13272b867ed')
+ @decorators.idempotent_id('67aec2d0-35fe-4503-9f92-f13272b867ed')
def test_list_servers_detailed_limit_results(self):
# Verify only the expected number of detailed results are returned
params = {'limit': 1}
diff --git a/tempest/api/compute/servers/test_list_servers_negative.py b/tempest/api/compute/servers/test_list_servers_negative.py
index f205ddf..18a78f0 100644
--- a/tempest/api/compute/servers/test_list_servers_negative.py
+++ b/tempest/api/compute/servers/test_list_servers_negative.py
@@ -13,12 +13,10 @@
# License for the specific language governing permissions and limitations
# under the License.
-from six import moves
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.compute import base
from tempest.common import waiters
-from tempest import test
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class ListServersNegativeTestJSON(base.BaseV2ComputeTest):
@@ -36,120 +34,112 @@
# by the test methods in this class. These
# servers are cleaned up automatically in the
# tearDownClass method of the super-class.
- cls.existing_fixtures = []
- cls.deleted_fixtures = []
- for x in moves.xrange(2):
- srv = cls.create_test_server(wait_until='ACTIVE')
- cls.existing_fixtures.append(srv)
+ body = cls.create_test_server(wait_until='ACTIVE', min_count=3)
- srv = cls.create_test_server()
- cls.client.delete_server(srv['id'])
- # We ignore errors on termination because the server may
- # be put into ERROR status on a quick spawn, then delete,
- # as the compute node expects the instance local status
- # to be spawning, not deleted. See LP Bug#1061167
- waiters.wait_for_server_termination(cls.client, srv['id'],
- ignore_error=True)
- cls.deleted_fixtures.append(srv)
+ # delete one of the created servers
+ cls.deleted_id = body['server']['id']
+ cls.client.delete_server(cls.deleted_id)
+ waiters.wait_for_server_termination(cls.client, cls.deleted_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('24a26f1a-1ddc-4eea-b0d7-a90cc874ad8f')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('24a26f1a-1ddc-4eea-b0d7-a90cc874ad8f')
def test_list_servers_with_a_deleted_server(self):
# Verify deleted servers do not show by default in list servers
# List servers and verify server not returned
body = self.client.list_servers()
servers = body['servers']
- deleted_ids = [s['id'] for s in self.deleted_fixtures]
actual = [srv for srv in servers
- if srv['id'] in deleted_ids]
- self.assertEqual([], actual)
+ if srv['id'] == self.deleted_id]
+ self.assertEmpty(actual)
- @test.attr(type=['negative'])
- @test.idempotent_id('ff01387d-c7ad-47b4-ae9e-64fa214638fe')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('ff01387d-c7ad-47b4-ae9e-64fa214638fe')
def test_list_servers_by_non_existing_image(self):
# Listing servers for a non existing image returns empty list
- non_existing_image = '1234abcd-zzz0-aaa9-ppp3-0987654abcde'
- body = self.client.list_servers(image=non_existing_image)
+ body = self.client.list_servers(image='non_existing_image')
servers = body['servers']
- self.assertEqual([], servers)
+ self.assertEmpty(servers)
- @test.attr(type=['negative'])
- @test.idempotent_id('5913660b-223b-44d4-a651-a0fbfd44ca75')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('5913660b-223b-44d4-a651-a0fbfd44ca75')
def test_list_servers_by_non_existing_flavor(self):
# Listing servers by non existing flavor returns empty list
- non_existing_flavor = 1234
- body = self.client.list_servers(flavor=non_existing_flavor)
+ body = self.client.list_servers(flavor='non_existing_flavor')
servers = body['servers']
- self.assertEqual([], servers)
+ self.assertEmpty(servers)
- @test.attr(type=['negative'])
- @test.idempotent_id('e2c77c4a-000a-4af3-a0bd-629a328bde7c')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('e2c77c4a-000a-4af3-a0bd-629a328bde7c')
def test_list_servers_by_non_existing_server_name(self):
# Listing servers for a non existent server name returns empty list
- non_existing_name = 'junk_server_1234'
- body = self.client.list_servers(name=non_existing_name)
+ body = self.client.list_servers(name='non_existing_server_name')
servers = body['servers']
- self.assertEqual([], servers)
+ self.assertEmpty(servers)
- @test.attr(type=['negative'])
- @test.idempotent_id('fcdf192d-0f74-4d89-911f-1ec002b822c4')
+ @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
- non_existing_status = 'BALONEY'
- body = self.client.list_servers(status=non_existing_status)
- servers = body['servers']
- self.assertEqual([], 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')
- @test.idempotent_id('12c80a9f-2dec-480e-882b-98ba15757659')
- def test_list_servers_by_limits(self):
- # List servers by specifying limits
- body = self.client.list_servers(limit=1)
- self.assertEqual(1, len([x for x in body['servers'] if 'id' in x]))
-
- @test.attr(type=['negative'])
- @test.idempotent_id('d47c17fb-eebd-4287-8e95-f20a7e627b18')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('d47c17fb-eebd-4287-8e95-f20a7e627b18')
def test_list_servers_by_limits_greater_than_actual_count(self):
+ # Gather the complete list of servers in the project for reference
+ full_list = self.client.list_servers()['servers']
# List servers by specifying a greater value for limit
- body = self.client.list_servers(limit=100)
- self.assertEqual(len(self.existing_fixtures), len(body['servers']))
+ limit = len(full_list) + 100
+ body = self.client.list_servers(limit=limit)
+ self.assertEqual(len(full_list), len(body['servers']))
- @test.attr(type=['negative'])
- @test.idempotent_id('679bc053-5e70-4514-9800-3dfab1a380a6')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('679bc053-5e70-4514-9800-3dfab1a380a6')
def test_list_servers_by_limits_pass_string(self):
# Return an error if a string value is passed for limit
self.assertRaises(lib_exc.BadRequest, self.client.list_servers,
limit='testing')
- @test.attr(type=['negative'])
- @test.idempotent_id('62610dd9-4713-4ee0-8beb-fd2c1aa7f950')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('62610dd9-4713-4ee0-8beb-fd2c1aa7f950')
def test_list_servers_by_limits_pass_negative_value(self):
# Return an error if a negative value for limit is passed
self.assertRaises(lib_exc.BadRequest, self.client.list_servers,
limit=-1)
- @test.attr(type=['negative'])
- @test.idempotent_id('87d12517-e20a-4c9c-97b6-dd1628d6d6c9')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('87d12517-e20a-4c9c-97b6-dd1628d6d6c9')
def test_list_servers_by_changes_since_invalid_date(self):
# Return an error when invalid date format is passed
params = {'changes-since': '2011/01/01'}
self.assertRaises(lib_exc.BadRequest, self.client.list_servers,
**params)
- @test.attr(type=['negative'])
- @test.idempotent_id('74745ad8-b346-45b5-b9b8-509d7447fc1f')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('74745ad8-b346-45b5-b9b8-509d7447fc1f')
def test_list_servers_by_changes_since_future_date(self):
- # Return an empty list when a date in the future is passed
- changes_since = {'changes-since': '2051-01-01T12:34:00Z'}
+ # Return an empty list when a date in the future is passed.
+ # updated_at field may haven't been set at the point in the boot
+ # process where build_request still exists, so add
+ # {'status': 'ACTIVE'} along with changes-since as filter.
+ changes_since = {'changes-since': '2051-01-01T12:34:00Z',
+ 'status': 'ACTIVE'}
body = self.client.list_servers(**changes_since)
- self.assertEqual(0, len(body['servers']))
+ self.assertEmpty(body['servers'])
- @test.attr(type=['negative'])
- @test.idempotent_id('93055106-2d34-46fe-af68-d9ddbf7ee570')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('93055106-2d34-46fe-af68-d9ddbf7ee570')
def test_list_servers_detail_server_is_deleted(self):
# Server details are not listed for a deleted server
- deleted_ids = [s['id'] for s in self.deleted_fixtures]
body = self.client.list_servers(detail=True)
servers = body['servers']
actual = [srv for srv in servers
- if srv['id'] in deleted_ids]
- self.assertEqual([], actual)
+ if srv['id'] == self.deleted_id]
+ self.assertEmpty(actual)
diff --git a/tempest/api/compute/servers/test_multiple_create.py b/tempest/api/compute/servers/test_multiple_create.py
index eb1beb1..059454d 100644
--- a/tempest/api/compute/servers/test_multiple_create.py
+++ b/tempest/api/compute/servers/test_multiple_create.py
@@ -14,41 +14,32 @@
# under the License.
from tempest.api.compute import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.common import compute
+from tempest.lib import decorators
class MultipleCreateTestJSON(base.BaseV2ComputeTest):
- _name = 'multiple-create-test'
- def _generate_name(self):
- return data_utils.rand_name(self._name)
-
- def _create_multiple_servers(self, name=None, wait_until=None, **kwargs):
- # NOTE: This is the right way to create_multiple servers and manage to
- # get the created servers into the servers list to be cleaned up after
- # all.
- kwargs['name'] = name if name else self._generate_name()
- if wait_until:
- kwargs['wait_until'] = wait_until
- body = self.create_test_server(**kwargs)
-
- return body
-
- @test.idempotent_id('61e03386-89c3-449c-9bb1-a06f423fd9d1')
+ @decorators.idempotent_id('61e03386-89c3-449c-9bb1-a06f423fd9d1')
def test_multiple_create(self):
- body = self._create_multiple_servers(wait_until='ACTIVE',
- min_count=1,
- max_count=2)
+ tenant_network = self.get_tenant_network()
+ body, servers = compute.create_test_server(
+ self.os_primary,
+ wait_until='ACTIVE',
+ min_count=2,
+ tenant_network=tenant_network)
+ for server in servers:
+ self.addCleanup(self.servers_client.delete_server, server['id'])
# NOTE(maurosr): do status response check and also make sure that
# reservation_id is not in the response body when the request send
# contains return_reservation_id=False
self.assertNotIn('reservation_id', body)
+ self.assertEqual(2, len(servers))
- @test.idempotent_id('864777fb-2f1e-44e3-b5b9-3eb6fa84f2f7')
+ @decorators.idempotent_id('864777fb-2f1e-44e3-b5b9-3eb6fa84f2f7')
def test_multiple_create_with_reservation_return(self):
- body = self._create_multiple_servers(wait_until='ACTIVE',
- min_count=1,
- max_count=2,
- return_reservation_id=True)
+ body = self.create_test_server(wait_until='ACTIVE',
+ min_count=1,
+ max_count=2,
+ return_reservation_id=True)
self.assertIn('reservation_id', body)
diff --git a/tempest/api/compute/servers/test_multiple_create_negative.py b/tempest/api/compute/servers/test_multiple_create_negative.py
index 3d8a732..422510f 100644
--- a/tempest/api/compute/servers/test_multiple_create_negative.py
+++ b/tempest/api/compute/servers/test_multiple_create_negative.py
@@ -13,60 +13,46 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.compute import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class MultipleCreateNegativeTestJSON(base.BaseV2ComputeTest):
- _name = 'multiple-create-test'
- def _generate_name(self):
- return data_utils.rand_name(self._name)
-
- def _create_multiple_servers(self, name=None, wait_until=None, **kwargs):
- # This is the right way to create_multiple servers and manage to get
- # the created servers into the servers list to be cleaned up after all.
- kwargs['name'] = kwargs.get('name', self._generate_name())
- body = self.create_test_server(**kwargs)
-
- return body
-
- @test.attr(type=['negative'])
- @test.idempotent_id('daf29d8d-e928-4a01-9a8c-b129603f3fc0')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('daf29d8d-e928-4a01-9a8c-b129603f3fc0')
def test_min_count_less_than_one(self):
invalid_min_count = 0
- self.assertRaises(lib_exc.BadRequest, self._create_multiple_servers,
+ self.assertRaises(lib_exc.BadRequest, self.create_test_server,
min_count=invalid_min_count)
- @test.attr(type=['negative'])
- @test.idempotent_id('999aa722-d624-4423-b813-0d1ac9884d7a')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('999aa722-d624-4423-b813-0d1ac9884d7a')
def test_min_count_non_integer(self):
invalid_min_count = 2.5
- self.assertRaises(lib_exc.BadRequest, self._create_multiple_servers,
+ self.assertRaises(lib_exc.BadRequest, self.create_test_server,
min_count=invalid_min_count)
- @test.attr(type=['negative'])
- @test.idempotent_id('a6f9c2ab-e060-4b82-b23c-4532cb9390ff')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('a6f9c2ab-e060-4b82-b23c-4532cb9390ff')
def test_max_count_less_than_one(self):
invalid_max_count = 0
- self.assertRaises(lib_exc.BadRequest, self._create_multiple_servers,
+ self.assertRaises(lib_exc.BadRequest, self.create_test_server,
max_count=invalid_max_count)
- @test.attr(type=['negative'])
- @test.idempotent_id('9c5698d1-d7af-4c80-b971-9d403135eea2')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('9c5698d1-d7af-4c80-b971-9d403135eea2')
def test_max_count_non_integer(self):
invalid_max_count = 2.5
- self.assertRaises(lib_exc.BadRequest, self._create_multiple_servers,
+ self.assertRaises(lib_exc.BadRequest, self.create_test_server,
max_count=invalid_max_count)
- @test.attr(type=['negative'])
- @test.idempotent_id('476da616-f1ef-4271-a9b1-b9fc87727cdf')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('476da616-f1ef-4271-a9b1-b9fc87727cdf')
def test_max_count_less_than_min_count(self):
min_count = 3
max_count = 2
- self.assertRaises(lib_exc.BadRequest, self._create_multiple_servers,
+ self.assertRaises(lib_exc.BadRequest, self.create_test_server,
min_count=min_count,
max_count=max_count)
diff --git a/tempest/api/compute/servers/test_novnc.py b/tempest/api/compute/servers/test_novnc.py
new file mode 100644
index 0000000..1dfd0f9
--- /dev/null
+++ b/tempest/api/compute/servers/test_novnc.py
@@ -0,0 +1,197 @@
+# Copyright 2016-2017 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.
+
+import struct
+
+import six
+import urllib3
+
+from tempest.api.compute import base
+from tempest.common import compute
+from tempest import config
+from tempest.lib import decorators
+
+CONF = config.CONF
+
+if six.PY2:
+ ord_func = ord
+else:
+ ord_func = int
+
+
+class NoVNCConsoleTestJSON(base.BaseV2ComputeTest):
+
+ @classmethod
+ def skip_checks(cls):
+ super(NoVNCConsoleTestJSON, cls).skip_checks()
+ if not CONF.compute_feature_enabled.vnc_console:
+ raise cls.skipException('VNC Console feature is disabled.')
+
+ def setUp(self):
+ super(NoVNCConsoleTestJSON, self).setUp()
+ self._websocket = None
+
+ def tearDown(self):
+ self.server_check_teardown()
+ super(NoVNCConsoleTestJSON, self).tearDown()
+ if self._websocket is not None:
+ self._websocket.close()
+
+ @classmethod
+ def setup_clients(cls):
+ super(NoVNCConsoleTestJSON, cls).setup_clients()
+ cls.client = cls.servers_client
+
+ @classmethod
+ def resource_setup(cls):
+ super(NoVNCConsoleTestJSON, cls).resource_setup()
+ cls.server = cls.create_test_server(wait_until="ACTIVE")
+
+ def _validate_novnc_html(self, vnc_url):
+ """Verify we can connect to novnc and get back the javascript."""
+ resp = urllib3.PoolManager().request('GET', vnc_url)
+ # Make sure that the GET request was accepted by the novncproxy
+ self.assertEqual(resp.status, 200, 'Got a Bad HTTP Response on the '
+ 'initial call: ' + six.text_type(resp.status))
+ # Do some basic validation to make sure it is an expected HTML document
+ resp_data = resp.data.decode()
+ self.assertIn('<html>', resp_data,
+ 'Not a valid html document in the response.')
+ self.assertIn('</html>', resp_data,
+ 'Not a valid html document in the response.')
+ # Just try to make sure we got JavaScript back for noVNC, since we
+ # won't actually use it since not inside of a browser
+ self.assertIn('noVNC', resp_data,
+ 'Not a valid noVNC javascript html document.')
+ self.assertIn('<script', resp_data,
+ 'Not a valid noVNC javascript html document.')
+
+ def _validate_rfb_negotiation(self):
+ """Verify we can connect to novnc and do the websocket connection."""
+ # Turn the Socket into a WebSocket to do the communication
+ data = self._websocket.receive_frame()
+ self.assertFalse(data is None or not data,
+ 'Token must be invalid because the connection '
+ 'closed.')
+ # Parse the RFB version from the data to make sure it is valid
+ # and belong to the known supported RFB versions.
+ version = float("%d.%d" % (int(data[4:7], base=10),
+ int(data[8:11], base=10)))
+ # Add the max RFB versions supported
+ supported_versions = [3.3, 3.8]
+ self.assertIn(version, supported_versions,
+ 'Bad RFB Version: ' + str(version))
+ # Send our RFB version to the server
+ self._websocket.send_frame(data)
+ # Get the sever authentication type and make sure None is supported
+ data = self._websocket.receive_frame()
+ self.assertIsNotNone(data, 'Expected authentication type None.')
+ data_length = len(data)
+ if version == 3.3:
+ # For RFB 3.3: in the security handshake, rather than a two-way
+ # negotiation, the server decides the security type and sends a
+ # single word(4 bytes).
+ self.assertEqual(
+ data_length, 4, 'Expected authentication type None.')
+ self.assertIn(1, [ord_func(data[i]) for i in (0, 3)],
+ 'Expected authentication type None.')
+ else:
+ self.assertGreaterEqual(
+ len(data), 2, 'Expected authentication type None.')
+ self.assertIn(
+ 1,
+ [ord_func(data[i + 1]) for i in range(ord_func(data[0]))],
+ 'Expected authentication type None.')
+ # Send to the server that we only support authentication
+ # type None
+ self._websocket.send_frame(six.int2byte(1))
+
+ # The server should send 4 bytes of 0's if security
+ # handshake succeeded
+ data = self._websocket.receive_frame()
+ self.assertEqual(
+ len(data), 4,
+ 'Server did not think security was successful.')
+ self.assertEqual(
+ [ord_func(i) for i in data], [0, 0, 0, 0],
+ 'Server did not think security was successful.')
+
+ # Say to leave the desktop as shared as part of client initialization
+ self._websocket.send_frame(six.int2byte(1))
+ # Get the server initialization packet back and make sure it is the
+ # right structure where bytes 20-24 is the name length and
+ # 24-N is the name
+ data = self._websocket.receive_frame()
+ data_length = len(data) if data is not None else 0
+ self.assertFalse(data_length <= 24 or
+ data_length != (struct.unpack(">L",
+ data[20:24])[0] + 24),
+ 'Server initialization was not the right format.')
+ # Since the rest of the data on the screen is arbitrary, we will
+ # close the socket and end our validation of the data at this point
+ # Assert that the latest check was false, meaning that the server
+ # initialization was the right format
+ self.assertFalse(data_length <= 24 or
+ data_length != (struct.unpack(">L",
+ data[20:24])[0] + 24))
+
+ def _validate_websocket_upgrade(self):
+ self.assertTrue(
+ self._websocket.response.startswith(b'HTTP/1.1 101 Switching '
+ b'Protocols\r\n'),
+ '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):
+ body = self.client.get_vnc_console(self.server['id'],
+ type='novnc')['console']
+ self.assertEqual('novnc', body['type'])
+ # Do the initial HTTP Request to novncproxy to get the NoVNC JavaScript
+ self._validate_novnc_html(body['url'])
+ # Do the WebSockify HTTP Request to novncproxy to do the RFB connection
+ self._websocket = compute.create_websocket(body['url'])
+ # Validate that we successfully connected and upgraded to Web Sockets
+ self._validate_websocket_upgrade()
+ # Validate the RFB Negotiation to determine if a valid VNC session
+ self._validate_rfb_negotiation()
+
+ @decorators.idempotent_id('f9c79937-addc-4aaa-9e0e-841eef02aeb7')
+ def test_novnc_bad_token(self):
+ body = self.client.get_vnc_console(self.server['id'],
+ type='novnc')['console']
+ self.assertEqual('novnc', body['type'])
+ # Do the WebSockify HTTP Request to novncproxy with a bad token
+ url = body['url'].replace('token=', 'token=bad')
+ self._websocket = compute.create_websocket(url)
+ # Make sure the novncproxy rejected the connection and closed it
+ data = self._websocket.receive_frame()
+ self.assertTrue(data is None or not data,
+ "The novnc proxy actually sent us some data, but we "
+ "expected it to close the connection.")
diff --git a/tempest/api/compute/servers/test_server_actions.py b/tempest/api/compute/servers/test_server_actions.py
index 71dfd96..9fc5af0 100644
--- a/tempest/api/compute/servers/test_server_actions.py
+++ b/tempest/api/compute/servers/test_server_actions.py
@@ -13,19 +13,20 @@
# License for the specific language governing permissions and limitations
# under the License.
-import logging
-
+from oslo_log import log as logging
from six.moves.urllib import parse as urlparse
-from tempest_lib import decorators
-from tempest_lib import exceptions as lib_exc
import testtools
from tempest.api.compute import base
-from tempest.common.utils import data_utils
+from tempest.common import compute
+from tempest.common import utils
from tempest.common.utils.linux import remote_client
from tempest.common import waiters
from tempest import config
-from tempest import test
+from tempest.lib.common import api_version_utils
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
CONF = config.CONF
@@ -33,8 +34,6 @@
class ServerActionsTestJSON(base.BaseV2ComputeTest):
- run_ssh = CONF.validation.run_validation
-
def setUp(self):
# NOTE(afazekas): Normally we use the same server with all test cases,
# but if it has an issue, we build a new one
@@ -45,13 +44,18 @@
self.server_id, 'ACTIVE')
except lib_exc.NotFound:
# The server was deleted by previous test, create a new one
+ # Use class level validation resources to avoid them being
+ # deleted once a test is over
+ validation_resources = self.get_class_validation_resources(
+ self.os_primary)
server = self.create_test_server(
validatable=True,
+ validation_resources=validation_resources,
wait_until='ACTIVE')
self.__class__.server_id = server['id']
except Exception:
# Rebuild server if something happened to it during a test
- self.__class__.server_id = self.rebuild_server(
+ self.__class__.server_id = self.recreate_server(
self.server_id, validatable=True)
def tearDown(self):
@@ -70,66 +74,96 @@
@classmethod
def resource_setup(cls):
- cls.set_validation_resources()
-
super(ServerActionsTestJSON, cls).resource_setup()
- cls.server_id = cls.rebuild_server(None, validatable=True)
+ cls.server_id = cls.recreate_server(None, validatable=True)
- @test.idempotent_id('6158df09-4b82-4ab3-af6d-29cf36af858d')
+ @decorators.idempotent_id('6158df09-4b82-4ab3-af6d-29cf36af858d')
@testtools.skipUnless(CONF.compute_feature_enabled.change_password,
'Change password not available.')
def test_change_server_password(self):
+ # Since this test messes with the password and makes the
+ # server unreachable, it should create its own server
+ validation_resources = self.get_test_validation_resources(
+ self.os_primary)
+ newserver = self.create_test_server(
+ validatable=True,
+ validation_resources=validation_resources,
+ wait_until='ACTIVE')
# The server's password should be set to the provided password
new_password = 'Newpass1234'
- self.client.change_password(self.server_id, adminPass=new_password)
- waiters.wait_for_server_status(self.client, self.server_id, 'ACTIVE')
+ self.client.change_password(newserver['id'], adminPass=new_password)
+ waiters.wait_for_server_status(self.client, newserver['id'], 'ACTIVE')
if CONF.validation.run_validation:
# Verify that the user can authenticate with the new password
- server = self.client.show_server(self.server_id)['server']
+ server = self.client.show_server(newserver['id'])['server']
linux_client = remote_client.RemoteClient(
- self.get_server_ip(server),
+ self.get_server_ip(server, validation_resources),
self.ssh_user,
- new_password)
+ new_password,
+ server=server,
+ servers_client=self.client)
linux_client.validate_authentication()
def _test_reboot_server(self, reboot_type):
if CONF.validation.run_validation:
+ validation_resources = self.get_class_validation_resources(
+ self.os_primary)
# Get the time the server was last rebooted,
server = self.client.show_server(self.server_id)['server']
linux_client = remote_client.RemoteClient(
- self.get_server_ip(server),
+ self.get_server_ip(server, validation_resources),
self.ssh_user,
self.password,
- self.validation_resources['keypair']['private_key'])
+ validation_resources['keypair']['private_key'],
+ server=server,
+ servers_client=self.client)
boot_time = linux_client.get_boot_time()
+ # NOTE: This sync is for avoiding the loss of pub key data
+ # in a server
+ linux_client.exec_command("sync")
+
self.client.reboot_server(self.server_id, type=reboot_type)
waiters.wait_for_server_status(self.client, self.server_id, 'ACTIVE')
if CONF.validation.run_validation:
# Log in and verify the boot time has changed
linux_client = remote_client.RemoteClient(
- self.get_server_ip(server),
+ self.get_server_ip(server, validation_resources),
self.ssh_user,
self.password,
- self.validation_resources['keypair']['private_key'])
+ validation_resources['keypair']['private_key'],
+ server=server,
+ servers_client=self.client)
new_boot_time = linux_client.get_boot_time()
- self.assertTrue(new_boot_time > boot_time,
- '%s > %s' % (new_boot_time, boot_time))
+ self.assertGreater(new_boot_time, boot_time,
+ '%s > %s' % (new_boot_time, boot_time))
- @test.attr(type='smoke')
- @test.idempotent_id('2cb1baf6-ac8d-4429-bf0d-ba8a0ba53e32')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('2cb1baf6-ac8d-4429-bf0d-ba8a0ba53e32')
def test_reboot_server_hard(self):
# The server should be power cycled
self._test_reboot_server('HARD')
@decorators.skip_because(bug="1014647")
- @test.idempotent_id('4640e3ef-a5df-482e-95a1-ceeeb0faa84d')
+ @decorators.idempotent_id('4640e3ef-a5df-482e-95a1-ceeeb0faa84d')
def test_reboot_server_soft(self):
# The server should be signaled to reboot gracefully
self._test_reboot_server('SOFT')
+ @decorators.idempotent_id('1d1c9104-1b0a-11e7-a3d4-fa163e65f5ce')
+ def test_remove_server_all_security_groups(self):
+ server = self.create_test_server(wait_until='ACTIVE')
+
+ # Remove all Security group
+ self.client.remove_security_group(
+ server['id'], name=server['security_groups'][0]['name'])
+
+ # Verify all Security group
+ server = self.client.show_server(server['id'])['server']
+ self.assertNotIn('security_groups', server)
+
def _rebuild_server_and_check(self, image_ref):
rebuilt_server = (self.client.rebuild_server(self.server_id, image_ref)
['server'])
@@ -139,11 +173,13 @@
.format(image_ref, rebuilt_server['image']['id']))
self.assertEqual(image_ref, rebuilt_server['image']['id'], msg)
- @test.idempotent_id('aaa6cdf3-55a7-461a-add9-1c8596b9a07c')
- def test_rebuild_server(self):
+ def _test_rebuild_server(self):
+ # Get the IPs the server has before rebuilding it
+ original_addresses = (self.client.show_server(self.server_id)['server']
+ ['addresses'])
# The server should be rebuilt using the provided image and data
meta = {'rebuild': 'server'}
- new_name = data_utils.rand_name('server')
+ new_name = data_utils.rand_name(self.__class__.__name__ + '-server')
password = 'rebuildPassw0rd'
rebuilt_server = self.client.rebuild_server(
self.server_id,
@@ -170,17 +206,31 @@
rebuilt_image_id = server['image']['id']
self.assertTrue(self.image_ref_alt.endswith(rebuilt_image_id))
self.assertEqual(new_name, server['name'])
+ self.assertEqual(original_addresses, server['addresses'])
if CONF.validation.run_validation:
- # TODO(jlanoux) add authentication with the provided password
+ validation_resources = self.get_class_validation_resources(
+ self.os_primary)
+ # Authentication is attempted in the following order of priority:
+ # 1.The key passed in, if one was passed in.
+ # 2.Any key we can find through an SSH agent (if allowed).
+ # 3.Any "id_rsa", "id_dsa" or "id_ecdsa" key discoverable in
+ # ~/.ssh/ (if allowed).
+ # 4.Plain username/password auth, if a password was given.
linux_client = remote_client.RemoteClient(
- self.get_server_ip(rebuilt_server),
+ self.get_server_ip(rebuilt_server, validation_resources),
self.ssh_user,
- self.password,
- self.validation_resources['keypair']['private_key'])
+ password,
+ validation_resources['keypair']['private_key'],
+ server=rebuilt_server,
+ servers_client=self.client)
linux_client.validate_authentication()
- @test.idempotent_id('30449a88-5aff-4f9b-9866-6ee9b17f906d')
+ @decorators.idempotent_id('aaa6cdf3-55a7-461a-add9-1c8596b9a07c')
+ def test_rebuild_server(self):
+ self._test_rebuild_server()
+
+ @decorators.idempotent_id('30449a88-5aff-4f9b-9866-6ee9b17f906d')
def test_rebuild_server_in_stop_state(self):
# The server in stop state should be rebuilt using the provided
# image and remain in SHUTOFF state
@@ -212,48 +262,94 @@
self.client.start_server(self.server_id)
- def _test_resize_server_confirm(self, stop=False):
+ @decorators.idempotent_id('b68bd8d6-855d-4212-b59b-2e704044dace')
+ @utils.services('volume')
+ def test_rebuild_server_with_volume_attached(self):
+ # create a new volume and attach it to the server
+ volume = self.create_volume()
+
+ server = self.client.show_server(self.server_id)['server']
+ self.attach_volume(server, volume)
+
+ # run general rebuild test
+ self._test_rebuild_server()
+
+ # make sure the volume is attached to the instance after rebuild
+ vol_after_rebuild = self.volumes_client.show_volume(volume['id'])
+ vol_after_rebuild = vol_after_rebuild['volume']
+ self.assertEqual('in-use', vol_after_rebuild['status'])
+ self.assertEqual(self.server_id,
+ vol_after_rebuild['attachments'][0]['server_id'])
+
+ def _test_resize_server_confirm(self, server_id, stop=False):
# The server's RAM and disk space should be modified to that of
# the provided flavor
if stop:
- self.client.stop_server(self.server_id)
- waiters.wait_for_server_status(self.client, self.server_id,
+ self.client.stop_server(server_id)
+ waiters.wait_for_server_status(self.client, server_id,
'SHUTOFF')
- self.client.resize_server(self.server_id, self.flavor_ref_alt)
- waiters.wait_for_server_status(self.client, self.server_id,
+ self.client.resize_server(server_id, self.flavor_ref_alt)
+ # NOTE(jlk): Explicitly delete the server to get a new one for later
+ # tests. Avoids resize down race issues.
+ self.addCleanup(self.delete_server, server_id)
+ waiters.wait_for_server_status(self.client, server_id,
'VERIFY_RESIZE')
- self.client.confirm_resize_server(self.server_id)
+ self.client.confirm_resize_server(server_id)
expected_status = 'SHUTOFF' if stop else 'ACTIVE'
- waiters.wait_for_server_status(self.client, self.server_id,
+ waiters.wait_for_server_status(self.client, server_id,
expected_status)
- server = self.client.show_server(self.server_id)['server']
+ server = self.client.show_server(server_id)['server']
self.assertEqual(self.flavor_ref_alt, server['flavor']['id'])
if stop:
# NOTE(mriedem): tearDown requires the server to be started.
- self.client.start_server(self.server_id)
+ self.client.start_server(server_id)
- # NOTE(jlk): Explicitly delete the server to get a new one for later
- # tests. Avoids resize down race issues.
- self.addCleanup(self.delete_server, self.server_id)
-
- @test.idempotent_id('1499262a-9328-4eda-9068-db1ac57498d2')
+ @decorators.idempotent_id('1499262a-9328-4eda-9068-db1ac57498d2')
@testtools.skipUnless(CONF.compute_feature_enabled.resize,
'Resize not available.')
def test_resize_server_confirm(self):
- self._test_resize_server_confirm(stop=False)
+ self._test_resize_server_confirm(self.server_id, stop=False)
- @test.idempotent_id('138b131d-66df-48c9-a171-64f45eb92962')
+ @decorators.idempotent_id('e6c28180-7454-4b59-b188-0257af08a63b')
+ @decorators.related_bug('1728603')
+ @testtools.skipUnless(CONF.compute_feature_enabled.resize,
+ 'Resize not available.')
+ @utils.services('volume')
+ def test_resize_volume_backed_server_confirm(self):
+ # We have to create a new server that is volume-backed since the one
+ # from setUp is not volume-backed.
+ server = self.create_test_server(
+ volume_backed=True, wait_until='ACTIVE')
+ self._test_resize_server_confirm(server['id'])
+ if CONF.compute_feature_enabled.console_output:
+ # Now do something interactive with the guest like get its console
+ # output; we don't actually care about the output,
+ # just that it doesn't raise an error.
+ self.client.get_console_output(server['id'])
+ if CONF.validation.run_validation:
+ validation_resources = self.get_class_validation_resources(
+ self.os_primary)
+ linux_client = remote_client.RemoteClient(
+ self.get_server_ip(server, validation_resources),
+ self.ssh_user,
+ password=None,
+ pkey=validation_resources['keypair']['private_key'],
+ server=server,
+ servers_client=self.client)
+ linux_client.validate_authentication()
+
+ @decorators.idempotent_id('138b131d-66df-48c9-a171-64f45eb92962')
@testtools.skipUnless(CONF.compute_feature_enabled.resize,
'Resize not available.')
def test_resize_server_confirm_from_stopped(self):
- self._test_resize_server_confirm(stop=True)
+ self._test_resize_server_confirm(self.server_id, stop=True)
- @test.idempotent_id('c03aab19-adb1-44f5-917d-c419577e9e68')
+ @decorators.idempotent_id('c03aab19-adb1-44f5-917d-c419577e9e68')
@testtools.skipUnless(CONF.compute_feature_enabled.resize,
'Resize not available.')
def test_resize_server_revert(self):
@@ -261,6 +357,9 @@
# values after a resize is reverted
self.client.resize_server(self.server_id, self.flavor_ref_alt)
+ # NOTE(zhufl): Explicitly delete the server to get a new one for later
+ # tests. Avoids resize down race issues.
+ self.addCleanup(self.delete_server, self.server_id)
waiters.wait_for_server_status(self.client, self.server_id,
'VERIFY_RESIZE')
@@ -270,45 +369,68 @@
server = self.client.show_server(self.server_id)['server']
self.assertEqual(self.flavor_ref, server['flavor']['id'])
- @test.idempotent_id('b963d4f1-94b3-4c40-9e97-7b583f46e470')
+ @decorators.idempotent_id('b963d4f1-94b3-4c40-9e97-7b583f46e470')
@testtools.skipUnless(CONF.compute_feature_enabled.snapshot,
'Snapshotting not available, backup not possible.')
- @test.services('image')
+ @utils.services('image')
def test_create_backup(self):
# Positive test:create backup successfully and rotate backups correctly
# create the first and the second backup
+
+ # Check if glance v1 is available to determine which client to use. We
+ # prefer glance v1 for the compute API tests since the compute image
+ # API proxy was written for glance v1.
+ if CONF.image_feature_enabled.api_v1:
+ glance_client = self.os_primary.image_client
+ elif CONF.image_feature_enabled.api_v2:
+ glance_client = self.os_primary.image_client_v2
+ else:
+ raise lib_exc.InvalidConfiguration(
+ 'Either api_v1 or api_v2 must be True in '
+ '[image-feature-enabled].')
+
backup1 = data_utils.rand_name('backup-1')
resp = self.client.create_backup(self.server_id,
backup_type='daily',
rotation=2,
- name=backup1).response
+ name=backup1)
oldest_backup_exist = True
# the oldest one should be deleted automatically in this test
def _clean_oldest_backup(oldest_backup):
if oldest_backup_exist:
try:
- self.os.image_client.delete_image(oldest_backup)
+ glance_client.delete_image(oldest_backup)
except lib_exc.NotFound:
pass
else:
LOG.warning("Deletion of oldest backup %s should not have "
"been successful as it should have been "
- "deleted during rotation." % oldest_backup)
+ "deleted during rotation.", oldest_backup)
- image1_id = data_utils.parse_image_id(resp['location'])
+ if api_version_utils.compare_version_header_to_response(
+ "OpenStack-API-Version", "compute 2.45", resp.response, "lt"):
+ image1_id = resp['image_id']
+ else:
+ image1_id = data_utils.parse_image_id(resp.response['location'])
self.addCleanup(_clean_oldest_backup, image1_id)
- self.os.image_client.wait_for_image_status(image1_id, 'active')
+ waiters.wait_for_image_status(glance_client,
+ image1_id, 'active')
backup2 = data_utils.rand_name('backup-2')
waiters.wait_for_server_status(self.client, self.server_id, 'ACTIVE')
resp = self.client.create_backup(self.server_id,
backup_type='daily',
rotation=2,
- name=backup2).response
- image2_id = data_utils.parse_image_id(resp['location'])
- self.addCleanup(self.os.image_client.delete_image, image2_id)
- self.os.image_client.wait_for_image_status(image2_id, 'active')
+ name=backup2)
+ if api_version_utils.compare_version_header_to_response(
+ "OpenStack-API-Version", "compute 2.45", resp.response, "lt"):
+ image2_id = resp['image_id']
+ else:
+ image2_id = data_utils.parse_image_id(resp.response['location'])
+ self.addCleanup(glance_client.delete_image, image2_id)
+ waiters.wait_for_image_status(glance_client,
+ image2_id, 'active')
# verify they have been created
properties = {
@@ -316,12 +438,22 @@
'backup_type': "daily",
'instance_uuid': self.server_id,
}
- image_list = self.os.image_client.list_images(
- detail=True,
- properties=properties,
- status='active',
- sort_key='created_at',
- sort_dir='asc')['images']
+ params = {
+ 'status': 'active',
+ 'sort_key': 'created_at',
+ 'sort_dir': 'asc'
+ }
+ if CONF.image_feature_enabled.api_v1:
+ for key, value in properties.items():
+ params['property-%s' % key] = value
+ image_list = glance_client.list_images(
+ detail=True,
+ **params)['images']
+ else:
+ # Additional properties are flattened in glance v2.
+ params.update(properties)
+ image_list = glance_client.list_images(params)['images']
+
self.assertEqual(2, len(image_list))
self.assertEqual((backup1, backup2),
(image_list[0]['name'], image_list[1]['name']))
@@ -333,19 +465,22 @@
resp = self.client.create_backup(self.server_id,
backup_type='daily',
rotation=2,
- name=backup3).response
- image3_id = data_utils.parse_image_id(resp['location'])
- self.addCleanup(self.os.image_client.delete_image, image3_id)
+ name=backup3)
+ if api_version_utils.compare_version_header_to_response(
+ "OpenStack-API-Version", "compute 2.45", resp.response, "lt"):
+ image3_id = resp['image_id']
+ else:
+ image3_id = data_utils.parse_image_id(resp.response['location'])
+ self.addCleanup(glance_client.delete_image, image3_id)
# the first back up should be deleted
waiters.wait_for_server_status(self.client, self.server_id, 'ACTIVE')
- self.os.image_client.wait_for_resource_deletion(image1_id)
+ glance_client.wait_for_resource_deletion(image1_id)
oldest_backup_exist = False
- image_list = self.os.image_client.list_images(
- detail=True,
- properties=properties,
- status='active',
- sort_key='created_at',
- sort_dir='asc')['images']
+ if CONF.image_feature_enabled.api_v1:
+ image_list = glance_client.list_images(
+ detail=True, **params)['images']
+ else:
+ image_list = glance_client.list_images(params)['images']
self.assertEqual(2, len(image_list),
'Unexpected number of images for '
'v2:test_create_backup; was the oldest backup not '
@@ -361,7 +496,7 @@
lines = len(output.split('\n'))
self.assertEqual(lines, 10)
- @test.idempotent_id('4b8867e6-fffa-4d54-b1d1-6fdda57be2f3')
+ @decorators.idempotent_id('4b8867e6-fffa-4d54-b1d1-6fdda57be2f3')
@testtools.skipUnless(CONF.compute_feature_enabled.console_output,
'Console output not supported.')
def test_get_console_output(self):
@@ -377,7 +512,7 @@
waiters.wait_for_server_status(self.client, self.server_id, 'ACTIVE')
self.wait_for(self._get_output)
- @test.idempotent_id('89104062-69d8-4b19-a71b-f47b7af093d7')
+ @decorators.idempotent_id('89104062-69d8-4b19-a71b-f47b7af093d7')
@testtools.skipUnless(CONF.compute_feature_enabled.console_output,
'Console output not supported.')
def test_get_console_output_with_unlimited_size(self):
@@ -390,12 +525,12 @@
# NOTE: This test tries to get full length console log, and the
# length should be bigger than the one of test_get_console_output.
- self.assertTrue(lines > 10, "Cannot get enough console log length."
- " (lines: %s)" % lines)
+ self.assertGreater(lines, 10, "Cannot get enough console log "
+ "length. (lines: %s)" % lines)
self.wait_for(_check_full_length_console_log)
- @test.idempotent_id('5b65d4e7-4ecd-437c-83c0-d6b79d927568')
+ @decorators.idempotent_id('5b65d4e7-4ecd-437c-83c0-d6b79d927568')
@testtools.skipUnless(CONF.compute_feature_enabled.console_output,
'Console output not supported.')
def test_get_console_output_server_id_in_shutoff_status(self):
@@ -404,7 +539,7 @@
# NOTE: SHUTOFF is irregular status. To avoid test instability,
# one server is created only for this test without using
- # the server that was created in setupClass.
+ # the server that was created in setUpClass.
server = self.create_test_server(wait_until='ACTIVE')
temp_server_id = server['id']
@@ -412,7 +547,7 @@
waiters.wait_for_server_status(self.client, temp_server_id, 'SHUTOFF')
self.wait_for(self._get_output)
- @test.idempotent_id('bd61a9fd-062f-4670-972b-2d6c3e3b9e73')
+ @decorators.idempotent_id('bd61a9fd-062f-4670-972b-2d6c3e3b9e73')
@testtools.skipUnless(CONF.compute_feature_enabled.pause,
'Pause is not available.')
def test_pause_unpause_server(self):
@@ -421,7 +556,7 @@
self.client.unpause_server(self.server_id)
waiters.wait_for_server_status(self.client, self.server_id, 'ACTIVE')
- @test.idempotent_id('0d8ee21e-b749-462d-83da-b85b41c86c7f')
+ @decorators.idempotent_id('0d8ee21e-b749-462d-83da-b85b41c86c7f')
@testtools.skipUnless(CONF.compute_feature_enabled.suspend,
'Suspend is not available.')
def test_suspend_resume_server(self):
@@ -431,43 +566,65 @@
self.client.resume_server(self.server_id)
waiters.wait_for_server_status(self.client, self.server_id, 'ACTIVE')
- @test.idempotent_id('77eba8e0-036e-4635-944b-f7a8f3b78dc9')
+ @decorators.idempotent_id('77eba8e0-036e-4635-944b-f7a8f3b78dc9')
@testtools.skipUnless(CONF.compute_feature_enabled.shelve,
'Shelve is not available.')
+ @utils.services('image')
def test_shelve_unshelve_server(self):
- self.client.shelve_server(self.server_id)
-
- offload_time = CONF.compute.shelved_offload_time
- if offload_time >= 0:
- waiters.wait_for_server_status(self.client, self.server_id,
- 'SHELVED_OFFLOADED',
- extra_timeout=offload_time)
+ if CONF.image_feature_enabled.api_v2:
+ glance_client = self.os_primary.image_client_v2
+ elif CONF.image_feature_enabled.api_v1:
+ glance_client = self.os_primary.image_client
else:
- waiters.wait_for_server_status(self.client, self.server_id,
- 'SHELVED')
+ raise lib_exc.InvalidConfiguration(
+ 'Either api_v1 or api_v2 must be True in '
+ '[image-feature-enabled].')
+ compute.shelve_server(self.client, self.server_id,
+ force_shelve_offload=True)
- self.client.shelve_offload_server(self.server_id)
- waiters.wait_for_server_status(self.client, self.server_id,
- 'SHELVED_OFFLOADED')
+ def _unshelve_server():
+ server_info = self.client.show_server(self.server_id)['server']
+ if 'SHELVED' in server_info['status']:
+ self.client.unshelve_server(self.server_id)
+ self.addCleanup(_unshelve_server)
server = self.client.show_server(self.server_id)['server']
image_name = server['name'] + '-shelved'
params = {'name': image_name}
- images = self.images_client.list_images(**params)['images']
+ if CONF.image_feature_enabled.api_v2:
+ images = glance_client.list_images(params)['images']
+ elif CONF.image_feature_enabled.api_v1:
+ images = glance_client.list_images(
+ detail=True, **params)['images']
self.assertEqual(1, len(images))
self.assertEqual(image_name, images[0]['name'])
self.client.unshelve_server(self.server_id)
waiters.wait_for_server_status(self.client, self.server_id, 'ACTIVE')
+ glance_client.wait_for_resource_deletion(images[0]['id'])
- @test.idempotent_id('af8eafd4-38a7-4a4b-bdbc-75145a580560')
+ @decorators.idempotent_id('8cf9f450-a871-42cf-9bef-77eba189c0b0')
+ @decorators.related_bug('1745529')
+ @testtools.skipUnless(CONF.compute_feature_enabled.shelve,
+ 'Shelve is not available.')
+ @testtools.skipUnless(CONF.compute_feature_enabled.pause,
+ 'Pause is not available.')
+ def test_shelve_paused_server(self):
+ server = self.create_test_server(wait_until='ACTIVE')
+ self.client.pause_server(server['id'])
+ waiters.wait_for_server_status(self.client, server['id'], 'PAUSED')
+ # Check if Shelve operation is successful on paused server.
+ compute.shelve_server(self.client, server['id'],
+ force_shelve_offload=True)
+
+ @decorators.idempotent_id('af8eafd4-38a7-4a4b-bdbc-75145a580560')
def test_stop_start_server(self):
self.client.stop_server(self.server_id)
waiters.wait_for_server_status(self.client, self.server_id, 'SHUTOFF')
self.client.start_server(self.server_id)
waiters.wait_for_server_status(self.client, self.server_id, 'ACTIVE')
- @test.idempotent_id('80a8094c-211e-440a-ab88-9e59d556c7ee')
+ @decorators.idempotent_id('80a8094c-211e-440a-ab88-9e59d556c7ee')
def test_lock_unlock_server(self):
# Lock the server,try server stop(exceptions throw),unlock it and retry
self.client.lock_server(self.server_id)
@@ -490,7 +647,7 @@
self.assertNotEqual('None', parsed_url.hostname)
self.assertIn(parsed_url.scheme, valid_scheme)
- @test.idempotent_id('c6bc11bf-592e-4015-9319-1c98dc64daf5')
+ @decorators.idempotent_id('c6bc11bf-592e-4015-9319-1c98dc64daf5')
@testtools.skipUnless(CONF.compute_feature_enabled.vnc_console,
'VNC Console feature is disabled.')
def test_get_vnc_console(self):
diff --git a/tempest/api/compute/servers/test_server_addresses.py b/tempest/api/compute/servers/test_server_addresses.py
index 864f38f..f79b05f 100644
--- a/tempest/api/compute/servers/test_server_addresses.py
+++ b/tempest/api/compute/servers/test_server_addresses.py
@@ -13,10 +13,9 @@
# License for the specific language governing permissions and limitations
# under the License.
-import six
-
from tempest.api.compute import base
-from tempest import test
+from tempest.common import utils
+from tempest.lib import decorators
class ServerAddressesTestJSON(base.BaseV2ComputeTest):
@@ -38,9 +37,9 @@
cls.server = cls.create_test_server(wait_until='ACTIVE')
- @test.attr(type='smoke')
- @test.idempotent_id('6eb718c0-02d9-4d5e-acd1-4e0c269cef39')
- @test.services('network')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('6eb718c0-02d9-4d5e-acd1-4e0c269cef39')
+ @utils.services('network')
def test_list_server_addresses(self):
# All public and private addresses for
# a server should be returned
@@ -49,16 +48,13 @@
# We do not know the exact network configuration, but an instance
# should at least have a single public or private address
- self.assertTrue(len(addresses) >= 1)
- for network_name, network_addresses in six.iteritems(addresses):
- self.assertTrue(len(network_addresses) >= 1)
- for address in network_addresses:
- self.assertTrue(address['addr'])
- self.assertTrue(address['version'])
+ self.assertNotEmpty(addresses)
+ for network_addresses in addresses.values():
+ self.assertNotEmpty(network_addresses)
- @test.attr(type='smoke')
- @test.idempotent_id('87bbc374-5538-4f64-b673-2b0e4443cc30')
- @test.services('network')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('87bbc374-5538-4f64-b673-2b0e4443cc30')
+ @utils.services('network')
def test_list_server_addresses_by_network(self):
# Providing a network type should filter
# the addresses return by that type
diff --git a/tempest/api/compute/servers/test_server_addresses_negative.py b/tempest/api/compute/servers/test_server_addresses_negative.py
index 3503dc2..b2b3cc0 100644
--- a/tempest/api/compute/servers/test_server_addresses_negative.py
+++ b/tempest/api/compute/servers/test_server_addresses_negative.py
@@ -13,10 +13,10 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.compute import base
-from tempest import test
+from tempest.common import utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class ServerAddressesNegativeTestJSON(base.BaseV2ComputeTest):
@@ -36,17 +36,17 @@
super(ServerAddressesNegativeTestJSON, cls).resource_setup()
cls.server = cls.create_test_server(wait_until='ACTIVE')
- @test.attr(type=['negative'])
- @test.idempotent_id('02c3f645-2d2e-4417-8525-68c0407d001b')
- @test.services('network')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('02c3f645-2d2e-4417-8525-68c0407d001b')
+ @utils.services('network')
def test_list_server_addresses_invalid_server_id(self):
# List addresses request should fail if server id not in system
self.assertRaises(lib_exc.NotFound, self.client.list_addresses,
'999')
- @test.attr(type=['negative'])
- @test.idempotent_id('a2ab5144-78c0-4942-a0ed-cc8edccfd9ba')
- @test.services('network')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('a2ab5144-78c0-4942-a0ed-cc8edccfd9ba')
+ @utils.services('network')
def test_list_server_addresses_by_network_neg(self):
# List addresses by network should fail if network name not valid
self.assertRaises(lib_exc.NotFound,
diff --git a/tempest/api/compute/servers/test_server_group.py b/tempest/api/compute/servers/test_server_group.py
index c23b365..5286c8f 100644
--- a/tempest/api/compute/servers/test_server_group.py
+++ b/tempest/api/compute/servers/test_server_group.py
@@ -13,24 +13,27 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import decorators
+import testtools
from tempest.api.compute import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.common import compute
+from tempest.common import utils
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
class ServerGroupTestJSON(base.BaseV2ComputeTest):
- """These tests check for the server-group APIs
+ """These tests check for the server-group APIs.
They create/delete server-groups with different policies.
policies = affinity/anti-affinity
It also adds the tests for list and get details of server-groups
"""
+
@classmethod
def skip_checks(cls):
super(ServerGroupTestJSON, cls).skip_checks()
- if not test.is_extension_enabled('os-server-groups', 'compute'):
+ if not utils.is_extension_enabled('os-server-groups', 'compute'):
msg = "os-server-groups extension is not enabled."
raise cls.skipException(msg)
@@ -42,12 +45,10 @@
@classmethod
def resource_setup(cls):
super(ServerGroupTestJSON, cls).resource_setup()
- server_group_name = data_utils.rand_name('server-group')
cls.policy = ['affinity']
cls.created_server_group = cls.create_test_server_group(
- server_group_name,
- cls.policy)
+ policy=cls.policy)
def _create_server_group(self, name, policy):
# create the test server-group with given policy
@@ -70,30 +71,23 @@
server_group = self._create_server_group(name, policy)
self._delete_server_group(server_group)
- @test.idempotent_id('5dc57eda-35b7-4af7-9e5f-3c2be3d2d68b')
+ @decorators.idempotent_id('5dc57eda-35b7-4af7-9e5f-3c2be3d2d68b')
def test_create_delete_server_group_with_affinity_policy(self):
# Create and Delete the server-group with affinity policy
self._create_delete_server_group(self.policy)
- @test.idempotent_id('3645a102-372f-4140-afad-13698d850d23')
+ @decorators.idempotent_id('3645a102-372f-4140-afad-13698d850d23')
def test_create_delete_server_group_with_anti_affinity_policy(self):
# Create and Delete the server-group with anti-affinity policy
policy = ['anti-affinity']
self._create_delete_server_group(policy)
- @decorators.skip_because(bug="1324348")
- @test.idempotent_id('6d9bae05-eb32-425d-a673-e14e1b1c6306')
- def test_create_delete_server_group_with_multiple_policies(self):
- # Create and Delete the server-group with multiple policies
- policies = ['affinity', 'affinity']
- self._create_delete_server_group(policies)
-
- @test.idempotent_id('154dc5a4-a2fe-44b5-b99e-f15806a4a113')
+ @decorators.idempotent_id('154dc5a4-a2fe-44b5-b99e-f15806a4a113')
def test_create_delete_multiple_server_groups_with_same_name_policy(self):
# Create and Delete the server-groups with same name and same policy
server_groups = []
server_group_name = data_utils.rand_name('server-group')
- for i in range(0, 2):
+ for _ in range(0, 2):
server_groups.append(self._create_server_group(server_group_name,
self.policy))
for key in ['name', 'policies']:
@@ -103,15 +97,31 @@
for i in range(0, 2):
self._delete_server_group(server_groups[i])
- @test.idempotent_id('b3545034-dd78-48f0-bdc2-a4adfa6d0ead')
+ @decorators.idempotent_id('b3545034-dd78-48f0-bdc2-a4adfa6d0ead')
def test_show_server_group(self):
# Get the server-group
body = self.client.show_server_group(
self.created_server_group['id'])['server_group']
self.assertEqual(self.created_server_group, body)
- @test.idempotent_id('d4874179-27b4-4d7d-80e4-6c560cdfe321')
+ @decorators.idempotent_id('d4874179-27b4-4d7d-80e4-6c560cdfe321')
def test_list_server_groups(self):
# List the server-group
body = self.client.list_server_groups()['server_groups']
self.assertIn(self.created_server_group, body)
+
+ @decorators.idempotent_id('ed20d3fb-9d1f-4329-b160-543fbd5d9811')
+ @testtools.skipUnless(
+ compute.is_scheduler_filter_enabled("ServerGroupAffinityFilter"),
+ 'ServerGroupAffinityFilter is not available.')
+ def test_create_server_with_scheduler_hint_group(self):
+ # Create a server with the scheduler hint "group".
+ hints = {'group': self.created_server_group['id']}
+ server = self.create_test_server(scheduler_hints=hints,
+ wait_until='ACTIVE')
+ self.addCleanup(self.delete_server, server['id'])
+
+ # Check a server is in the group
+ server_group = (self.server_groups_client.show_server_group(
+ self.created_server_group['id'])['server_group'])
+ self.assertIn(server['id'], server_group['members'])
diff --git a/tempest/api/compute/servers/test_server_metadata.py b/tempest/api/compute/servers/test_server_metadata.py
index 9c07677..fe95018 100644
--- a/tempest/api/compute/servers/test_server_metadata.py
+++ b/tempest/api/compute/servers/test_server_metadata.py
@@ -14,7 +14,7 @@
# under the License.
from tempest.api.compute import base
-from tempest import test
+from tempest.lib import decorators
class ServerMetadataTestJSON(base.BaseV2ComputeTest):
@@ -23,94 +23,91 @@
def setup_clients(cls):
super(ServerMetadataTestJSON, cls).setup_clients()
cls.client = cls.servers_client
- cls.quotas = cls.quotas_client
@classmethod
def resource_setup(cls):
super(ServerMetadataTestJSON, cls).resource_setup()
- server = cls.create_test_server(metadata={}, wait_until='ACTIVE')
- cls.server_id = server['id']
+ cls.server = cls.create_test_server(metadata={}, wait_until='ACTIVE')
def setUp(self):
super(ServerMetadataTestJSON, self).setUp()
meta = {'key1': 'value1', 'key2': 'value2'}
- self.client.set_server_metadata(self.server_id, meta)['metadata']
+ self.client.set_server_metadata(self.server['id'], meta)
- @test.idempotent_id('479da087-92b3-4dcf-aeb3-fd293b2d14ce')
+ @decorators.idempotent_id('479da087-92b3-4dcf-aeb3-fd293b2d14ce')
def test_list_server_metadata(self):
# All metadata key/value pairs for a server should be returned
- resp_metadata = (self.client.list_server_metadata(self.server_id)
+ resp_metadata = (self.client.list_server_metadata(self.server['id'])
['metadata'])
# Verify the expected metadata items are in the list
expected = {'key1': 'value1', 'key2': 'value2'}
self.assertEqual(expected, resp_metadata)
- @test.idempotent_id('211021f6-21de-4657-a68f-908878cfe251')
+ @decorators.idempotent_id('211021f6-21de-4657-a68f-908878cfe251')
def test_set_server_metadata(self):
# The server's metadata should be replaced with the provided values
# Create a new set of metadata for the server
req_metadata = {'meta2': 'data2', 'meta3': 'data3'}
- self.client.set_server_metadata(self.server_id,
- req_metadata)['metadata']
+ self.client.set_server_metadata(self.server['id'], req_metadata)
# Verify the expected values are correct, and that the
# previous values have been removed
- resp_metadata = (self.client.list_server_metadata(self.server_id)
+ resp_metadata = (self.client.list_server_metadata(self.server['id'])
['metadata'])
self.assertEqual(resp_metadata, req_metadata)
- @test.idempotent_id('344d981e-0c33-4997-8a5d-6c1d803e4134')
+ @decorators.idempotent_id('344d981e-0c33-4997-8a5d-6c1d803e4134')
def test_update_server_metadata(self):
# The server's metadata values should be updated to the
# provided values
meta = {'key1': 'alt1', 'key3': 'value3'}
- self.client.update_server_metadata(self.server_id, meta)
+ self.client.update_server_metadata(self.server['id'], meta)
# Verify the values have been updated to the proper values
- resp_metadata = (self.client.list_server_metadata(self.server_id)
+ resp_metadata = (self.client.list_server_metadata(self.server['id'])
['metadata'])
expected = {'key1': 'alt1', 'key2': 'value2', 'key3': 'value3'}
self.assertEqual(expected, resp_metadata)
- @test.idempotent_id('0f58d402-e34a-481d-8af8-b392b17426d9')
+ @decorators.idempotent_id('0f58d402-e34a-481d-8af8-b392b17426d9')
def test_update_metadata_empty_body(self):
# The original metadata should not be lost if empty metadata body is
# passed
meta = {}
- self.client.update_server_metadata(self.server_id, meta)
- resp_metadata = (self.client.list_server_metadata(self.server_id)
+ self.client.update_server_metadata(self.server['id'], meta)
+ resp_metadata = (self.client.list_server_metadata(self.server['id'])
['metadata'])
expected = {'key1': 'value1', 'key2': 'value2'}
self.assertEqual(expected, resp_metadata)
- @test.idempotent_id('3043c57d-7e0e-49a6-9a96-ad569c265e6a')
+ @decorators.idempotent_id('3043c57d-7e0e-49a6-9a96-ad569c265e6a')
def test_get_server_metadata_item(self):
# The value for a specific metadata key should be returned
- meta = self.client.show_server_metadata_item(self.server_id,
+ meta = self.client.show_server_metadata_item(self.server['id'],
'key2')['meta']
self.assertEqual('value2', meta['key2'])
- @test.idempotent_id('58c02d4f-5c67-40be-8744-d3fa5982eb1c')
+ @decorators.idempotent_id('58c02d4f-5c67-40be-8744-d3fa5982eb1c')
def test_set_server_metadata_item(self):
# The item's value should be updated to the provided value
# Update the metadata value
meta = {'nova': 'alt'}
- self.client.set_server_metadata_item(self.server_id, 'nova', meta)
+ self.client.set_server_metadata_item(self.server['id'], 'nova', meta)
# Verify the meta item's value has been updated
- resp_metadata = (self.client.list_server_metadata(self.server_id)
+ resp_metadata = (self.client.list_server_metadata(self.server['id'])
['metadata'])
expected = {'key1': 'value1', 'key2': 'value2', 'nova': 'alt'}
self.assertEqual(expected, resp_metadata)
- @test.idempotent_id('127642d6-4c7b-4486-b7cd-07265a378658')
+ @decorators.idempotent_id('127642d6-4c7b-4486-b7cd-07265a378658')
def test_delete_server_metadata_item(self):
# The metadata value/key pair should be deleted from the server
- self.client.delete_server_metadata_item(self.server_id, 'key1')
+ self.client.delete_server_metadata_item(self.server['id'], 'key1')
# Verify the metadata item has been removed
- resp_metadata = (self.client.list_server_metadata(self.server_id)
+ resp_metadata = (self.client.list_server_metadata(self.server['id'])
['metadata'])
expected = {'key2': 'value2'}
self.assertEqual(expected, resp_metadata)
diff --git a/tempest/api/compute/servers/test_server_metadata_negative.py b/tempest/api/compute/servers/test_server_metadata_negative.py
index 18d80be..482ba09 100644
--- a/tempest/api/compute/servers/test_server_metadata_negative.py
+++ b/tempest/api/compute/servers/test_server_metadata_negative.py
@@ -13,11 +13,10 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.compute import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class ServerMetadataNegativeTestJSON(base.BaseV2ComputeTest):
@@ -26,18 +25,15 @@
def setup_clients(cls):
super(ServerMetadataNegativeTestJSON, cls).setup_clients()
cls.client = cls.servers_client
- cls.quotas = cls.quotas_client
@classmethod
def resource_setup(cls):
super(ServerMetadataNegativeTestJSON, cls).resource_setup()
cls.tenant_id = cls.client.tenant_id
- server = cls.create_test_server(metadata={}, wait_until='ACTIVE')
+ cls.server = cls.create_test_server(metadata={}, wait_until='ACTIVE')
- cls.server_id = server['id']
-
- @test.attr(type=['negative'])
- @test.idempotent_id('fe114a8f-3a57-4eff-9ee2-4e14628df049')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('fe114a8f-3a57-4eff-9ee2-4e14628df049')
def test_server_create_metadata_key_too_long(self):
# Attempt to start a server with a meta-data key that is > 255
# characters
@@ -52,8 +48,8 @@
# no teardown - all creates should fail
- @test.attr(type=['negative'])
- @test.idempotent_id('92431555-4d8b-467c-b95b-b17daa5e57ff')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('92431555-4d8b-467c-b95b-b17daa5e57ff')
def test_create_server_metadata_blank_key(self):
# Blank key should trigger an error.
meta = {'': 'data1'}
@@ -61,8 +57,8 @@
self.create_test_server,
metadata=meta)
- @test.attr(type=['negative'])
- @test.idempotent_id('4d9cd7a3-2010-4b41-b8fe-3bbf0b169466')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('4d9cd7a3-2010-4b41-b8fe-3bbf0b169466')
def test_server_metadata_non_existent_server(self):
# GET on a non-existent server should not succeed
non_existent_server_id = data_utils.rand_uuid()
@@ -71,8 +67,8 @@
non_existent_server_id,
'test2')
- @test.attr(type=['negative'])
- @test.idempotent_id('f408e78e-3066-4097-9299-3b0182da812e')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('f408e78e-3066-4097-9299-3b0182da812e')
def test_list_server_metadata_non_existent_server(self):
# List metadata on a non-existent server should not succeed
non_existent_server_id = data_utils.rand_uuid()
@@ -80,18 +76,18 @@
self.client.list_server_metadata,
non_existent_server_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('0025fbd6-a4ba-4cde-b8c2-96805dcfdabc')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('0025fbd6-a4ba-4cde-b8c2-96805dcfdabc')
def test_wrong_key_passed_in_body(self):
# Raise BadRequest if key in uri does not match
# the key passed in body.
meta = {'testkey': 'testvalue'}
self.assertRaises(lib_exc.BadRequest,
self.client.set_server_metadata_item,
- self.server_id, 'key', meta)
+ self.server['id'], 'key', meta)
- @test.attr(type=['negative'])
- @test.idempotent_id('0df38c2a-3d4e-4db5-98d8-d4d9fa843a12')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('0df38c2a-3d4e-4db5-98d8-d4d9fa843a12')
def test_set_metadata_non_existent_server(self):
# Set metadata on a non-existent server should not succeed
non_existent_server_id = data_utils.rand_uuid()
@@ -101,8 +97,8 @@
non_existent_server_id,
meta)
- @test.attr(type=['negative'])
- @test.idempotent_id('904b13dc-0ef2-4e4c-91cd-3b4a0f2f49d8')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('904b13dc-0ef2-4e4c-91cd-3b4a0f2f49d8')
def test_update_metadata_non_existent_server(self):
# An update should not happen for a non-existent server
non_existent_server_id = data_utils.rand_uuid()
@@ -112,17 +108,17 @@
non_existent_server_id,
meta)
- @test.attr(type=['negative'])
- @test.idempotent_id('a452f38c-05c2-4b47-bd44-a4f0bf5a5e48')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('a452f38c-05c2-4b47-bd44-a4f0bf5a5e48')
def test_update_metadata_with_blank_key(self):
# Blank key should trigger an error
meta = {'': 'data1'}
self.assertRaises(lib_exc.BadRequest,
self.client.update_server_metadata,
- self.server_id, meta=meta)
+ self.server['id'], meta=meta)
- @test.attr(type=['negative'])
- @test.idempotent_id('6bbd88e1-f8b3-424d-ba10-ae21c45ada8d')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('6bbd88e1-f8b3-424d-ba10-ae21c45ada8d')
def test_delete_metadata_non_existent_server(self):
# Should not be able to delete metadata item from a non-existent server
non_existent_server_id = data_utils.rand_uuid()
@@ -131,13 +127,14 @@
non_existent_server_id,
'd')
- @test.attr(type=['negative'])
- @test.idempotent_id('d8c0a210-a5c3-4664-be04-69d96746b547')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('d8c0a210-a5c3-4664-be04-69d96746b547')
def test_metadata_items_limit(self):
# A 403 Forbidden or 413 Overlimit (old behaviour) exception
# will be raised while exceeding metadata items limit for
# tenant.
- quota_set = self.quotas.show_quota_set(self.tenant_id)['quota_set']
+ quota_set = self.quotas_client.show_quota_set(
+ self.tenant_id)['quota_set']
quota_metadata = quota_set['metadata_items']
if quota_metadata == -1:
raise self.skipException("No limit for metadata_items")
@@ -147,31 +144,31 @@
req_metadata['key' + str(num)] = 'val' + str(num)
self.assertRaises((lib_exc.OverLimit, lib_exc.Forbidden),
self.client.set_server_metadata,
- self.server_id, req_metadata)
+ self.server['id'], req_metadata)
# A 403 Forbidden or 413 Overlimit (old behaviour) exception
# will be raised while exceeding metadata items limit for
# tenant.
self.assertRaises((lib_exc.Forbidden, lib_exc.OverLimit),
self.client.update_server_metadata,
- self.server_id, req_metadata)
+ self.server['id'], req_metadata)
- @test.attr(type=['negative'])
- @test.idempotent_id('96100343-7fa9-40d8-80fa-d29ef588ce1c')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('96100343-7fa9-40d8-80fa-d29ef588ce1c')
def test_set_server_metadata_blank_key(self):
# Raise a bad request error for blank key.
# set_server_metadata will replace all metadata with new value
meta = {'': 'data1'}
self.assertRaises(lib_exc.BadRequest,
self.client.set_server_metadata,
- self.server_id, meta=meta)
+ self.server['id'], meta=meta)
- @test.attr(type=['negative'])
- @test.idempotent_id('64a91aee-9723-4863-be44-4c9d9f1e7d0e')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('64a91aee-9723-4863-be44-4c9d9f1e7d0e')
def test_set_server_metadata_missing_metadata(self):
# Raise a bad request error for a missing metadata field
# set_server_metadata will replace all metadata with new value
meta = {'meta1': 'data1'}
self.assertRaises(lib_exc.BadRequest,
self.client.set_server_metadata,
- self.server_id, meta=meta, no_metadata_field=True)
+ self.server['id'], meta=meta, no_metadata_field=True)
diff --git a/tempest/api/compute/servers/test_server_password.py b/tempest/api/compute/servers/test_server_password.py
index 9b41708..e6a668a 100644
--- a/tempest/api/compute/servers/test_server_password.py
+++ b/tempest/api/compute/servers/test_server_password.py
@@ -15,25 +15,20 @@
from tempest.api.compute import base
-from tempest import test
+from tempest.lib import decorators
class ServerPasswordTestJSON(base.BaseV2ComputeTest):
@classmethod
- def setup_clients(cls):
- super(ServerPasswordTestJSON, cls).setup_clients()
- cls.client = cls.servers_client
-
- @classmethod
def resource_setup(cls):
super(ServerPasswordTestJSON, cls).resource_setup()
cls.server = cls.create_test_server(wait_until="ACTIVE")
- @test.idempotent_id('f83b582f-62a8-4f22-85b0-0dee50ff783a')
+ @decorators.idempotent_id('f83b582f-62a8-4f22-85b0-0dee50ff783a')
def test_get_server_password(self):
- self.client.show_password(self.server['id'])
+ self.servers_client.show_password(self.server['id'])
- @test.idempotent_id('f8229e8b-b625-4493-800a-bde86ac611ea')
+ @decorators.idempotent_id('f8229e8b-b625-4493-800a-bde86ac611ea')
def test_delete_server_password(self):
- self.client.delete_password(self.server['id'])
+ self.servers_client.delete_password(self.server['id'])
diff --git a/tempest/api/compute/servers/test_server_personality.py b/tempest/api/compute/servers/test_server_personality.py
index c948f8c..6f32b46 100644
--- a/tempest/api/compute/servers/test_server_personality.py
+++ b/tempest/api/compute/servers/test_server_personality.py
@@ -13,14 +13,16 @@
# License for the specific language governing permissions and limitations
# under the License.
-import base64
-from tempest_lib import exceptions as lib_exc
+from oslo_serialization import base64
from tempest.api.compute import base
from tempest.common.utils.linux import remote_client
from tempest.common import waiters
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
CONF = config.CONF
@@ -33,11 +35,6 @@
super(ServerPersonalityTestJSON, cls).setup_credentials()
@classmethod
- def resource_setup(cls):
- cls.set_validation_resources()
- super(ServerPersonalityTestJSON, cls).resource_setup()
-
- @classmethod
def skip_checks(cls):
super(ServerPersonalityTestJSON, cls).skip_checks()
if not CONF.compute_feature_enabled.personality:
@@ -47,34 +44,52 @@
def setup_clients(cls):
super(ServerPersonalityTestJSON, cls).setup_clients()
cls.client = cls.servers_client
- cls.user_client = cls.limits_client
- @test.idempotent_id('3cfe87fd-115b-4a02-b942-7dc36a337fdf')
+ @decorators.idempotent_id('3cfe87fd-115b-4a02-b942-7dc36a337fdf')
def test_create_server_with_personality(self):
file_contents = 'This is a test file.'
file_path = '/test.txt'
personality = [{'path': file_path,
- 'contents': base64.b64encode(file_contents)}]
- created_server = self.create_test_server(personality=personality,
- wait_until='ACTIVE',
- validatable=True)
+ 'contents': base64.encode_as_text(file_contents)}]
+ password = data_utils.rand_password()
+ validation_resources = self.get_test_validation_resources(
+ self.os_primary)
+ created_server = self.create_test_server(
+ personality=personality, adminPass=password, wait_until='ACTIVE',
+ validatable=True,
+ validation_resources=validation_resources)
+ self.addCleanup(waiters.wait_for_server_termination,
+ self.servers_client, created_server['id'])
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.servers_client.delete_server,
+ created_server['id'])
server = self.client.show_server(created_server['id'])['server']
if CONF.validation.run_validation:
linux_client = remote_client.RemoteClient(
- self.get_server_ip(server),
- self.ssh_user, created_server['adminPass'],
- self.validation_resources['keypair']['private_key'])
+ self.get_server_ip(server, validation_resources),
+ self.ssh_user, password,
+ validation_resources['keypair']['private_key'],
+ server=server,
+ servers_client=self.client)
self.assertEqual(file_contents,
linux_client.exec_command(
'sudo cat %s' % file_path))
- @test.idempotent_id('128966d8-71fc-443c-8cab-08e24114ecc9')
+ @decorators.idempotent_id('128966d8-71fc-443c-8cab-08e24114ecc9')
def test_rebuild_server_with_personality(self):
- server = self.create_test_server(wait_until='ACTIVE', validatable=True)
+ validation_resources = self.get_test_validation_resources(
+ self.os_primary)
+ server = self.create_test_server(
+ wait_until='ACTIVE', validatable=True,
+ validation_resources=validation_resources)
server_id = server['id']
+ self.addCleanup(waiters.wait_for_server_termination,
+ self.servers_client, server_id)
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.servers_client.delete_server, server_id)
file_contents = 'Test server rebuild.'
personality = [{'path': 'rebuild.txt',
- 'contents': base64.b64encode(file_contents)}]
+ 'contents': base64.encode_as_text(file_contents)}]
rebuilt_server = self.client.rebuild_server(server_id,
self.image_ref_alt,
personality=personality)
@@ -82,51 +97,64 @@
self.assertEqual(self.image_ref_alt,
rebuilt_server['server']['image']['id'])
- @test.idempotent_id('176cd8c9-b9e8-48ee-a480-180beab292bf')
+ @decorators.idempotent_id('176cd8c9-b9e8-48ee-a480-180beab292bf')
def test_personality_files_exceed_limit(self):
# Server creation should fail if greater than the maximum allowed
# number of files are injected into the server.
file_contents = 'This is a test file.'
personality = []
- limits = self.user_client.show_limits()['limits']
+ limits = self.limits_client.show_limits()['limits']
max_file_limit = limits['absolute']['maxPersonality']
if max_file_limit == -1:
raise self.skipException("No limit for personality files")
- for i in range(0, int(max_file_limit) + 1):
+ for i in range(0, max_file_limit + 1):
path = 'etc/test' + str(i) + '.txt'
personality.append({'path': path,
- 'contents': base64.b64encode(file_contents)})
+ 'contents': base64.encode_as_text(
+ file_contents)})
# A 403 Forbidden or 413 Overlimit (old behaviour) exception
# will be raised when out of quota
self.assertRaises((lib_exc.Forbidden, lib_exc.OverLimit),
self.create_test_server, personality=personality)
- @test.idempotent_id('52f12ee8-5180-40cc-b417-31572ea3d555')
+ @decorators.idempotent_id('52f12ee8-5180-40cc-b417-31572ea3d555')
def test_can_create_server_with_max_number_personality_files(self):
# Server should be created successfully if maximum allowed number of
# files is injected into the server during creation.
file_contents = 'This is a test file.'
- limits = self.user_client.show_limits()['limits']
+ limits = self.limits_client.show_limits()['limits']
max_file_limit = limits['absolute']['maxPersonality']
if max_file_limit == -1:
raise self.skipException("No limit for personality files")
person = []
- for i in range(0, int(max_file_limit)):
- path = '/etc/test' + str(i) + '.txt'
+ for i in range(0, max_file_limit):
+ # NOTE(andreaf) The cirros disk image is blank before boot
+ # so we can only inject safely to /
+ path = '/test' + str(i) + '.txt'
person.append({
'path': path,
- 'contents': base64.b64encode(file_contents),
+ 'contents': base64.encode_as_text(file_contents + str(i)),
})
- created_server = self.create_test_server(personality=person,
- wait_until='ACTIVE',
- validatable=True)
+ password = data_utils.rand_password()
+ validation_resources = self.get_test_validation_resources(
+ self.os_primary)
+ created_server = self.create_test_server(
+ personality=person, adminPass=password, wait_until='ACTIVE',
+ validatable=True, validation_resources=validation_resources)
+ self.addCleanup(waiters.wait_for_server_termination,
+ self.servers_client, created_server['id'])
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.servers_client.delete_server,
+ created_server['id'])
server = self.client.show_server(created_server['id'])['server']
if CONF.validation.run_validation:
linux_client = remote_client.RemoteClient(
- self.get_server_ip(server),
- self.ssh_user, created_server['adminPass'],
- self.validation_resources['keypair']['private_key'])
+ self.get_server_ip(server, validation_resources),
+ self.ssh_user, password,
+ validation_resources['keypair']['private_key'],
+ server=server,
+ servers_client=self.client)
for i in person:
- self.assertEqual(base64.b64decode(i['contents']),
+ self.assertEqual(base64.decode_as_text(i['contents']),
linux_client.exec_command(
'sudo cat %s' % i['path']))
diff --git a/tempest/api/compute/servers/test_server_rescue.py b/tempest/api/compute/servers/test_server_rescue.py
index 2296980..b0ef3bc 100644
--- a/tempest/api/compute/servers/test_server_rescue.py
+++ b/tempest/api/compute/servers/test_server_rescue.py
@@ -13,11 +13,13 @@
# License for the specific language governing permissions and limitations
# under the License.
+import testtools
+
from tempest.api.compute import base
-from tempest.common.utils import data_utils
from tempest.common import waiters
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
CONF = config.CONF
@@ -40,85 +42,52 @@
def resource_setup(cls):
super(ServerRescueTestJSON, cls).resource_setup()
- # Floating IP creation
- body = cls.floating_ips_client.create_floating_ip()['floating_ip']
- cls.floating_ip_id = str(body['id']).strip()
- cls.floating_ip = str(body['ip']).strip()
+ password = data_utils.rand_password()
+ server = cls.create_test_server(adminPass=password,
+ wait_until='ACTIVE')
+ cls.servers_client.rescue_server(server['id'], adminPass=password)
+ waiters.wait_for_server_status(cls.servers_client, server['id'],
+ 'RESCUE')
+ cls.rescued_server_id = server['id']
- # Security group creation
- cls.sg_name = data_utils.rand_name('sg')
- cls.sg_desc = data_utils.rand_name('sg-desc')
- cls.sg = cls.security_groups_client.create_security_group(
- name=cls.sg_name, description=cls.sg_desc)['security_group']
- cls.sg_id = cls.sg['id']
-
- # Server for positive tests
- server = cls.create_test_server(wait_until='BUILD')
- cls.server_id = server['id']
- cls.password = server['adminPass']
- waiters.wait_for_server_status(cls.servers_client, cls.server_id,
- 'ACTIVE')
-
- def setUp(self):
- super(ServerRescueTestJSON, self).setUp()
-
- @classmethod
- def resource_cleanup(cls):
- # Deleting the floating IP which is created in this method
- cls.floating_ips_client.delete_floating_ip(cls.floating_ip_id)
- cls.sg = cls.security_groups_client.delete_security_group(
- cls.sg_id)
- super(ServerRescueTestJSON, cls).resource_cleanup()
-
- def tearDown(self):
- super(ServerRescueTestJSON, self).tearDown()
-
- def _unrescue(self, server_id):
- self.servers_client.unrescue_server(server_id)
- waiters.wait_for_server_status(self.servers_client, server_id,
- 'ACTIVE')
-
- @test.idempotent_id('fd032140-714c-42e4-a8fd-adcd8df06be6')
+ @decorators.idempotent_id('fd032140-714c-42e4-a8fd-adcd8df06be6')
def test_rescue_unrescue_instance(self):
- self.servers_client.rescue_server(
- self.server_id, adminPass=self.password)
- waiters.wait_for_server_status(self.servers_client, self.server_id,
+ password = data_utils.rand_password()
+ server = self.create_test_server(adminPass=password,
+ wait_until='ACTIVE')
+ self.servers_client.rescue_server(server['id'], adminPass=password)
+ waiters.wait_for_server_status(self.servers_client, server['id'],
'RESCUE')
- self.servers_client.unrescue_server(self.server_id)
- waiters.wait_for_server_status(self.servers_client, self.server_id,
+ self.servers_client.unrescue_server(server['id'])
+ waiters.wait_for_server_status(self.servers_client, server['id'],
'ACTIVE')
- @test.idempotent_id('4842e0cf-e87d-4d9d-b61f-f4791da3cacc')
+ @decorators.idempotent_id('4842e0cf-e87d-4d9d-b61f-f4791da3cacc')
+ @testtools.skipUnless(CONF.network.public_network_id,
+ 'The public_network_id option must be specified.')
+ @testtools.skipUnless(CONF.network_feature_enabled.floating_ips,
+ "Floating ips are not available")
def test_rescued_vm_associate_dissociate_floating_ip(self):
- # Rescue the server
- self.servers_client.rescue_server(
- self.server_id, adminPass=self.password)
- waiters.wait_for_server_status(self.servers_client, self.server_id,
- 'RESCUE')
- self.addCleanup(self._unrescue, self.server_id)
-
# Association of floating IP to a rescued vm
- client = self.floating_ips_client
- client.associate_floating_ip_to_server(self.floating_ip,
- self.server_id)
+ floating_ip_body = self.floating_ips_client.create_floating_ip(
+ pool=CONF.network.floating_network_name)['floating_ip']
+ self.addCleanup(self.floating_ips_client.delete_floating_ip,
+ floating_ip_body['id'])
+
+ self.floating_ips_client.associate_floating_ip_to_server(
+ str(floating_ip_body['ip']).strip(), self.rescued_server_id)
# Disassociation of floating IP that was associated in this method
- client.disassociate_floating_ip_from_server(self.floating_ip,
- self.server_id)
+ self.floating_ips_client.disassociate_floating_ip_from_server(
+ str(floating_ip_body['ip']).strip(), self.rescued_server_id)
- @test.idempotent_id('affca41f-7195-492d-8065-e09eee245404')
+ @decorators.idempotent_id('affca41f-7195-492d-8065-e09eee245404')
def test_rescued_vm_add_remove_security_group(self):
- # Rescue the server
- self.servers_client.rescue_server(
- self.server_id, adminPass=self.password)
- waiters.wait_for_server_status(self.servers_client, self.server_id,
- 'RESCUE')
- self.addCleanup(self._unrescue, self.server_id)
-
# Add Security group
- self.servers_client.add_security_group(self.server_id,
- name=self.sg_name)
+ sg = self.create_security_group()
+ self.servers_client.add_security_group(self.rescued_server_id,
+ name=sg['name'])
# Delete Security group
- self.servers_client.remove_security_group(self.server_id,
- name=self.sg_name)
+ self.servers_client.remove_security_group(self.rescued_server_id,
+ name=sg['name'])
diff --git a/tempest/api/compute/servers/test_server_rescue_negative.py b/tempest/api/compute/servers/test_server_rescue_negative.py
index 65ad2f5..1260c6b 100644
--- a/tempest/api/compute/servers/test_server_rescue_negative.py
+++ b/tempest/api/compute/servers/test_server_rescue_negative.py
@@ -13,14 +13,15 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
import testtools
from tempest.api.compute import base
-from tempest.common.utils import data_utils
+from tempest.common import utils
from tempest.common import waiters
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
CONF = config.CONF
@@ -43,14 +44,15 @@
def resource_setup(cls):
super(ServerRescueNegativeTestJSON, cls).resource_setup()
cls.device = CONF.compute.volume_device_name
-
+ cls.password = data_utils.rand_password()
+ rescue_password = data_utils.rand_password()
# Server for negative tests
- server = cls.create_test_server(wait_until='BUILD')
- resc_server = cls.create_test_server(wait_until='ACTIVE')
+ server = cls.create_test_server(adminPass=cls.password,
+ wait_until='BUILD')
+ resc_server = cls.create_test_server(adminPass=rescue_password,
+ wait_until='ACTIVE')
cls.server_id = server['id']
- cls.password = server['adminPass']
cls.rescue_id = resc_server['id']
- rescue_password = resc_server['adminPass']
cls.servers_client.rescue_server(
cls.rescue_id, adminPass=rescue_password)
@@ -59,20 +61,6 @@
waiters.wait_for_server_status(cls.servers_client,
cls.server_id, 'ACTIVE')
- def _create_volume(self):
- volume = self.volumes_extensions_client.create_volume(
- size=CONF.volume.volume_size, display_name=data_utils.rand_name(
- self.__class__.__name__ + '_volume'))['volume']
- self.addCleanup(self.delete_volume, volume['id'])
- waiters.wait_for_volume_status(self.volumes_extensions_client,
- volume['id'], 'available')
- return volume
-
- def _detach(self, server_id, volume_id):
- self.servers_client.detach_volume(server_id, volume_id)
- waiters.wait_for_volume_status(self.volumes_extensions_client,
- volume_id, 'available')
-
def _unrescue(self, server_id):
self.servers_client.unrescue_server(server_id)
waiters.wait_for_server_status(self.servers_client,
@@ -83,10 +71,10 @@
waiters.wait_for_server_status(self.servers_client,
server_id, 'ACTIVE')
- @test.idempotent_id('cc3a883f-43c0-4fb6-a9bb-5579d64984ed')
+ @decorators.idempotent_id('cc3a883f-43c0-4fb6-a9bb-5579d64984ed')
@testtools.skipUnless(CONF.compute_feature_enabled.pause,
'Pause is not available.')
- @test.attr(type=['negative'])
+ @decorators.attr(type=['negative'])
def test_rescue_paused_instance(self):
# Rescue a paused server
self.servers_client.pause_server(self.server_id)
@@ -97,14 +85,14 @@
self.servers_client.rescue_server,
self.server_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('db22b618-f157-4566-a317-1b6d467a8094')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('db22b618-f157-4566-a317-1b6d467a8094')
def test_rescued_vm_reboot(self):
self.assertRaises(lib_exc.Conflict, self.servers_client.reboot_server,
self.rescue_id, type='HARD')
- @test.attr(type=['negative'])
- @test.idempotent_id('6dfc0a55-3a77-4564-a144-1587b7971dde')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('6dfc0a55-3a77-4564-a144-1587b7971dde')
def test_rescue_non_existent_server(self):
# Rescue a non-existing server
non_existent_server = data_utils.rand_uuid()
@@ -112,19 +100,19 @@
self.servers_client.rescue_server,
non_existent_server)
- @test.attr(type=['negative'])
- @test.idempotent_id('70cdb8a1-89f8-437d-9448-8844fd82bf46')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('70cdb8a1-89f8-437d-9448-8844fd82bf46')
def test_rescued_vm_rebuild(self):
self.assertRaises(lib_exc.Conflict,
self.servers_client.rebuild_server,
self.rescue_id,
self.image_ref_alt)
- @test.idempotent_id('d0ccac79-0091-4cf4-a1ce-26162d0cc55f')
- @test.services('volume')
- @test.attr(type=['negative'])
+ @decorators.idempotent_id('d0ccac79-0091-4cf4-a1ce-26162d0cc55f')
+ @utils.services('volume')
+ @decorators.attr(type=['negative'])
def test_rescued_vm_attach_volume(self):
- volume = self._create_volume()
+ volume = self.create_volume()
# Rescue the server
self.servers_client.rescue_server(self.server_id,
@@ -140,18 +128,15 @@
volumeId=volume['id'],
device='/dev/%s' % self.device)
- @test.idempotent_id('f56e465b-fe10-48bf-b75d-646cda3a8bc9')
- @test.services('volume')
- @test.attr(type=['negative'])
+ @decorators.idempotent_id('f56e465b-fe10-48bf-b75d-646cda3a8bc9')
+ @utils.services('volume')
+ @decorators.attr(type=['negative'])
def test_rescued_vm_detach_volume(self):
- volume = self._create_volume()
+ volume = self.create_volume()
# Attach the volume to the server
- self.servers_client.attach_volume(self.server_id,
- volumeId=volume['id'],
- device='/dev/%s' % self.device)
- waiters.wait_for_volume_status(self.volumes_extensions_client,
- volume['id'], 'in-use')
+ server = self.servers_client.show_server(self.server_id)['server']
+ self.attach_volume(server, volume, device='/dev/%s' % self.device)
# Rescue the server
self.servers_client.rescue_server(self.server_id,
@@ -159,7 +144,6 @@
waiters.wait_for_server_status(self.servers_client,
self.server_id, 'RESCUE')
# addCleanup is a LIFO queue
- self.addCleanup(self._detach, self.server_id, volume['id'])
self.addCleanup(self._unrescue, self.server_id)
# Detach the volume from the server expecting failure
diff --git a/tempest/api/compute/servers/test_server_tags.py b/tempest/api/compute/servers/test_server_tags.py
new file mode 100644
index 0000000..8d0a4e3
--- /dev/null
+++ b/tempest/api/compute/servers/test_server_tags.py
@@ -0,0 +1,108 @@
+# Copyright 2017 AT&T 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.
+
+import six
+
+from tempest.api.compute import base
+from tempest.common import utils
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+
+
+class ServerTagsTestJSON(base.BaseV2ComputeTest):
+
+ min_microversion = '2.26'
+ max_microversion = 'latest'
+
+ @classmethod
+ def skip_checks(cls):
+ super(ServerTagsTestJSON, cls).skip_checks()
+ if not utils.is_extension_enabled('os-server-tags', 'compute'):
+ msg = "os-server-tags extension is not enabled."
+ raise cls.skipException(msg)
+
+ @classmethod
+ def setup_clients(cls):
+ super(ServerTagsTestJSON, cls).setup_clients()
+ cls.client = cls.servers_client
+
+ @classmethod
+ def resource_setup(cls):
+ super(ServerTagsTestJSON, cls).resource_setup()
+ cls.server = cls.create_test_server(wait_until='ACTIVE')
+
+ def _update_server_tags(self, server_id, tags):
+ if not isinstance(tags, (list, tuple)):
+ tags = [tags]
+ for tag in tags:
+ self.client.update_tag(server_id, tag)
+ self.addCleanup(self.client.delete_all_tags, server_id)
+
+ @decorators.idempotent_id('8d95abe2-c658-4c42-9a44-c0258500306b')
+ def test_create_delete_tag(self):
+ # Check that no tags exist.
+ fetched_tags = self.client.list_tags(self.server['id'])['tags']
+ self.assertEmpty(fetched_tags)
+
+ # Add server tag to the server.
+ assigned_tag = data_utils.rand_name('tag')
+ self._update_server_tags(self.server['id'], assigned_tag)
+
+ # Check that added tag exists.
+ fetched_tags = self.client.list_tags(self.server['id'])['tags']
+ self.assertEqual([assigned_tag], fetched_tags)
+
+ # Remove assigned tag from server and check that it was removed.
+ self.client.delete_tag(self.server['id'], assigned_tag)
+ fetched_tags = self.client.list_tags(self.server['id'])['tags']
+ self.assertEmpty(fetched_tags)
+
+ @decorators.idempotent_id('a2c1af8c-127d-417d-974b-8115f7e3d831')
+ def test_update_all_tags(self):
+ # Add server tags to the server.
+ tags = [data_utils.rand_name('tag'), data_utils.rand_name('tag')]
+ self._update_server_tags(self.server['id'], tags)
+
+ # Replace tags with new tags and check that they are present.
+ new_tags = [data_utils.rand_name('tag'), data_utils.rand_name('tag')]
+ replaced_tags = self.client.update_all_tags(
+ self.server['id'], new_tags)['tags']
+ six.assertCountEqual(self, new_tags, replaced_tags)
+
+ # List the tags and check that the tags were replaced.
+ fetched_tags = self.client.list_tags(self.server['id'])['tags']
+ six.assertCountEqual(self, new_tags, fetched_tags)
+
+ @decorators.idempotent_id('a63b2a74-e918-4b7c-bcab-10c855f3a57e')
+ def test_delete_all_tags(self):
+ # Add server tags to the server.
+ assigned_tags = [data_utils.rand_name('tag'),
+ data_utils.rand_name('tag')]
+ self._update_server_tags(self.server['id'], assigned_tags)
+
+ # Delete tags from the server and check that they were deleted.
+ self.client.delete_all_tags(self.server['id'])
+ fetched_tags = self.client.list_tags(self.server['id'])['tags']
+ self.assertEmpty(fetched_tags)
+
+ @decorators.idempotent_id('81279a66-61c3-4759-b830-a2dbe64cbe08')
+ def test_check_tag_existence(self):
+ # Add server tag to the server.
+ assigned_tag = data_utils.rand_name('tag')
+ self._update_server_tags(self.server['id'], assigned_tag)
+
+ # Check that added tag exists. Throws a 404 if not found, else a 204,
+ # which was already checked by the schema validation.
+ self.client.check_tag_existence(self.server['id'], assigned_tag)
diff --git a/tempest/api/compute/servers/test_servers.py b/tempest/api/compute/servers/test_servers.py
index 8e2fbf1..56d973e 100644
--- a/tempest/api/compute/servers/test_servers.py
+++ b/tempest/api/compute/servers/test_servers.py
@@ -13,10 +13,16 @@
# License for the specific language governing permissions and limitations
# under the License.
+import testtools
+
from tempest.api.compute import base
-from tempest.common.utils import data_utils
from tempest.common import waiters
-from tempest import test
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+
+CONF = config.CONF
class ServersTestJSON(base.BaseV2ComputeTest):
@@ -26,31 +32,46 @@
super(ServersTestJSON, cls).setup_clients()
cls.client = cls.servers_client
- def tearDown(self):
- self.clear_servers()
- super(ServersTestJSON, self).tearDown()
-
- @test.idempotent_id('b92d5ec7-b1dd-44a2-87e4-45e888c46ef0')
+ @decorators.idempotent_id('b92d5ec7-b1dd-44a2-87e4-45e888c46ef0')
+ @testtools.skipUnless(CONF.compute_feature_enabled.
+ enable_instance_password,
+ 'Instance password not available.')
def test_create_server_with_admin_password(self):
# If an admin password is provided on server creation, the server's
# root password should be set to that password.
server = self.create_test_server(adminPass='testpassword')
+ self.addCleanup(waiters.wait_for_server_termination,
+ self.servers_client, server['id'])
+ self.addCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ self.servers_client.delete_server, server['id'])
# Verify the password is set correctly in the response
self.assertEqual('testpassword', server['adminPass'])
- @test.idempotent_id('8fea6be7-065e-47cf-89b8-496e6f96c699')
+ @decorators.idempotent_id('8fea6be7-065e-47cf-89b8-496e6f96c699')
def test_create_with_existing_server_name(self):
# Creating a server with a name that already exists is allowed
# TODO(sdague): clear out try, we do cleanup one layer up
- server_name = data_utils.rand_name('server')
+ server_name = data_utils.rand_name(
+ self.__class__.__name__ + '-server')
server = self.create_test_server(name=server_name,
wait_until='ACTIVE')
id1 = server['id']
+ self.addCleanup(waiters.wait_for_server_termination,
+ self.servers_client, id1)
+ self.addCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ self.servers_client.delete_server, id1)
server = self.create_test_server(name=server_name,
wait_until='ACTIVE')
id2 = server['id']
+ self.addCleanup(waiters.wait_for_server_termination,
+ self.servers_client, id2)
+ self.addCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ self.servers_client.delete_server, id2)
self.assertNotEqual(id1, id2, "Did not create a new server")
server = self.client.show_server(id1)['server']
name1 = server['name']
@@ -58,7 +79,7 @@
name2 = server['name']
self.assertEqual(name1, name2)
- @test.idempotent_id('f9e15296-d7f9-4e62-b53f-a04e89160833')
+ @decorators.idempotent_id('f9e15296-d7f9-4e62-b53f-a04e89160833')
def test_create_specify_keypair(self):
# Specify a keypair while creating a server
@@ -67,12 +88,17 @@
self.addCleanup(self.keypairs_client.delete_keypair, key_name)
self.keypairs_client.list_keypairs()
server = self.create_test_server(key_name=key_name)
+ self.addCleanup(waiters.wait_for_server_termination,
+ self.servers_client, server['id'])
+ self.addCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ self.servers_client.delete_server, server['id'])
waiters.wait_for_server_status(self.client, server['id'], 'ACTIVE')
server = self.client.show_server(server['id'])['server']
self.assertEqual(key_name, server['key_name'])
def _update_server_name(self, server_id, status, prefix_name='server'):
- # The server name should be changed to the the provided value
+ # The server name should be changed to the provided value
new_name = data_utils.rand_name(prefix_name)
# Update the server with a new name
@@ -85,31 +111,37 @@
self.assertEqual(new_name, server['name'])
return server
- @test.idempotent_id('5e6ccff8-349d-4852-a8b3-055df7988dd2')
+ @decorators.idempotent_id('5e6ccff8-349d-4852-a8b3-055df7988dd2')
def test_update_server_name(self):
- # The server name should be changed to the the provided value
+ # The server name should be changed to the provided value
server = self.create_test_server(wait_until='ACTIVE')
+ self.addCleanup(waiters.wait_for_server_termination,
+ self.servers_client, server['id'])
+ self.addCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ self.servers_client.delete_server, server['id'])
# Update instance name with non-ASCII characters
prefix_name = u'\u00CD\u00F1st\u00E1\u00F1c\u00E9'
self._update_server_name(server['id'], 'ACTIVE', prefix_name)
- @test.idempotent_id('6ac19cb1-27a3-40ec-b350-810bdc04c08e')
- def test_update_server_name_in_stop_state(self):
- # The server name should be changed to the the provided value
- server = self.create_test_server(wait_until='ACTIVE')
+ # stop server and check server name update again
self.client.stop_server(server['id'])
waiters.wait_for_server_status(self.client, server['id'], 'SHUTOFF')
# Update instance name with non-ASCII characters
- prefix_name = u'\u00CD\u00F1st\u00E1\u00F1c\u00E9'
updated_server = self._update_server_name(server['id'],
'SHUTOFF',
prefix_name)
self.assertNotIn('progress', updated_server)
- @test.idempotent_id('89b90870-bc13-4b73-96af-f9d4f2b70077')
+ @decorators.idempotent_id('89b90870-bc13-4b73-96af-f9d4f2b70077')
def test_update_access_server_address(self):
# The server's access addresses should reflect the provided values
server = self.create_test_server(wait_until='ACTIVE')
+ self.addCleanup(waiters.wait_for_server_termination,
+ self.servers_client, server['id'])
+ self.addCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ self.servers_client.delete_server, server['id'])
# Update the IPv4 and IPv6 access addresses
self.client.update_server(server['id'],
@@ -122,10 +154,89 @@
self.assertEqual('1.1.1.1', server['accessIPv4'])
self.assertEqual('::babe:202:202', server['accessIPv6'])
- @test.idempotent_id('38fb1d02-c3c5-41de-91d3-9bc2025a75eb')
+ @decorators.idempotent_id('38fb1d02-c3c5-41de-91d3-9bc2025a75eb')
def test_create_server_with_ipv6_addr_only(self):
# Create a server without an IPv4 address(only IPv6 address).
server = self.create_test_server(accessIPv6='2001:2001::3')
+ self.addCleanup(waiters.wait_for_server_termination,
+ self.servers_client, server['id'])
+ self.addCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ self.servers_client.delete_server, server['id'])
waiters.wait_for_server_status(self.client, server['id'], 'ACTIVE')
server = self.client.show_server(server['id'])['server']
self.assertEqual('2001:2001::3', server['accessIPv6'])
+
+ @decorators.related_bug('1730756')
+ @decorators.idempotent_id('defbaca5-d611-49f5-ae21-56ee25d2db49')
+ def test_create_server_specify_multibyte_character_name(self):
+ # prefix character is:
+ # http://unicode.org/cldr/utility/character.jsp?a=20A1
+
+ # We use a string with 3 byte utf-8 character due to nova
+ # will return 400(Bad Request) if we attempt to send a name which has
+ # 4 byte utf-8 character.
+ utf8_name = data_utils.rand_name(b'\xe2\x82\xa1'.decode('utf-8'))
+ self.create_test_server(name=utf8_name, wait_until='ACTIVE')
+
+
+class ServerShowV247Test(base.BaseV2ComputeTest):
+ 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)
+
+
+class ServerShowV263Test(base.BaseV2ComputeTest):
+ min_microversion = '2.63'
+ max_microversion = 'latest'
+
+ @decorators.idempotent_id('71b8e3d5-11d2-494f-b917-b094a4afed3c')
+ def test_show_update_rebuild_list_server(self):
+ trusted_certs = ['test-cert-1', 'test-cert-2']
+ server = self.create_test_server(
+ trusted_image_certificates=trusted_certs,
+ wait_until='ACTIVE')
+
+ # Check show API response schema
+ self.servers_client.show_server(server['id'])['server']
+
+ # Check update API response schema
+ self.servers_client.update_server(server['id'])
+ waiters.wait_for_server_status(self.servers_client,
+ server['id'], 'ACTIVE')
+
+ # Check 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')
+
+ # Check list details API response schema
+ params = {'trusted_image_certificates': trusted_certs}
+ servers = self.servers_client.list_servers(
+ detail=True, **params)['servers']
+ self.assertNotEmpty(servers)
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..2434884
--- /dev/null
+++ b/tempest/api/compute/servers/test_servers_microversions.py
@@ -0,0 +1,66 @@
+# 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')
+
+
+class ServerShowV257Test(base.BaseV2ComputeTest):
+ min_microversion = '2.57'
+ max_microversion = 'latest'
+
+ @decorators.idempotent_id('803df848-080a-4261-8f11-b020cd9b6f60')
+ def test_rebuild_server(self):
+ server = self.create_test_server(wait_until='ACTIVE')
+ user_data = "ZWNobyAiaGVsbG8gd29ybGQi"
+ # Checking rebuild API response schema
+ self.servers_client.rebuild_server(server['id'], self.image_ref_alt,
+ user_data=user_data)
+ 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 ed8484e..e944c28 100644
--- a/tempest/api/compute/servers/test_servers_negative.py
+++ b/tempest/api/compute/servers/test_servers_negative.py
@@ -15,29 +15,29 @@
import sys
-from tempest_lib import exceptions as lib_exc
import testtools
from tempest.api.compute import base
-from tempest.common.utils import data_utils
+from tempest.common import compute
+from tempest.common import utils
from tempest.common import waiters
from tempest import config
-from tempest import test
+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 ServersNegativeTestJSON(base.BaseV2ComputeTest):
- credentials = ['primary', 'alt']
-
def setUp(self):
super(ServersNegativeTestJSON, self).setUp()
try:
waiters.wait_for_server_status(self.client, self.server_id,
'ACTIVE')
except Exception:
- self.__class__.server_id = self.rebuild_server(self.server_id)
+ self.__class__.server_id = self.recreate_server(self.server_id)
def tearDown(self):
self.server_check_teardown()
@@ -47,7 +47,6 @@
def setup_clients(cls):
super(ServersNegativeTestJSON, cls).setup_clients()
cls.client = cls.servers_client
- cls.alt_client = cls.os_alt.servers_client
@classmethod
def resource_setup(cls):
@@ -55,8 +54,13 @@
server = cls.create_test_server(wait_until='ACTIVE')
cls.server_id = server['id']
- @test.attr(type=['negative'])
- @test.idempotent_id('dbbfd247-c40c-449e-8f6c-d2aa7c7da7cf')
+ server = cls.create_test_server()
+ cls.client.delete_server(server['id'])
+ waiters.wait_for_server_termination(cls.client, server['id'])
+ cls.deleted_server_id = server['id']
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('dbbfd247-c40c-449e-8f6c-d2aa7c7da7cf')
def test_server_name_blank(self):
# Create a server with name parameter empty
@@ -64,8 +68,8 @@
self.create_test_server,
name='')
- @test.attr(type=['negative'])
- @test.idempotent_id('b8a7235e-5246-4a8f-a08e-b34877c6586f')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('b8a7235e-5246-4a8f-a08e-b34877c6586f')
@testtools.skipUnless(CONF.compute_feature_enabled.personality,
'Nova personality feature disabled')
def test_personality_file_contents_not_encoded(self):
@@ -79,8 +83,8 @@
self.create_test_server,
personality=person)
- @test.attr(type=['negative'])
- @test.idempotent_id('fcba1052-0a50-4cf3-b1ac-fae241edf02f')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('fcba1052-0a50-4cf3-b1ac-fae241edf02f')
def test_create_with_invalid_image(self):
# Create a server with an unknown image
@@ -88,8 +92,8 @@
self.create_test_server,
image_id=-1)
- @test.attr(type=['negative'])
- @test.idempotent_id('18f5227f-d155-4429-807c-ccb103887537')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('18f5227f-d155-4429-807c-ccb103887537')
def test_create_with_invalid_flavor(self):
# Create a server with an unknown flavor
@@ -97,8 +101,8 @@
self.create_test_server,
flavor=-1,)
- @test.attr(type=['negative'])
- @test.idempotent_id('7f70a4d1-608f-4794-9e56-cb182765972c')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('7f70a4d1-608f-4794-9e56-cb182765972c')
def test_invalid_access_ip_v4_address(self):
# An access IPv4 address must match a valid address pattern
@@ -106,8 +110,8 @@
self.assertRaises(lib_exc.BadRequest,
self.create_test_server, accessIPv4=IPv4)
- @test.attr(type=['negative'])
- @test.idempotent_id('5226dd80-1e9c-4d8a-b5f9-b26ca4763fd0')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('5226dd80-1e9c-4d8a-b5f9-b26ca4763fd0')
def test_invalid_ip_v6_address(self):
# An access IPv6 address must match a valid address pattern
@@ -116,10 +120,10 @@
self.assertRaises(lib_exc.BadRequest,
self.create_test_server, accessIPv6=IPv6)
- @test.idempotent_id('7ea45b3e-e770-46fa-bfcc-9daaf6d987c0')
+ @decorators.idempotent_id('7ea45b3e-e770-46fa-bfcc-9daaf6d987c0')
@testtools.skipUnless(CONF.compute_feature_enabled.resize,
'Resize not available.')
- @test.attr(type=['negative'])
+ @decorators.attr(type=['negative'])
def test_resize_nonexistent_server(self):
# Resize a non-existent server
nonexistent_server = data_utils.rand_uuid()
@@ -127,37 +131,37 @@
self.client.resize_server,
nonexistent_server, self.flavor_ref)
- @test.idempotent_id('ced1a1d7-2ab6-45c9-b90f-b27d87b30efd')
+ @decorators.idempotent_id('ced1a1d7-2ab6-45c9-b90f-b27d87b30efd')
@testtools.skipUnless(CONF.compute_feature_enabled.resize,
'Resize not available.')
- @test.attr(type=['negative'])
+ @decorators.attr(type=['negative'])
def test_resize_server_with_non_existent_flavor(self):
# Resize a server with non-existent flavor
nonexistent_flavor = data_utils.rand_uuid()
self.assertRaises(lib_exc.BadRequest, self.client.resize_server,
self.server_id, flavor_ref=nonexistent_flavor)
- @test.idempotent_id('45436a7d-a388-4a35-a9d8-3adc5d0d940b')
+ @decorators.idempotent_id('45436a7d-a388-4a35-a9d8-3adc5d0d940b')
@testtools.skipUnless(CONF.compute_feature_enabled.resize,
'Resize not available.')
- @test.attr(type=['negative'])
+ @decorators.attr(type=['negative'])
def test_resize_server_with_null_flavor(self):
# Resize a server with null flavor
self.assertRaises(lib_exc.BadRequest, self.client.resize_server,
self.server_id, flavor_ref="")
- @test.attr(type=['negative'])
- @test.idempotent_id('d4c023a0-9c55-4747-9dd5-413b820143c7')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('d4c023a0-9c55-4747-9dd5-413b820143c7')
def test_reboot_non_existent_server(self):
# Reboot a non existent server
nonexistent_server = data_utils.rand_uuid()
self.assertRaises(lib_exc.NotFound, self.client.reboot_server,
nonexistent_server, type='SOFT')
- @test.idempotent_id('d1417e7f-a509-41b5-a102-d5eed8613369')
+ @decorators.idempotent_id('d1417e7f-a509-41b5-a102-d5eed8613369')
@testtools.skipUnless(CONF.compute_feature_enabled.pause,
'Pause is not available.')
- @test.attr(type=['negative'])
+ @decorators.attr(type=['negative'])
def test_pause_paused_server(self):
# Pause a paused server.
self.client.pause_server(self.server_id)
@@ -167,59 +171,73 @@
self.server_id)
self.client.unpause_server(self.server_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('98fa0458-1485-440f-873b-fe7f0d714930')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('98fa0458-1485-440f-873b-fe7f0d714930')
def test_rebuild_deleted_server(self):
# Rebuild a deleted server
- server = self.create_test_server()
- self.client.delete_server(server['id'])
- waiters.wait_for_server_termination(self.client, server['id'])
-
self.assertRaises(lib_exc.NotFound,
self.client.rebuild_server,
- server['id'], self.image_ref_alt)
+ self.deleted_server_id, self.image_ref)
- @test.attr(type=['negative'])
- @test.idempotent_id('581a397d-5eab-486f-9cf9-1014bbd4c984')
+ @decorators.related_bug('1660878', status_code=409)
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('581a397d-5eab-486f-9cf9-1014bbd4c984')
def test_reboot_deleted_server(self):
# Reboot a deleted server
- server = self.create_test_server()
- self.client.delete_server(server['id'])
- waiters.wait_for_server_termination(self.client, server['id'])
-
self.assertRaises(lib_exc.NotFound, self.client.reboot_server,
- server['id'], type='SOFT')
+ self.deleted_server_id, type='SOFT')
- @test.attr(type=['negative'])
- @test.idempotent_id('d86141a7-906e-4731-b187-d64a2ea61422')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('d86141a7-906e-4731-b187-d64a2ea61422')
def test_rebuild_non_existent_server(self):
# Rebuild a non existent server
nonexistent_server = data_utils.rand_uuid()
self.assertRaises(lib_exc.NotFound,
self.client.rebuild_server,
nonexistent_server,
- self.image_ref_alt)
+ self.image_ref)
- @test.attr(type=['negative'])
- @test.idempotent_id('fd57f159-68d6-4c2a-902b-03070828a87e')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('fd57f159-68d6-4c2a-902b-03070828a87e')
def test_create_numeric_server_name(self):
server_name = 12345
self.assertRaises(lib_exc.BadRequest,
self.create_test_server,
name=server_name)
- @test.attr(type=['negative'])
- @test.idempotent_id('c3e0fb12-07fc-4d76-a22e-37409887afe8')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('c3e0fb12-07fc-4d76-a22e-37409887afe8')
def test_create_server_name_length_exceeds_256(self):
- # Create a server with name length exceeding 256 characters
+ # Create a server with name length exceeding 255 characters
server_name = 'a' * 256
self.assertRaises(lib_exc.BadRequest,
self.create_test_server,
name=server_name)
- @test.attr(type=['negative'])
- @test.idempotent_id('4e72dc2d-44c5-4336-9667-f7972e95c402')
+ @decorators.attr(type=['negative'])
+ @decorators.related_bug('1651064', status_code=500)
+ @utils.services('volume')
+ @decorators.idempotent_id('12146ac1-d7df-4928-ad25-b1f99e5286cd')
+ def test_create_server_invalid_bdm_in_2nd_dict(self):
+ volume = self.create_volume()
+ bdm_1st = {"source_type": "image",
+ "delete_on_termination": True,
+ "boot_index": 0,
+ "uuid": self.image_ref,
+ "destination_type": "local"}
+ bdm_2nd = {"source_type": "volume",
+ "uuid": volume["id"],
+ "destination_type": "invalid"}
+ bdm = [bdm_1st, bdm_2nd]
+
+ self.assertRaises(lib_exc.BadRequest,
+ self.create_test_server,
+ image_id=self.image_ref,
+ block_device_mapping_v2=bdm)
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('4e72dc2d-44c5-4336-9667-f7972e95c402')
def test_create_with_invalid_network_uuid(self):
# Pass invalid network uuid while creating a server
@@ -229,8 +247,8 @@
self.create_test_server,
networks=networks)
- @test.attr(type=['negative'])
- @test.idempotent_id('7a2efc39-530c-47de-b875-2dd01c8d39bd')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('7a2efc39-530c-47de-b875-2dd01c8d39bd')
def test_create_with_non_existent_keypair(self):
# Pass a non-existent keypair while creating a server
@@ -239,8 +257,8 @@
self.create_test_server,
key_name=key_name)
- @test.attr(type=['negative'])
- @test.idempotent_id('7fc74810-0bd2-4cd7-8244-4f33a9db865a')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('7fc74810-0bd2-4cd7-8244-4f33a9db865a')
def test_create_server_metadata_exceeds_length_limit(self):
# Pass really long metadata while creating a server
@@ -249,19 +267,20 @@
self.create_test_server,
metadata=metadata)
- @test.attr(type=['negative'])
- @test.idempotent_id('aa8eed43-e2cb-4ebf-930b-da14f6a21d81')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('aa8eed43-e2cb-4ebf-930b-da14f6a21d81')
def test_update_name_of_non_existent_server(self):
# Update name of a non-existent server
nonexistent_server = data_utils.rand_uuid()
- new_name = data_utils.rand_name('server') + '_updated'
+ new_name = data_utils.rand_name(
+ self.__class__.__name__ + '-server') + '_updated'
self.assertRaises(lib_exc.NotFound, self.client.update_server,
nonexistent_server, name=new_name)
- @test.attr(type=['negative'])
- @test.idempotent_id('38204696-17c6-44da-9590-40f87fb5a899')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('38204696-17c6-44da-9590-40f87fb5a899')
def test_update_server_set_empty_name(self):
# Update name of the server to an empty string
@@ -270,18 +289,8 @@
self.assertRaises(lib_exc.BadRequest, self.client.update_server,
self.server_id, name=new_name)
- @test.attr(type=['negative'])
- @test.idempotent_id('543d84c1-dd2e-4c6d-8cb2-b9da0efaa384')
- def test_update_server_of_another_tenant(self):
- # Update name of a server that belongs to another tenant
-
- new_name = self.server_id + '_new'
- self.assertRaises(lib_exc.NotFound,
- self.alt_client.update_server, self.server_id,
- name=new_name)
-
- @test.attr(type=['negative'])
- @test.idempotent_id('5c8e244c-dada-4590-9944-749c455b431f')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('5c8e244c-dada-4590-9944-749c455b431f')
def test_update_server_name_length_exceeds_256(self):
# Update name of server exceed the name length limit
@@ -291,8 +300,8 @@
self.server_id,
name=new_name)
- @test.attr(type=['negative'])
- @test.idempotent_id('1041b4e6-514b-4855-96a5-e974b60870a3')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('1041b4e6-514b-4855-96a5-e974b60870a3')
def test_delete_non_existent_server(self):
# Delete a non existent server
@@ -300,31 +309,23 @@
self.assertRaises(lib_exc.NotFound, self.client.delete_server,
nonexistent_server)
- @test.attr(type=['negative'])
- @test.idempotent_id('5c75009d-3eea-423e-bea3-61b09fd25f9c')
- def test_delete_a_server_of_another_tenant(self):
- # Delete a server that belongs to another tenant
- self.assertRaises(lib_exc.NotFound,
- self.alt_client.delete_server,
- self.server_id)
-
- @test.attr(type=['negative'])
- @test.idempotent_id('75f79124-277c-45e6-a373-a1d6803f4cc4')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('75f79124-277c-45e6-a373-a1d6803f4cc4')
def test_delete_server_pass_negative_id(self):
# Pass an invalid string parameter to delete server
self.assertRaises(lib_exc.NotFound, self.client.delete_server, -1)
- @test.attr(type=['negative'])
- @test.idempotent_id('f4d7279b-5fd2-4bf2-9ba4-ae35df0d18c5')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('f4d7279b-5fd2-4bf2-9ba4-ae35df0d18c5')
def test_delete_server_pass_id_exceeding_length_limit(self):
# Pass a server ID that exceeds length limit to delete server
self.assertRaises(lib_exc.NotFound, self.client.delete_server,
- sys.maxint + 1)
+ sys.maxsize + 1)
- @test.attr(type=['negative'])
- @test.idempotent_id('c5fa6041-80cd-483b-aa6d-4e45f19d093c')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('c5fa6041-80cd-483b-aa6d-4e45f19d093c')
def test_create_with_nonexistent_security_group(self):
# Create a server with a nonexistent security group
@@ -333,66 +334,66 @@
self.create_test_server,
security_groups=security_groups)
- @test.attr(type=['negative'])
- @test.idempotent_id('3436b02f-1b1e-4f03-881e-c6a602327439')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('3436b02f-1b1e-4f03-881e-c6a602327439')
def test_get_non_existent_server(self):
# Get a non existent server details
nonexistent_server = data_utils.rand_uuid()
self.assertRaises(lib_exc.NotFound, self.client.show_server,
nonexistent_server)
- @test.attr(type=['negative'])
- @test.idempotent_id('a31460a9-49e1-42aa-82ee-06e0bb7c2d03')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('a31460a9-49e1-42aa-82ee-06e0bb7c2d03')
def test_stop_non_existent_server(self):
# Stop a non existent server
nonexistent_server = data_utils.rand_uuid()
self.assertRaises(lib_exc.NotFound, self.servers_client.stop_server,
nonexistent_server)
- @test.idempotent_id('6a8dc0c6-6cd4-4c0a-9f32-413881828091')
+ @decorators.idempotent_id('6a8dc0c6-6cd4-4c0a-9f32-413881828091')
@testtools.skipUnless(CONF.compute_feature_enabled.pause,
'Pause is not available.')
- @test.attr(type=['negative'])
+ @decorators.attr(type=['negative'])
def test_pause_non_existent_server(self):
# pause a non existent server
nonexistent_server = data_utils.rand_uuid()
self.assertRaises(lib_exc.NotFound, self.client.pause_server,
nonexistent_server)
- @test.idempotent_id('705b8e3a-e8a7-477c-a19b-6868fc24ac75')
+ @decorators.idempotent_id('705b8e3a-e8a7-477c-a19b-6868fc24ac75')
@testtools.skipUnless(CONF.compute_feature_enabled.pause,
'Pause is not available.')
- @test.attr(type=['negative'])
+ @decorators.attr(type=['negative'])
def test_unpause_non_existent_server(self):
# unpause a non existent server
nonexistent_server = data_utils.rand_uuid()
self.assertRaises(lib_exc.NotFound, self.client.unpause_server,
nonexistent_server)
- @test.idempotent_id('c8e639a7-ece8-42dd-a2e0-49615917ba4f')
+ @decorators.idempotent_id('c8e639a7-ece8-42dd-a2e0-49615917ba4f')
@testtools.skipUnless(CONF.compute_feature_enabled.pause,
'Pause is not available.')
- @test.attr(type=['negative'])
+ @decorators.attr(type=['negative'])
def test_unpause_server_invalid_state(self):
# unpause an active server.
self.assertRaises(lib_exc.Conflict,
self.client.unpause_server,
self.server_id)
- @test.idempotent_id('d1f032d5-7b6e-48aa-b252-d5f16dd994ca')
+ @decorators.idempotent_id('d1f032d5-7b6e-48aa-b252-d5f16dd994ca')
@testtools.skipUnless(CONF.compute_feature_enabled.suspend,
'Suspend is not available.')
- @test.attr(type=['negative'])
+ @decorators.attr(type=['negative'])
def test_suspend_non_existent_server(self):
# suspend a non existent server
nonexistent_server = data_utils.rand_uuid()
self.assertRaises(lib_exc.NotFound, self.client.suspend_server,
nonexistent_server)
- @test.idempotent_id('7f323206-05a9-4bf8-996b-dd5b2036501b')
+ @decorators.idempotent_id('7f323206-05a9-4bf8-996b-dd5b2036501b')
@testtools.skipUnless(CONF.compute_feature_enabled.suspend,
'Suspend is not available.')
- @test.attr(type=['negative'])
+ @decorators.attr(type=['negative'])
def test_suspend_server_invalid_state(self):
# suspend a suspended server.
self.client.suspend_server(self.server_id)
@@ -403,28 +404,28 @@
self.server_id)
self.client.resume_server(self.server_id)
- @test.idempotent_id('221cd282-bddb-4837-a683-89c2487389b6')
+ @decorators.idempotent_id('221cd282-bddb-4837-a683-89c2487389b6')
@testtools.skipUnless(CONF.compute_feature_enabled.suspend,
'Suspend is not available.')
- @test.attr(type=['negative'])
+ @decorators.attr(type=['negative'])
def test_resume_non_existent_server(self):
# resume a non existent server
nonexistent_server = data_utils.rand_uuid()
self.assertRaises(lib_exc.NotFound, self.client.resume_server,
nonexistent_server)
- @test.idempotent_id('ccb6294d-c4c9-498f-8a43-554c098bfadb')
+ @decorators.idempotent_id('ccb6294d-c4c9-498f-8a43-554c098bfadb')
@testtools.skipUnless(CONF.compute_feature_enabled.suspend,
'Suspend is not available.')
- @test.attr(type=['negative'])
+ @decorators.attr(type=['negative'])
def test_resume_server_invalid_state(self):
# resume an active server.
self.assertRaises(lib_exc.Conflict,
self.client.resume_server,
self.server_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('7dd919e7-413f-4198-bebb-35e2a01b13e9')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('7dd919e7-413f-4198-bebb-35e2a01b13e9')
def test_get_console_output_of_non_existent_server(self):
# get the console output for a non existent server
nonexistent_server = data_utils.rand_uuid()
@@ -432,8 +433,8 @@
self.client.get_console_output,
nonexistent_server, length=10)
- @test.attr(type=['negative'])
- @test.idempotent_id('6f47992b-5144-4250-9f8b-f00aa33950f3')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('6f47992b-5144-4250-9f8b-f00aa33950f3')
def test_force_delete_nonexistent_server_id(self):
# force-delete a non existent server
nonexistent_server = data_utils.rand_uuid()
@@ -441,8 +442,8 @@
self.client.force_delete_server,
nonexistent_server)
- @test.attr(type=['negative'])
- @test.idempotent_id('9c6d38cc-fcfb-437a-85b9-7b788af8bf01')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('9c6d38cc-fcfb-437a-85b9-7b788af8bf01')
def test_restore_nonexistent_server_id(self):
# restore-delete a non existent server
nonexistent_server = data_utils.rand_uuid()
@@ -450,47 +451,42 @@
self.client.restore_soft_deleted_server,
nonexistent_server)
- @test.attr(type=['negative'])
- @test.idempotent_id('7fcadfab-bd6a-4753-8db7-4a51e51aade9')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('7fcadfab-bd6a-4753-8db7-4a51e51aade9')
def test_restore_server_invalid_state(self):
# we can only restore-delete a server in 'soft-delete' state
self.assertRaises(lib_exc.Conflict,
self.client.restore_soft_deleted_server,
self.server_id)
- @test.idempotent_id('abca56e2-a892-48ea-b5e5-e07e69774816')
+ @decorators.idempotent_id('abca56e2-a892-48ea-b5e5-e07e69774816')
@testtools.skipUnless(CONF.compute_feature_enabled.shelve,
'Shelve is not available.')
- @test.attr(type=['negative'])
+ @decorators.attr(type=['negative'])
def test_shelve_non_existent_server(self):
# shelve a non existent server
nonexistent_server = data_utils.rand_uuid()
self.assertRaises(lib_exc.NotFound, self.client.shelve_server,
nonexistent_server)
- @test.idempotent_id('443e4f9b-e6bf-4389-b601-3a710f15fddd')
+ @decorators.idempotent_id('443e4f9b-e6bf-4389-b601-3a710f15fddd')
@testtools.skipUnless(CONF.compute_feature_enabled.shelve,
'Shelve is not available.')
- @test.attr(type=['negative'])
+ @decorators.attr(type=['negative'])
def test_shelve_shelved_server(self):
# shelve a shelved server.
- self.client.shelve_server(self.server_id)
+ compute.shelve_server(self.client, self.server_id)
- offload_time = CONF.compute.shelved_offload_time
- if offload_time >= 0:
- waiters.wait_for_server_status(self.client,
- self.server_id,
- 'SHELVED_OFFLOADED',
- extra_timeout=offload_time)
- else:
- waiters.wait_for_server_status(self.client,
- self.server_id,
- 'SHELVED')
+ def _unshelve_server():
+ server_info = self.client.show_server(self.server_id)['server']
+ if 'SHELVED' in server_info['status']:
+ self.client.unshelve_server(self.server_id)
+ self.addCleanup(_unshelve_server)
server = self.client.show_server(self.server_id)['server']
image_name = server['name'] + '-shelved'
params = {'name': image_name}
- images = self.images_client.list_images(**params)['images']
+ images = self.compute_images_client.list_images(**params)['images']
self.assertEqual(1, len(images))
self.assertEqual(image_name, images[0]['name'])
@@ -500,22 +496,94 @@
self.client.unshelve_server(self.server_id)
- @test.idempotent_id('23d23b37-afaf-40d7-aa5d-5726f82d8821')
+ @decorators.idempotent_id('23d23b37-afaf-40d7-aa5d-5726f82d8821')
@testtools.skipUnless(CONF.compute_feature_enabled.shelve,
'Shelve is not available.')
- @test.attr(type=['negative'])
+ @decorators.attr(type=['negative'])
def test_unshelve_non_existent_server(self):
# unshelve a non existent server
nonexistent_server = data_utils.rand_uuid()
self.assertRaises(lib_exc.NotFound, self.client.unshelve_server,
nonexistent_server)
- @test.idempotent_id('8f198ded-1cca-4228-9e65-c6b449c54880')
+ @decorators.idempotent_id('8f198ded-1cca-4228-9e65-c6b449c54880')
@testtools.skipUnless(CONF.compute_feature_enabled.shelve,
'Shelve is not available.')
- @test.attr(type=['negative'])
+ @decorators.attr(type=['negative'])
def test_unshelve_server_invalid_state(self):
# unshelve an active server.
self.assertRaises(lib_exc.Conflict,
self.client.unshelve_server,
self.server_id)
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('74085be3-a370-4ca2-bc51-2d0e10e0f573')
+ @utils.services('volume', 'image')
+ def test_create_server_from_non_bootable_volume(self):
+ # Create a volume
+ volume = self.create_volume()
+
+ # Update volume bootable status to false
+ self.volumes_client.set_bootable_volume(volume['id'],
+ bootable=False)
+
+ # Verify bootable flag was updated
+ nonbootable_vol = self.volumes_client.show_volume(
+ volume['id'])['volume']
+ self.assertEqual('false', nonbootable_vol['bootable'])
+
+ # Block device mapping
+ bd_map = [{'boot_index': '0',
+ 'uuid': volume['id'],
+ 'source_type': 'volume',
+ 'destination_type': 'volume',
+ 'delete_on_termination': False}]
+
+ # Try creating a server from non-bootable volume
+ self.assertRaises(lib_exc.BadRequest,
+ self.create_test_server,
+ image_id='',
+ wait_until='ACTIVE',
+ block_device_mapping_v2=bd_map)
+
+
+class ServersNegativeTestMultiTenantJSON(base.BaseV2ComputeTest):
+
+ credentials = ['primary', 'alt']
+
+ def setUp(self):
+ super(ServersNegativeTestMultiTenantJSON, self).setUp()
+ try:
+ waiters.wait_for_server_status(self.servers_client, self.server_id,
+ 'ACTIVE')
+ except Exception:
+ self.__class__.server_id = self.recreate_server(self.server_id)
+
+ @classmethod
+ def setup_clients(cls):
+ super(ServersNegativeTestMultiTenantJSON, cls).setup_clients()
+ cls.alt_client = cls.os_alt.servers_client
+
+ @classmethod
+ def resource_setup(cls):
+ super(ServersNegativeTestMultiTenantJSON, cls).resource_setup()
+ server = cls.create_test_server(wait_until='ACTIVE')
+ cls.server_id = server['id']
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('543d84c1-dd2e-4c6d-8cb2-b9da0efaa384')
+ def test_update_server_of_another_tenant(self):
+ # Update name of a server that belongs to another tenant
+
+ new_name = self.server_id + '_new'
+ self.assertRaises(lib_exc.NotFound,
+ self.alt_client.update_server, self.server_id,
+ name=new_name)
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('5c75009d-3eea-423e-bea3-61b09fd25f9c')
+ def test_delete_a_server_of_another_tenant(self):
+ # Delete a server that belongs to another tenant
+ self.assertRaises(lib_exc.NotFound,
+ self.alt_client.delete_server,
+ self.server_id)
diff --git a/tempest/api/compute/servers/test_virtual_interfaces.py b/tempest/api/compute/servers/test_virtual_interfaces.py
index 7aa6d34..5fb1711 100644
--- a/tempest/api/compute/servers/test_virtual_interfaces.py
+++ b/tempest/api/compute/servers/test_virtual_interfaces.py
@@ -14,17 +14,23 @@
# under the License.
import netaddr
-from tempest_lib import decorators
+import testtools
from tempest.api.compute import base
+from tempest.common import utils
from tempest import config
-from tempest import test
+from tempest.lib import decorators
+from tempest.lib import exceptions
CONF = config.CONF
+# TODO(mriedem): Remove this test class once the nova queens branch goes into
+# extended maintenance mode.
class VirtualInterfacesTestJSON(base.BaseV2ComputeTest):
+ depends_on_nova_network = True
+
@classmethod
def setup_credentials(cls):
# This test needs a network and a subnet
@@ -39,23 +45,25 @@
@classmethod
def resource_setup(cls):
super(VirtualInterfacesTestJSON, cls).resource_setup()
- server = cls.create_test_server(wait_until='ACTIVE')
- cls.server_id = server['id']
+ cls.server = cls.create_test_server(wait_until='ACTIVE')
- @decorators.skip_because(bug="1183436",
- condition=CONF.service_available.neutron)
- @test.idempotent_id('96c4e2ef-5e4d-4d7f-87f5-fed6dca18016')
- @test.services('network')
+ @decorators.idempotent_id('96c4e2ef-5e4d-4d7f-87f5-fed6dca18016')
+ @utils.services('network')
def test_list_virtual_interfaces(self):
# Positive test:Should be able to GET the virtual interfaces list
# for a given server_id
- output = self.client.list_virtual_interfaces(self.server_id)
- self.assertIsNotNone(output)
- virt_ifaces = output
- self.assertNotEqual(0, len(virt_ifaces['virtual_interfaces']),
- 'Expected virtual interfaces, got 0 interfaces.')
- for virt_iface in virt_ifaces['virtual_interfaces']:
- mac_address = virt_iface['mac_address']
- self.assertTrue(netaddr.valid_mac(mac_address),
- "Invalid mac address detected. mac address: %s"
- % mac_address)
+
+ if CONF.service_available.neutron:
+ with testtools.ExpectedException(exceptions.BadRequest):
+ self.client.list_virtual_interfaces(self.server['id'])
+ else:
+ output = self.client.list_virtual_interfaces(self.server['id'])
+ virt_ifaces = output['virtual_interfaces']
+ self.assertNotEmpty(virt_ifaces,
+ 'Expected virtual interfaces, got 0 '
+ 'interfaces.')
+ for virt_iface in virt_ifaces:
+ mac_address = virt_iface['mac_address']
+ self.assertTrue(netaddr.valid_mac(mac_address),
+ "Invalid mac address detected. mac address: %s"
+ % mac_address)
diff --git a/tempest/api/compute/servers/test_virtual_interfaces_negative.py b/tempest/api/compute/servers/test_virtual_interfaces_negative.py
index 577a673..ec4d7a8 100644
--- a/tempest/api/compute/servers/test_virtual_interfaces_negative.py
+++ b/tempest/api/compute/servers/test_virtual_interfaces_negative.py
@@ -13,34 +13,32 @@
# License for the specific language governing permissions and limitations
# under the License.
-import uuid
-
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.compute import base
-from tempest import test
+from tempest.common import utils
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+# TODO(mriedem): Remove this test class once the nova queens branch goes into
+# extended maintenance mode.
class VirtualInterfacesNegativeTestJSON(base.BaseV2ComputeTest):
+ depends_on_nova_network = True
+
@classmethod
def setup_credentials(cls):
# For this test no network resources are needed
cls.set_network_resources()
super(VirtualInterfacesNegativeTestJSON, cls).setup_credentials()
- @classmethod
- def setup_clients(cls):
- super(VirtualInterfacesNegativeTestJSON, cls).setup_clients()
- cls.client = cls.servers_client
-
- @test.attr(type=['negative'])
- @test.idempotent_id('64ebd03c-1089-4306-93fa-60f5eb5c803c')
- @test.services('network')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('64ebd03c-1089-4306-93fa-60f5eb5c803c')
+ @utils.services('network')
def test_list_virtual_interfaces_invalid_server_id(self):
# Negative test: Should not be able to GET virtual interfaces
# for an invalid server_id
- invalid_server_id = str(uuid.uuid4())
+ invalid_server_id = data_utils.rand_uuid()
self.assertRaises(lib_exc.NotFound,
- self.client.list_virtual_interfaces,
+ self.servers_client.list_virtual_interfaces,
invalid_server_id)
diff --git a/tempest/api/compute/test_authorization.py b/tempest/api/compute/test_authorization.py
deleted file mode 100644
index e363fc4..0000000
--- a/tempest/api/compute/test_authorization.py
+++ /dev/null
@@ -1,447 +0,0 @@
-# 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.
-
-import six
-
-from oslo_log import log as logging
-from tempest_lib import exceptions as lib_exc
-
-from tempest.api.compute import base
-from tempest.common.utils import data_utils
-from tempest import config
-from tempest import test
-
-CONF = config.CONF
-
-LOG = logging.getLogger(__name__)
-
-
-class AuthorizationTestJSON(base.BaseV2ComputeTest):
-
- credentials = ['primary', 'alt']
-
- @classmethod
- def skip_checks(cls):
- super(AuthorizationTestJSON, cls).skip_checks()
- if not CONF.service_available.glance:
- raise cls.skipException('Glance is not available.')
-
- @classmethod
- def setup_credentials(cls):
- # No network resources required for this test
- cls.set_network_resources()
- super(AuthorizationTestJSON, cls).setup_credentials()
-
- @classmethod
- def setup_clients(cls):
- super(AuthorizationTestJSON, cls).setup_clients()
- cls.client = cls.os.servers_client
- cls.images_client = cls.os.images_client
- cls.glance_client = cls.os.image_client
- cls.keypairs_client = cls.os.keypairs_client
- cls.security_client = cls.os.compute_security_groups_client
- cls.rule_client = cls.os.security_group_rules_client
-
- cls.alt_client = cls.alt_manager.servers_client
- cls.alt_images_client = cls.alt_manager.images_client
- cls.alt_keypairs_client = cls.alt_manager.keypairs_client
- cls.alt_security_client = (
- cls.alt_manager.compute_security_groups_client)
- cls.alt_rule_client = cls.alt_manager.security_group_rules_client
-
- @classmethod
- def resource_setup(cls):
- super(AuthorizationTestJSON, cls).resource_setup()
- server = cls.create_test_server(wait_until='ACTIVE')
- cls.server = cls.client.show_server(server['id'])['server']
-
- name = data_utils.rand_name('image')
- body = cls.glance_client.create_image(name=name,
- container_format='bare',
- disk_format='raw',
- is_public=False)['image']
- image_id = body['id']
- image_file = six.StringIO(('*' * 1024))
- body = cls.glance_client.update_image(image_id,
- data=image_file)['image']
- cls.glance_client.wait_for_image_status(image_id, 'active')
- cls.image = cls.images_client.show_image(image_id)['image']
-
- cls.keypairname = data_utils.rand_name('keypair')
- cls.keypairs_client.create_keypair(name=cls.keypairname)
-
- name = data_utils.rand_name('security')
- description = data_utils.rand_name('description')
- cls.security_group = cls.security_client.create_security_group(
- name=name, description=description)['security_group']
-
- parent_group_id = cls.security_group['id']
- ip_protocol = 'tcp'
- from_port = 22
- to_port = 22
- cls.rule = cls.rule_client.create_security_group_rule(
- parent_group_id=parent_group_id, ip_protocol=ip_protocol,
- from_port=from_port, to_port=to_port)['security_group_rule']
-
- @classmethod
- def resource_cleanup(cls):
- if hasattr(cls, 'image'):
- cls.images_client.delete_image(cls.image['id'])
- if hasattr(cls, 'keypairname'):
- cls.keypairs_client.delete_keypair(cls.keypairname)
- if hasattr(cls, 'security_group'):
- cls.security_client.delete_security_group(cls.security_group['id'])
- super(AuthorizationTestJSON, cls).resource_cleanup()
-
- @test.idempotent_id('56816e4a-bd34-47b5-aee9-268c3efeb5d4')
- def test_get_server_for_alt_account_fails(self):
- # A GET request for a server on another user's account should fail
- self.assertRaises(lib_exc.NotFound, self.alt_client.show_server,
- self.server['id'])
-
- @test.idempotent_id('fb8a4870-6d9d-44ad-8375-95d52e98d9f6')
- def test_delete_server_for_alt_account_fails(self):
- # A DELETE request for another user's server should fail
- self.assertRaises(lib_exc.NotFound, self.alt_client.delete_server,
- self.server['id'])
-
- @test.idempotent_id('d792f91f-1d49-4eb5-b1ff-b229c4b9dc64')
- def test_update_server_for_alt_account_fails(self):
- # An update server request for another user's server should fail
- self.assertRaises(lib_exc.NotFound, self.alt_client.update_server,
- self.server['id'], name='test')
-
- @test.idempotent_id('488f24df-d7f7-4207-949a-f17fcb8e8769')
- def test_list_server_addresses_for_alt_account_fails(self):
- # A list addresses request for another user's server should fail
- self.assertRaises(lib_exc.NotFound, self.alt_client.list_addresses,
- self.server['id'])
-
- @test.idempotent_id('00b442d0-2e72-40e7-9b1f-31772e36da01')
- def test_list_server_addresses_by_network_for_alt_account_fails(self):
- # A list address/network request for another user's server should fail
- server_id = self.server['id']
- self.assertRaises(lib_exc.NotFound,
- self.alt_client.list_addresses_by_network, server_id,
- 'public')
-
- @test.idempotent_id('cc90b35a-19f0-45d2-b680-2aabf934aa22')
- def test_list_servers_with_alternate_tenant(self):
- # A list on servers from one tenant should not
- # show on alternate tenant
- # Listing servers from alternate tenant
- alt_server_ids = []
- body = self.alt_client.list_servers()
- alt_server_ids = [s['id'] for s in body['servers']]
- self.assertNotIn(self.server['id'], alt_server_ids)
-
- @test.idempotent_id('376dbc16-0779-4384-a723-752774799641')
- def test_change_password_for_alt_account_fails(self):
- # A change password request for another user's server should fail
- self.assertRaises(lib_exc.NotFound, self.alt_client.change_password,
- self.server['id'], adminPass='newpass')
-
- @test.idempotent_id('14cb5ff5-f646-45ca-8f51-09081d6c0c24')
- def test_reboot_server_for_alt_account_fails(self):
- # A reboot request for another user's server should fail
- self.assertRaises(lib_exc.NotFound, self.alt_client.reboot_server,
- self.server['id'], type='HARD')
-
- @test.idempotent_id('8a0bce51-cd00-480b-88ba-dbc7d8408a37')
- def test_rebuild_server_for_alt_account_fails(self):
- # A rebuild request for another user's server should fail
- self.assertRaises(lib_exc.NotFound, self.alt_client.rebuild_server,
- self.server['id'], self.image_ref_alt)
-
- @test.idempotent_id('e4da647e-f982-4e61-9dad-1d1abebfb933')
- def test_resize_server_for_alt_account_fails(self):
- # A resize request for another user's server should fail
- self.assertRaises(lib_exc.NotFound, self.alt_client.resize_server,
- self.server['id'], self.flavor_ref_alt)
-
- @test.idempotent_id('a9fe8112-0ffa-4902-b061-f892bd5fe0d3')
- def test_create_image_for_alt_account_fails(self):
- # A create image request for another user's server should fail
- self.assertRaises(lib_exc.NotFound,
- self.alt_images_client.create_image,
- self.server['id'], name='testImage')
-
- @test.idempotent_id('95d445f6-babc-4f2e-aea3-aa24ec5e7f0d')
- def test_create_server_with_unauthorized_image(self):
- # Server creation with another user's image should fail
- self.assertRaises(lib_exc.BadRequest, self.alt_client.create_server,
- name='test', imageRef=self.image['id'],
- flavorRef=self.flavor_ref)
-
- @test.idempotent_id('acf8724b-142b-4044-82c3-78d31a533f24')
- def test_create_server_fails_when_tenant_incorrect(self):
- # BUG(sdague): this test should fail because of bad auth url,
- # which means that when we run with a service catalog without
- # project_id in the urls, it should fail to fail, and thus
- # fail the test. It does not.
- #
- # The 400 BadRequest is clearly ambiguous, and something else
- # is wrong about this request. This should be fixed.
- #
- # A create server request should fail if the tenant id does not match
- # the current user
- # Change the base URL to impersonate another user
- self.alt_client.auth_provider.set_alt_auth_data(
- request_part='url',
- auth_data=self.client.auth_provider.auth_data
- )
- self.assertRaises(lib_exc.BadRequest,
- self.alt_client.create_server, name='test',
- imageRef=self.image['id'], flavorRef=self.flavor_ref)
-
- @test.idempotent_id('f03d1ded-7fd4-4d29-bc13-e2391f29c625')
- def test_create_keypair_in_analt_user_tenant(self):
- """create keypair should not function for alternate tenant
-
- POST {alt_service_url}/os-keypairs
-
- Attempt to create a keypair against an alternate tenant by
- changing using a different tenant's service url. This should
- return a BadRequest. This tests basic tenant isolation protections.
-
- NOTE(sdague): if the environment does not use project_id in
- the service urls, this test is not valid. Skip under these
- conditions.
-
- """
- if self.alt_keypairs_client.base_url == self.keypairs_client.base_url:
- raise self.skipException("Service urls don't include project_id")
-
- k_name = data_utils.rand_name('keypair')
- try:
- # Change the base URL to impersonate another user
- self.alt_keypairs_client.auth_provider.set_alt_auth_data(
- request_part='url',
- auth_data=self.keypairs_client.auth_provider.auth_data
- )
- resp = {}
- resp['status'] = None
- self.assertRaises(lib_exc.BadRequest,
- self.alt_keypairs_client.create_keypair,
- name=k_name)
- finally:
- # Next request the base_url is back to normal
- if (resp['status'] is not None):
- self.alt_keypairs_client.delete_keypair(k_name)
- LOG.error("Create keypair request should not happen "
- "if the tenant id does not match the current user")
-
- @test.idempotent_id('85bcdd8f-56b4-4868-ae56-63fbf6f7e405')
- def test_get_keypair_of_alt_account_fails(self):
- # A GET request for another user's keypair should fail
- self.assertRaises(lib_exc.NotFound,
- self.alt_keypairs_client.show_keypair,
- self.keypairname)
-
- @test.idempotent_id('6d841683-a8e0-43da-a1b8-b339f7692b61')
- def test_delete_keypair_of_alt_account_fails(self):
- # A DELETE request for another user's keypair should fail
- self.assertRaises(lib_exc.NotFound,
- self.alt_keypairs_client.delete_keypair,
- self.keypairname)
-
- @test.idempotent_id('fcb2e144-36e3-4dfb-9f9f-e72fcdec5656')
- def test_get_image_for_alt_account_fails(self):
- # A GET request for an image on another user's account should fail
- self.assertRaises(lib_exc.NotFound,
- self.alt_images_client.show_image, self.image['id'])
-
- @test.idempotent_id('9facb962-f043-4a9d-b9ee-166a32dea098')
- def test_delete_image_for_alt_account_fails(self):
- # A DELETE request for another user's image should fail
- self.assertRaises(lib_exc.NotFound,
- self.alt_images_client.delete_image,
- self.image['id'])
-
- @test.idempotent_id('752c917e-83be-499d-a422-3559127f7d3c')
- def test_create_security_group_in_analt_user_tenant(self):
- """create security group should not function for alternate tenant
-
- POST {alt_service_url}/os-security-groups
-
- Attempt to create a security group against an alternate tenant
- by changing using a different tenant's service url. This
- should return a BadRequest. This tests basic tenant isolation
- protections.
-
- NOTE(sdague): if the environment does not use project_id in
- the service urls, this test is not valid. Skip under these
- conditions.
-
- """
- if self.alt_security_client.base_url == self.security_client.base_url:
- raise self.skipException("Service urls don't include project_id")
-
- s_name = data_utils.rand_name('security')
- s_description = data_utils.rand_name('security')
- try:
- # Change the base URL to impersonate another user
- self.alt_security_client.auth_provider.set_alt_auth_data(
- request_part='url',
- auth_data=self.security_client.auth_provider.auth_data
- )
- resp = {}
- resp['status'] = None
- self.assertRaises(lib_exc.BadRequest,
- self.alt_security_client.create_security_group,
- name=s_name, description=s_description)
- finally:
- # Next request the base_url is back to normal
- if resp['status'] is not None:
- self.alt_security_client.delete_security_group(resp['id'])
- LOG.error("Create Security Group request should not happen if"
- "the tenant id does not match the current user")
-
- @test.idempotent_id('9db3590f-4d15-4e5f-985e-b28514919a6f')
- def test_get_security_group_of_alt_account_fails(self):
- # A GET request for another user's security group should fail
- self.assertRaises(lib_exc.NotFound,
- self.alt_security_client.show_security_group,
- self.security_group['id'])
-
- @test.idempotent_id('155387a5-2bbc-4acf-ab06-698dae537ea5')
- def test_delete_security_group_of_alt_account_fails(self):
- # A DELETE request for another user's security group should fail
- self.assertRaises(lib_exc.NotFound,
- self.alt_security_client.delete_security_group,
- self.security_group['id'])
-
- @test.idempotent_id('b2b76de0-210a-4089-b921-591c9ec552f6')
- def test_create_security_group_rule_in_analt_user_tenant(self):
- """create security group rule should not function for alternate tenant
-
- POST {alt_service_url}/os-security-group-rules
-
- Attempt to create a security group rule against an alternate
- tenant by changing using a different tenant's service
- url. This should return a BadRequest. This tests basic tenant
- isolation protections.
-
- NOTE(sdague): if the environment does not use project_id in
- the service urls, this test is not valid. Skip under these
- conditions.
-
- """
- if self.alt_security_client.base_url == self.security_client.base_url:
- raise self.skipException("Service urls don't include project_id")
-
- parent_group_id = self.security_group['id']
- ip_protocol = 'icmp'
- from_port = -1
- to_port = -1
- try:
- # Change the base URL to impersonate another user
- self.alt_rule_client.auth_provider.set_alt_auth_data(
- request_part='url',
- auth_data=self.rule_client.auth_provider.auth_data
- )
- resp = {}
- resp['status'] = None
- self.assertRaises(lib_exc.BadRequest,
- self.alt_rule_client.
- create_security_group_rule,
- parent_group_id=parent_group_id,
- ip_protocol=ip_protocol,
- from_port=from_port, to_port=to_port)
- finally:
- # Next request the base_url is back to normal
- if resp['status'] is not None:
- self.alt_rule_client.delete_security_group_rule(resp['id'])
- LOG.error("Create security group rule request should not "
- "happen if the tenant id does not match the"
- " current user")
-
- @test.idempotent_id('c6044177-37ef-4ce4-b12c-270ddf26d7da')
- def test_delete_security_group_rule_of_alt_account_fails(self):
- # A DELETE request for another user's security group rule
- # should fail
- self.assertRaises(lib_exc.NotFound,
- self.alt_rule_client.delete_security_group_rule,
- self.rule['id'])
-
- @test.idempotent_id('c5f52351-53d9-4fc9-83e5-917f7f5e3d71')
- def test_set_metadata_of_alt_account_server_fails(self):
- # A set metadata for another user's server should fail
- req_metadata = {'meta1': 'data1', 'meta2': 'data2'}
- self.assertRaises(lib_exc.NotFound,
- self.alt_client.set_server_metadata,
- self.server['id'],
- req_metadata)
-
- @test.idempotent_id('fb6f51e9-df15-4939-898d-1aca38c258f0')
- def test_set_metadata_of_alt_account_image_fails(self):
- # A set metadata for another user's image should fail
- req_metadata = {'meta1': 'value1', 'meta2': 'value2'}
- self.assertRaises(lib_exc.NotFound,
- self.alt_images_client.set_image_metadata,
- self.image['id'], req_metadata)
-
- @test.idempotent_id('dea1936a-473d-49f2-92ad-97bb7aded22e')
- def test_get_metadata_of_alt_account_server_fails(self):
- # A get metadata for another user's server should fail
- req_metadata = {'meta1': 'data1'}
- self.client.set_server_metadata(self.server['id'], req_metadata)
- self.addCleanup(self.client.delete_server_metadata_item,
- self.server['id'], 'meta1')
- self.assertRaises(lib_exc.NotFound,
- self.alt_client.show_server_metadata_item,
- self.server['id'], 'meta1')
-
- @test.idempotent_id('16b2d724-0d3b-4216-a9fa-97bd4d9cf670')
- def test_get_metadata_of_alt_account_image_fails(self):
- # A get metadata for another user's image should fail
- req_metadata = {'meta1': 'value1'}
- self.addCleanup(self.images_client.delete_image_metadata_item,
- self.image['id'], 'meta1')
- self.images_client.set_image_metadata(self.image['id'],
- req_metadata)
- self.assertRaises(lib_exc.NotFound,
- self.alt_images_client.show_image_metadata_item,
- self.image['id'], 'meta1')
-
- @test.idempotent_id('79531e2e-e721-493c-8b30-a35db36fdaa6')
- def test_delete_metadata_of_alt_account_server_fails(self):
- # A delete metadata for another user's server should fail
- req_metadata = {'meta1': 'data1'}
- self.addCleanup(self.client.delete_server_metadata_item,
- self.server['id'], 'meta1')
- self.client.set_server_metadata(self.server['id'], req_metadata)
- self.assertRaises(lib_exc.NotFound,
- self.alt_client.delete_server_metadata_item,
- self.server['id'], 'meta1')
-
- @test.idempotent_id('a5175dcf-cef8-43d6-9b77-3cb707d62e94')
- def test_delete_metadata_of_alt_account_image_fails(self):
- # A delete metadata for another user's image should fail
- req_metadata = {'meta1': 'data1'}
- self.addCleanup(self.images_client.delete_image_metadata_item,
- self.image['id'], 'meta1')
- self.images_client.set_image_metadata(self.image['id'],
- req_metadata)
- self.assertRaises(lib_exc.NotFound,
- self.alt_images_client.delete_image_metadata_item,
- self.image['id'], 'meta1')
-
- @test.idempotent_id('b0c1e7a0-8853-40fd-8384-01f93d116cae')
- def test_get_console_output_of_alt_account_server_fails(self):
- # A Get Console Output for another user's server should fail
- self.assertRaises(lib_exc.NotFound,
- self.alt_client.get_console_output,
- self.server['id'], length=10)
diff --git a/tempest/api/compute/test_extensions.py b/tempest/api/compute/test_extensions.py
index 6e57aff..34faf5f 100644
--- a/tempest/api/compute/test_extensions.py
+++ b/tempest/api/compute/test_extensions.py
@@ -16,8 +16,9 @@
from oslo_log import log as logging
from tempest.api.compute import base
+from tempest.common import utils
from tempest import config
-from tempest import test
+from tempest.lib import decorators
CONF = config.CONF
@@ -25,27 +26,29 @@
LOG = logging.getLogger(__name__)
-class ExtensionsTestJSON(base.BaseV2ComputeTest):
+class ExtensionsTest(base.BaseV2ComputeTest):
- @test.idempotent_id('3bb27738-b759-4e0d-a5fa-37d7a6df07d1')
+ @decorators.idempotent_id('3bb27738-b759-4e0d-a5fa-37d7a6df07d1')
def test_list_extensions(self):
# List of all extensions
- if len(CONF.compute_feature_enabled.api_extensions) == 0:
+ if not CONF.compute_feature_enabled.api_extensions:
raise self.skipException('There are not any extensions configured')
extensions = self.extensions_client.list_extensions()['extensions']
ext = CONF.compute_feature_enabled.api_extensions[0]
+
+ # Log extensions list
+ extension_list = map(lambda x: x['alias'], extensions)
+ LOG.debug("Nova extensions: %s", ','.join(extension_list))
+
if ext == 'all':
self.assertIn('Hosts', map(lambda x: x['name'], extensions))
elif ext:
- self.assertIn(ext, map(lambda x: x['alias'], extensions))
+ self.assertIn(ext, extension_list)
else:
raise self.skipException('There are not any extensions configured')
- # Log extensions list
- extension_list = map(lambda x: x['alias'], extensions)
- LOG.debug("Nova extensions: %s" % ','.join(extension_list))
- @test.idempotent_id('05762f39-bdfa-4cdb-9b46-b78f8e78e2fd')
- @test.requires_ext(extension='os-consoles', service='compute')
+ @decorators.idempotent_id('05762f39-bdfa-4cdb-9b46-b78f8e78e2fd')
+ @utils.requires_ext(extension='os-consoles', service='compute')
def test_get_extension(self):
# get the specified extensions
extension = self.extensions_client.show_extension('os-consoles')
diff --git a/tempest/api/compute/test_live_block_migration_negative.py b/tempest/api/compute/test_live_block_migration_negative.py
deleted file mode 100644
index 2cd85f2..0000000
--- a/tempest/api/compute/test_live_block_migration_negative.py
+++ /dev/null
@@ -1,60 +0,0 @@
-# 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 tempest_lib import exceptions as lib_exc
-
-from tempest.api.compute import base
-from tempest.common.utils import data_utils
-from tempest.common import waiters
-from tempest import config
-from tempest import test
-
-CONF = config.CONF
-
-
-class LiveBlockMigrationNegativeTestJSON(base.BaseV2ComputeAdminTest):
- _host_key = 'OS-EXT-SRV-ATTR:host'
-
- @classmethod
- def skip_checks(cls):
- super(LiveBlockMigrationNegativeTestJSON, cls).skip_checks()
- if not CONF.compute_feature_enabled.live_migration:
- raise cls.skipException("Live migration is not enabled")
-
- @classmethod
- def setup_clients(cls):
- super(LiveBlockMigrationNegativeTestJSON, cls).setup_clients()
- cls.admin_hosts_client = cls.os_adm.hosts_client
- cls.admin_servers_client = cls.os_adm.servers_client
-
- def _migrate_server_to(self, server_id, dest_host):
- bmflm = CONF.compute_feature_enabled.block_migration_for_live_migration
- body = self.admin_servers_client.live_migrate_server(
- server_id, host=dest_host, block_migration=bmflm,
- disk_over_commit=False)
- return body
-
- @test.attr(type=['negative'])
- @test.idempotent_id('7fb7856e-ae92-44c9-861a-af62d7830bcb')
- def test_invalid_host_for_migration(self):
- # Migrating to an invalid host should not change the status
- target_host = data_utils.rand_name('host')
- server = self.create_test_server(wait_until="ACTIVE")
- server_id = server['id']
-
- self.assertRaises(lib_exc.BadRequest, self._migrate_server_to,
- server_id, target_host)
- waiters.wait_for_server_status(self.servers_client, server_id,
- 'ACTIVE')
diff --git a/tempest/api/compute/test_networks.py b/tempest/api/compute/test_networks.py
index d4b8003..b8c79d7 100644
--- a/tempest/api/compute/test_networks.py
+++ b/tempest/api/compute/test_networks.py
@@ -14,7 +14,7 @@
from tempest.api.compute import base
from tempest import config
-from tempest import test
+from tempest.lib import decorators
CONF = config.CONF
@@ -29,9 +29,9 @@
@classmethod
def setup_clients(cls):
super(ComputeNetworksTest, cls).setup_clients()
- cls.client = cls.os.compute_networks_client
+ cls.client = cls.os_primary.compute_networks_client
- @test.idempotent_id('3fe07175-312e-49a5-a623-5f52eeada4c2')
+ @decorators.idempotent_id('3fe07175-312e-49a5-a623-5f52eeada4c2')
def test_list_networks(self):
networks = self.client.list_networks()['networks']
self.assertNotEmpty(networks, "No networks found.")
diff --git a/tempest/api/compute/test_quotas.py b/tempest/api/compute/test_quotas.py
index 43f4c97..a62492d 100644
--- a/tempest/api/compute/test_quotas.py
+++ b/tempest/api/compute/test_quotas.py
@@ -15,7 +15,8 @@
from tempest.api.compute import base
from tempest.common import tempest_fixtures as fixtures
-from tempest import test
+from tempest.common import utils
+from tempest.lib import decorators
class QuotasTestJSON(base.BaseV2ComputeTest):
@@ -23,7 +24,7 @@
@classmethod
def skip_checks(cls):
super(QuotasTestJSON, cls).skip_checks()
- if not test.is_extension_enabled('os-quota-sets', 'compute'):
+ if not utils.is_extension_enabled('os-quota-sets', 'compute'):
msg = "quotas extension not enabled."
raise cls.skipException(msg)
@@ -42,15 +43,21 @@
super(QuotasTestJSON, cls).resource_setup()
cls.tenant_id = cls.client.tenant_id
cls.user_id = cls.client.user_id
- cls.default_quota_set = set(('injected_file_content_bytes',
- 'metadata_items', 'injected_files',
- 'ram', 'floating_ips',
- 'fixed_ips', 'key_pairs',
- 'injected_file_path_bytes',
- 'instances', 'security_group_rules',
- 'cores', 'security_groups'))
+ cls.default_quota_set = set(('metadata_items', 'ram', 'key_pairs',
+ 'instances', 'cores',
+ 'server_group_members', 'server_groups'))
+ if cls.is_requested_microversion_compatible('2.35'):
+ cls.default_quota_set = \
+ cls.default_quota_set | set(['fixed_ips', 'floating_ips',
+ 'security_group_rules',
+ 'security_groups'])
+ if cls.is_requested_microversion_compatible('2.56'):
+ cls.default_quota_set = \
+ cls.default_quota_set | set(['injected_file_content_bytes',
+ 'injected_file_path_bytes',
+ 'injected_files'])
- @test.idempotent_id('f1ef0a97-dbbb-4cca-adc5-c9fbc4f76107')
+ @decorators.idempotent_id('f1ef0a97-dbbb-4cca-adc5-c9fbc4f76107')
def test_get_quotas(self):
# User can get the quota set for it's tenant
expected_quota_set = self.default_quota_set | set(['id'])
@@ -60,13 +67,13 @@
self.assertIn(quota, quota_set.keys())
# get the quota set using user id
- quota_set = self.client.show_quota_set(self.tenant_id,
- self.user_id)['quota_set']
+ quota_set = self.client.show_quota_set(
+ self.tenant_id, user_id=self.user_id)['quota_set']
self.assertEqual(quota_set['id'], self.tenant_id)
for quota in expected_quota_set:
self.assertIn(quota, quota_set.keys())
- @test.idempotent_id('9bfecac7-b966-4f47-913f-1a9e2c12134a')
+ @decorators.idempotent_id('9bfecac7-b966-4f47-913f-1a9e2c12134a')
def test_get_default_quotas(self):
# User can get the default quota set for it's tenant
expected_quota_set = self.default_quota_set | set(['id'])
@@ -76,11 +83,11 @@
for quota in expected_quota_set:
self.assertIn(quota, quota_set.keys())
- @test.idempotent_id('cd65d997-f7e4-4966-a7e9-d5001b674fdc')
+ @decorators.idempotent_id('cd65d997-f7e4-4966-a7e9-d5001b674fdc')
def test_compare_tenant_quotas_with_default_quotas(self):
# Tenants are created with the default quota values
- defualt_quota_set = \
+ default_quota_set = \
self.client.show_default_quota_set(self.tenant_id)['quota_set']
tenant_quota_set = (self.client.show_quota_set(self.tenant_id)
['quota_set'])
- self.assertEqual(defualt_quota_set, tenant_quota_set)
+ self.assertEqual(default_quota_set, tenant_quota_set)
diff --git a/tempest/api/compute/test_tenant_networks.py b/tempest/api/compute/test_tenant_networks.py
index 96b7ef6..f4eada0 100644
--- a/tempest/api/compute/test_tenant_networks.py
+++ b/tempest/api/compute/test_tenant_networks.py
@@ -13,15 +13,17 @@
# under the License.
from tempest.api.compute import base
-from tempest import test
+from tempest.common import utils
+from tempest.lib import decorators
class ComputeTenantNetworksTest(base.BaseV2ComputeTest):
+ max_microversion = '2.35'
@classmethod
def resource_setup(cls):
super(ComputeTenantNetworksTest, cls).resource_setup()
- cls.client = cls.os.tenant_networks_client
+ cls.client = cls.os_primary.tenant_networks_client
cls.network = cls.get_tenant_network()
@classmethod
@@ -29,8 +31,8 @@
cls.set_network_resources(network=True)
super(ComputeTenantNetworksTest, cls).setup_credentials()
- @test.idempotent_id('edfea98e-bbe3-4c7a-9739-87b986baff26')
- @test.services('network')
+ @decorators.idempotent_id('edfea98e-bbe3-4c7a-9739-87b986baff26')
+ @utils.services('network')
def test_list_show_tenant_networks(self):
# Fetch all networks that are visible to the tenant: this may include
# shared and external networks
diff --git a/tempest/api/compute/test_versions.py b/tempest/api/compute/test_versions.py
index 8b84a21..6141553 100644
--- a/tempest/api/compute/test_versions.py
+++ b/tempest/api/compute/test_versions.py
@@ -13,12 +13,13 @@
# under the License.
from tempest.api.compute import base
-from tempest import test
+from tempest.lib import decorators
class TestVersions(base.BaseV2ComputeTest):
- @test.idempotent_id('6c0a0990-43b6-4529-9b61-5fd8daf7c55c')
+ @decorators.idempotent_id('6c0a0990-43b6-4529-9b61-5fd8daf7c55c')
+ @decorators.attr(type='smoke')
def test_list_api_versions(self):
"""Test that a get of the unversioned url returns the choices doc.
@@ -36,7 +37,8 @@
self.assertEqual(versions[0]['id'], 'v2.0',
"The first listed version should be v2.0")
- @test.idempotent_id('b953a29e-929c-4a8e-81be-ec3a7e03cb76')
+ @decorators.idempotent_id('b953a29e-929c-4a8e-81be-ec3a7e03cb76')
+ @decorators.attr(type='smoke')
def test_get_version_details(self):
"""Test individual version endpoints info works.
diff --git a/tempest/api/compute/volumes/test_attach_volume.py b/tempest/api/compute/volumes/test_attach_volume.py
index b4837f7..caa445d 100644
--- a/tempest/api/compute/volumes/test_attach_volume.py
+++ b/tempest/api/compute/volumes/test_attach_volume.py
@@ -16,23 +16,22 @@
import testtools
from tempest.api.compute import base
+from tempest.common import compute
+from tempest.common import utils
from tempest.common.utils.linux import remote_client
from tempest.common import waiters
from tempest import config
-from tempest import test
+from tempest.lib import decorators
CONF = config.CONF
-class AttachVolumeTestJSON(base.BaseV2ComputeTest):
-
- def __init__(self, *args, **kwargs):
- super(AttachVolumeTestJSON, self).__init__(*args, **kwargs)
- self.attachment = None
+class BaseAttachVolumeTest(base.BaseV2ComputeTest):
+ """Base class for the attach volume tests in this module."""
@classmethod
def skip_checks(cls):
- super(AttachVolumeTestJSON, cls).skip_checks()
+ super(BaseAttachVolumeTest, cls).skip_checks()
if not CONF.service_available.cinder:
skip_msg = ("%s skipped as Cinder is not available" % cls.__name__)
raise cls.skipException(skip_msg)
@@ -40,113 +39,402 @@
@classmethod
def setup_credentials(cls):
cls.prepare_instance_network()
- super(AttachVolumeTestJSON, cls).setup_credentials()
+ super(BaseAttachVolumeTest, cls).setup_credentials()
@classmethod
def resource_setup(cls):
- cls.set_validation_resources()
-
- super(AttachVolumeTestJSON, cls).resource_setup()
+ super(BaseAttachVolumeTest, cls).resource_setup()
cls.device = CONF.compute.volume_device_name
- def _detach(self, server_id, volume_id):
- if self.attachment:
- self.servers_client.detach_volume(server_id, volume_id)
- self.volumes_client.wait_for_volume_status(volume_id, 'available')
-
- def _delete_volume(self):
- # Delete the created Volumes
- if self.volume:
- self.volumes_client.delete_volume(self.volume['id'])
- self.volumes_client.wait_for_resource_deletion(self.volume['id'])
- self.volume = None
-
- def _create_and_attach(self):
+ def _create_server(self):
# Start a server and wait for it to become ready
- admin_pass = self.image_ssh_password
- self.server = self.create_test_server(
+ validation_resources = self.get_test_validation_resources(
+ self.os_primary)
+ server = self.create_test_server(
validatable=True,
+ validation_resources=validation_resources,
wait_until='ACTIVE',
- adminPass=admin_pass)
-
+ adminPass=self.image_ssh_password)
+ self.addCleanup(self.delete_server, server['id'])
# Record addresses so that we can ssh later
- self.server['addresses'] = self.servers_client.list_addresses(
- self.server['id'])['addresses']
+ server['addresses'] = self.servers_client.list_addresses(
+ server['id'])['addresses']
+ return server, validation_resources
- # Create a volume and wait for it to become ready
- self.volume = self.volumes_client.create_volume(
- CONF.volume.volume_size, display_name='test')['volume']
- self.addCleanup(self._delete_volume)
- self.volumes_client.wait_for_volume_status(self.volume['id'],
- 'available')
- # Attach the volume to the server
- self.attachment = self.servers_client.attach_volume(
- self.server['id'],
- volumeId=self.volume['id'],
- device='/dev/%s' % self.device)['volumeAttachment']
- self.volumes_client.wait_for_volume_status(self.volume['id'], 'in-use')
+class AttachVolumeTestJSON(BaseAttachVolumeTest):
- self.addCleanup(self._detach, self.server['id'], self.volume['id'])
-
- @test.idempotent_id('52e9045a-e90d-4c0d-9087-79d657faffff')
- @testtools.skipUnless(CONF.validation.run_validation,
- 'SSH required for this test')
+ @decorators.idempotent_id('52e9045a-e90d-4c0d-9087-79d657faffff')
def test_attach_detach_volume(self):
# Stop and Start a server with an attached volume, ensuring that
# the volume remains attached.
- self._create_and_attach()
+ server, validation_resources = self._create_server()
- self.servers_client.stop_server(self.server['id'])
- waiters.wait_for_server_status(self.servers_client, self.server['id'],
+ # NOTE(andreaf) Create one remote client used throughout the test.
+ if CONF.validation.run_validation:
+ linux_client = remote_client.RemoteClient(
+ self.get_server_ip(server, validation_resources),
+ self.image_ssh_user,
+ self.image_ssh_password,
+ validation_resources['keypair']['private_key'],
+ server=server,
+ servers_client=self.servers_client)
+ # NOTE(andreaf) We need to ensure the ssh key has been
+ # injected in the guest before we power cycle
+ linux_client.validate_authentication()
+
+ volume = self.create_volume()
+ attachment = self.attach_volume(server, volume,
+ device=('/dev/%s' % self.device))
+
+ self.servers_client.stop_server(server['id'])
+ waiters.wait_for_server_status(self.servers_client, server['id'],
'SHUTOFF')
- self.servers_client.start_server(self.server['id'])
- waiters.wait_for_server_status(self.servers_client, self.server['id'],
+ self.servers_client.start_server(server['id'])
+ waiters.wait_for_server_status(self.servers_client, server['id'],
'ACTIVE')
- linux_client = remote_client.RemoteClient(
- self.get_server_ip(self.server),
- self.image_ssh_user,
- self.server['adminPass'],
- self.validation_resources['keypair']['private_key'])
+ if CONF.validation.run_validation:
+ disks = linux_client.get_disks()
+ device_name_to_match = '\n' + self.device + ' '
+ self.assertIn(device_name_to_match, disks)
- partitions = linux_client.get_partitions()
- self.assertIn(self.device, partitions)
+ self.servers_client.detach_volume(server['id'], attachment['volumeId'])
+ waiters.wait_for_volume_resource_status(
+ self.volumes_client, attachment['volumeId'], 'available')
- self._detach(self.server['id'], self.volume['id'])
- self.attachment = None
- self.servers_client.stop_server(self.server['id'])
- waiters.wait_for_server_status(self.servers_client, self.server['id'],
+ self.servers_client.stop_server(server['id'])
+ waiters.wait_for_server_status(self.servers_client, server['id'],
'SHUTOFF')
- self.servers_client.start_server(self.server['id'])
- waiters.wait_for_server_status(self.servers_client, self.server['id'],
+ self.servers_client.start_server(server['id'])
+ waiters.wait_for_server_status(self.servers_client, server['id'],
'ACTIVE')
- linux_client = remote_client.RemoteClient(
- self.get_server_ip(self.server),
- self.image_ssh_user,
- self.server['adminPass'],
- self.validation_resources['keypair']['private_key'])
+ if CONF.validation.run_validation:
+ disks = linux_client.get_disks()
+ self.assertNotIn(device_name_to_match, disks)
- partitions = linux_client.get_partitions()
- self.assertNotIn(self.device, partitions)
-
- @test.idempotent_id('7fa563fe-f0f7-43eb-9e22-a1ece036b513')
+ @decorators.idempotent_id('7fa563fe-f0f7-43eb-9e22-a1ece036b513')
def test_list_get_volume_attachments(self):
- # Create Server, Volume and attach that Volume to Server
- self._create_and_attach()
- # List Volume attachment of the server
+ # List volume attachment of the server
+ server, _ = self._create_server()
+ volume_1st = self.create_volume()
+ attachment_1st = self.attach_volume(server, volume_1st,
+ device=('/dev/%s' % self.device))
body = self.servers_client.list_volume_attachments(
- self.server['id'])['volumeAttachments']
+ server['id'])['volumeAttachments']
self.assertEqual(1, len(body))
- self.assertIn(self.attachment, body)
+ self.assertIn(attachment_1st, body)
- # Get Volume attachment of the server
+ # Get volume attachment of the server
body = self.servers_client.show_volume_attachment(
- self.server['id'],
- self.attachment['id'])['volumeAttachment']
- self.assertEqual(self.server['id'], body['serverId'])
- self.assertEqual(self.volume['id'], body['volumeId'])
- self.assertEqual(self.attachment['id'], body['id'])
+ server['id'],
+ attachment_1st['id'])['volumeAttachment']
+ self.assertEqual(server['id'], body['serverId'])
+ self.assertEqual(volume_1st['id'], body['volumeId'])
+ self.assertEqual(attachment_1st['id'], body['id'])
+
+ # attach one more volume to server
+ volume_2nd = self.create_volume()
+ attachment_2nd = self.attach_volume(server, volume_2nd)
+ body = self.servers_client.list_volume_attachments(
+ server['id'])['volumeAttachments']
+ self.assertEqual(2, len(body))
+
+ for attachment in [attachment_1st, attachment_2nd]:
+ body = self.servers_client.show_volume_attachment(
+ server['id'], attachment['id'])['volumeAttachment']
+ self.assertEqual(server['id'], body['serverId'])
+ self.assertEqual(attachment['volumeId'], body['volumeId'])
+ self.assertEqual(attachment['id'], body['id'])
+ self.servers_client.detach_volume(server['id'],
+ attachment['volumeId'])
+ waiters.wait_for_volume_resource_status(
+ self.volumes_client, attachment['volumeId'], 'available')
+
+
+class AttachVolumeShelveTestJSON(BaseAttachVolumeTest):
+ """Testing volume with shelved instance.
+
+ This test checks the attaching and detaching volumes from
+ a shelved or shelved offload instance.
+ """
+
+ min_microversion = '2.20'
+ max_microversion = 'latest'
+
+ @classmethod
+ def skip_checks(cls):
+ super(AttachVolumeShelveTestJSON, cls).skip_checks()
+ if not CONF.compute_feature_enabled.shelve:
+ raise cls.skipException('Shelve is not available.')
+
+ def _count_volumes(self, server, validation_resources):
+ # Count number of volumes on an instance
+ volumes = 0
+ if CONF.validation.run_validation:
+ linux_client = remote_client.RemoteClient(
+ self.get_server_ip(server, validation_resources),
+ self.image_ssh_user,
+ self.image_ssh_password,
+ validation_resources['keypair']['private_key'],
+ server=server,
+ servers_client=self.servers_client)
+
+ command = 'grep -c -E [vs]d.$ /proc/partitions'
+ volumes = int(linux_client.exec_command(command).strip())
+ return volumes
+
+ def _shelve_server(self, server, validation_resources):
+ # NOTE(andreaf) If we are going to shelve a server, we should
+ # check first whether the server is ssh-able. Otherwise we
+ # won't be able to distinguish failures introduced by shelve
+ # from pre-existing ones. Also it's good to wait for cloud-init
+ # to be done and sshd server to be running before shelving to
+ # avoid breaking the VM
+ if CONF.validation.run_validation:
+ linux_client = remote_client.RemoteClient(
+ self.get_server_ip(server, validation_resources),
+ self.image_ssh_user,
+ self.image_ssh_password,
+ validation_resources['keypair']['private_key'],
+ server=server,
+ servers_client=self.servers_client)
+ linux_client.validate_authentication()
+
+ # If validation went ok, or it was skipped, shelve the server
+ compute.shelve_server(self.servers_client, server['id'])
+
+ def _unshelve_server_and_check_volumes(self, server,
+ validation_resources,
+ number_of_volumes):
+ # Unshelve the instance and check that there are expected volumes
+ self.servers_client.unshelve_server(server['id'])
+ waiters.wait_for_server_status(self.servers_client,
+ server['id'],
+ 'ACTIVE')
+ if CONF.validation.run_validation:
+ counted_volumes = self._count_volumes(
+ server, validation_resources)
+ self.assertEqual(number_of_volumes, counted_volumes)
+
+ @decorators.idempotent_id('13a940b6-3474-4c3c-b03f-29b89112bfee')
+ def test_attach_volume_shelved_or_offload_server(self):
+ # Create server, count number of volumes on it, shelve
+ # server and attach pre-created volume to shelved server
+ server, validation_resources = self._create_server()
+ volume = self.create_volume()
+ num_vol = self._count_volumes(server, validation_resources)
+ self._shelve_server(server, validation_resources)
+ attachment = self.attach_volume(server, volume,
+ device=('/dev/%s' % self.device))
+
+ # Unshelve the instance and check that attached volume exists
+ self._unshelve_server_and_check_volumes(
+ server, validation_resources, num_vol + 1)
+
+ # Get volume attachment of the server
+ volume_attachment = self.servers_client.show_volume_attachment(
+ server['id'],
+ attachment['id'])['volumeAttachment']
+ self.assertEqual(server['id'], volume_attachment['serverId'])
+ self.assertEqual(attachment['id'], volume_attachment['id'])
+ # Check the mountpoint is not None after unshelve server even in
+ # case of shelved_offloaded.
+ self.assertIsNotNone(volume_attachment['device'])
+
+ @decorators.idempotent_id('b54e86dd-a070-49c4-9c07-59ae6dae15aa')
+ def test_detach_volume_shelved_or_offload_server(self):
+ # Count number of volumes on instance, shelve
+ # server and attach pre-created volume to shelved server
+ server, validation_resources = self._create_server()
+ volume = self.create_volume()
+ num_vol = self._count_volumes(server, validation_resources)
+ self._shelve_server(server, validation_resources)
+
+ # Attach and then detach the volume
+ self.attach_volume(server, volume, device=('/dev/%s' % self.device))
+ self.servers_client.detach_volume(server['id'], volume['id'])
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume['id'], 'available')
+
+ # Unshelve the instance and check that we have the expected number of
+ # volume(s)
+ self._unshelve_server_and_check_volumes(
+ server, validation_resources, num_vol)
+
+
+class AttachVolumeMultiAttachTest(BaseAttachVolumeTest):
+ min_microversion = '2.60'
+ max_microversion = 'latest'
+
+ @classmethod
+ def skip_checks(cls):
+ super(AttachVolumeMultiAttachTest, cls).skip_checks()
+ if not CONF.compute_feature_enabled.volume_multiattach:
+ raise cls.skipException('Volume multi-attach is not available.')
+
+ def _attach_volume_to_servers(self, volume, servers):
+ """Attaches the given volume to the list of servers.
+
+ :param volume: The multiattach volume to use.
+ :param servers: list of server instances on which the volume will be
+ attached
+ :returns: dict of server ID to volumeAttachment dict entries
+ """
+ attachments = {}
+ for server in servers:
+ # map the server id to the volume attachment
+ attachments[server['id']] = self.attach_volume(server, volume)
+ # NOTE(mriedem): In the case of multi-attach, after the first
+ # attach the volume will be in-use. On the second attach, nova will
+ # 'reserve' the volume which puts it back into 'attaching' status
+ # and then the volume shouldn't go back to in-use until the compute
+ # actually attaches the server to the volume.
+ return attachments
+
+ def _detach_multiattach_volume(self, volume_id, server_id):
+ """Detaches a multiattach volume from the given server.
+
+ Depending on the number of attachments the volume has, this method
+ will wait for the volume to go to back to 'in-use' status if there are
+ more attachments or 'available' state if there are no more attachments.
+ """
+ # Count the number of attachments before starting the detach.
+ volume = self.volumes_client.show_volume(volume_id)['volume']
+ attachments = volume['attachments']
+ wait_status = 'in-use' if len(attachments) > 1 else 'available'
+ # Now detach the volume from the given server.
+ self.servers_client.detach_volume(server_id, volume_id)
+ # Now wait for the volume status to change.
+ waiters.wait_for_volume_resource_status(
+ self.volumes_client, volume_id, wait_status)
+
+ def _create_multiattach_volume(self, bootable=False):
+ kwargs = {}
+ if bootable:
+ kwargs['image_ref'] = CONF.compute.image_ref
+ return self.create_volume(multiattach=True, **kwargs)
+
+ def _create_and_multiattach(self):
+ """Creates two server instances and a volume and attaches to both.
+
+ :returns: A three-item tuple of the list of created servers,
+ the created volume, and dict of server ID to volumeAttachment
+ dict entries
+ """
+ servers = []
+ for x in range(2):
+ name = 'multiattach-server-%i' % x
+ servers.append(self.create_test_server(name=name))
+
+ # Now wait for the servers to be ACTIVE.
+ for server in servers:
+ waiters.wait_for_server_status(self.servers_client, server['id'],
+ 'ACTIVE')
+
+ volume = self._create_multiattach_volume()
+
+ # Attach the volume to the servers
+ attachments = self._attach_volume_to_servers(volume, servers)
+ return servers, volume, attachments
+
+ @decorators.idempotent_id('8d5853f7-56e7-4988-9b0c-48cea3c7049a')
+ def test_list_get_volume_attachments_multiattach(self):
+ # Attach a single volume to two servers.
+ servers, volume, attachments = self._create_and_multiattach()
+
+ # List attachments from the volume and make sure the server uuids
+ # are in that list.
+ vol_attachments = self.volumes_client.show_volume(
+ volume['id'])['volume']['attachments']
+ attached_server_ids = [attachment['server_id']
+ for attachment in vol_attachments]
+ self.assertEqual(2, len(attached_server_ids))
+
+ # List Volume attachment of the servers
+ for server in servers:
+ self.assertIn(server['id'], attached_server_ids)
+ vol_attachments = self.servers_client.list_volume_attachments(
+ server['id'])['volumeAttachments']
+ self.assertEqual(1, len(vol_attachments))
+ attachment = attachments[server['id']]
+ self.assertDictEqual(attachment, vol_attachments[0])
+ # Detach the volume from this server.
+ self._detach_multiattach_volume(volume['id'], server['id'])
+
+ def _boot_from_multiattach_volume(self):
+ """Boots a server from a multiattach volume.
+
+ The volume will not be deleted when the server is deleted.
+
+ :returns: 2-item tuple of (server, volume)
+ """
+ volume = self._create_multiattach_volume(bootable=True)
+ # Now create a server from the bootable volume.
+ bdm = [{
+ 'uuid': volume['id'],
+ 'source_type': 'volume',
+ 'destination_type': 'volume',
+ 'boot_index': 0,
+ 'delete_on_termination': False}]
+ server = self.create_test_server(
+ image_id='', block_device_mapping_v2=bdm, wait_until='ACTIVE')
+ # Assert the volume is attached to the server.
+ attachments = self.servers_client.list_volume_attachments(
+ server['id'])['volumeAttachments']
+ self.assertEqual(1, len(attachments))
+ self.assertEqual(volume['id'], attachments[0]['volumeId'])
+ return server, volume
+
+ @decorators.idempotent_id('65e33aa2-185b-44c8-b22e-e524973ed625')
+ def test_boot_from_multiattach_volume(self):
+ """Simple test to boot an instance from a multiattach volume."""
+ self._boot_from_multiattach_volume()
+
+ @utils.services('image')
+ @decorators.idempotent_id('885ac48a-2d7a-40c5-ae8b-1993882d724c')
+ def test_snapshot_volume_backed_multiattach(self):
+ """Boots a server from a multiattach volume and snapshots the server.
+
+ Creating the snapshot of the server will also create a snapshot of
+ the volume.
+ """
+ server, volume = self._boot_from_multiattach_volume()
+ # Create a snapshot of the server (and volume implicitly).
+ self.create_image_from_server(
+ server['id'], name='multiattach-snapshot',
+ wait_until='active', wait_for_server=True)
+ # TODO(mriedem): Make sure the volume snapshot exists. This requires
+ # adding the volume snapshots client to BaseV2ComputeTest.
+ # Delete the server, wait for it to be gone, and make sure the volume
+ # still exists.
+ self.servers_client.delete_server(server['id'])
+ waiters.wait_for_server_termination(self.servers_client, server['id'])
+ # Delete the volume and cascade the delete of the volume snapshot.
+ self.volumes_client.delete_volume(volume['id'], cascade=True)
+ # Now we have to wait for the volume to be gone otherwise the normal
+ # teardown will fail since it will race with our call and the snapshot
+ # might still exist.
+ self.volumes_client.wait_for_resource_deletion(volume['id'])
+
+ @decorators.idempotent_id('f01c7169-a124-4fc7-ae60-5e380e247c9c')
+ @testtools.skipUnless(CONF.compute_feature_enabled.resize,
+ 'Resize not available.')
+ def test_resize_server_with_multiattached_volume(self):
+ # Attach a single volume to multiple servers, then resize the servers
+ servers, volume, _ = self._create_and_multiattach()
+
+ for server in servers:
+ self.resize_server(server['id'], self.flavor_ref_alt)
+
+ for server in servers:
+ self._detach_multiattach_volume(volume['id'], server['id'])
+
+ # TODO(mriedem): Might be interesting to create a bootable multiattach
+ # volume with delete_on_termination=True, create server1 from the
+ # volume, then attach it to server2, and then delete server1 in which
+ # case the volume won't be deleted because it's still attached to
+ # server2 and make sure the volume is still attached to server2.
diff --git a/tempest/api/compute/volumes/test_attach_volume_negative.py b/tempest/api/compute/volumes/test_attach_volume_negative.py
new file mode 100644
index 0000000..8618148
--- /dev/null
+++ b/tempest/api/compute/volumes/test_attach_volume_negative.py
@@ -0,0 +1,71 @@
+# Copyright 2016 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 import config
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+CONF = config.CONF
+
+
+class AttachVolumeNegativeTest(base.BaseV2ComputeTest):
+
+ @classmethod
+ def skip_checks(cls):
+ super(AttachVolumeNegativeTest, cls).skip_checks()
+ if not CONF.service_available.cinder:
+ skip_msg = ("%s skipped as Cinder is not available" % cls.__name__)
+ raise cls.skipException(skip_msg)
+
+ @decorators.attr(type=['negative'])
+ @decorators.related_bug('1630783', status_code=500)
+ @decorators.idempotent_id('a313b5cd-fbd0-49cc-94de-870e99f763c7')
+ def test_delete_attached_volume(self):
+ server = self.create_test_server(wait_until='ACTIVE')
+ volume = self.create_volume()
+
+ path = "/dev/%s" % CONF.compute.volume_device_name
+ self.attach_volume(server, volume, device=path)
+
+ self.assertRaises(lib_exc.BadRequest,
+ self.delete_volume, volume['id'])
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('aab919e2-d992-4cbb-a4ed-745c2475398c')
+ def test_attach_attached_volume_to_same_server(self):
+ # Test attaching the same volume to the same instance once
+ # it's already attached. The nova/cinder validation for this differs
+ # depending on whether or not cinder v3.27 is being used to attach
+ # the volume to the instance.
+ server = self.create_test_server(wait_until='ACTIVE')
+ volume = self.create_volume()
+
+ self.attach_volume(server, volume)
+
+ 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/compute/volumes/test_volume_snapshots.py b/tempest/api/compute/volumes/test_volume_snapshots.py
index a00c0ba..b8ca81d 100644
--- a/tempest/api/compute/volumes/test_volume_snapshots.py
+++ b/tempest/api/compute/volumes/test_volume_snapshots.py
@@ -14,10 +14,10 @@
# under the License.
from tempest.api.compute import base
-from tempest.common.utils import data_utils
from tempest.common import waiters
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
CONF = config.CONF
@@ -25,12 +25,20 @@
class VolumesSnapshotsTestJSON(base.BaseV2ComputeTest):
+ # These tests will fail with a 404 starting from microversion 2.36. For
+ # more information, see:
+ # https://developer.openstack.org/api-ref/compute/#volume-extension-os-volumes-os-snapshots-deprecated
+ max_microversion = '2.35'
+
@classmethod
def skip_checks(cls):
super(VolumesSnapshotsTestJSON, cls).skip_checks()
if not CONF.service_available.cinder:
skip_msg = ("%s skipped as Cinder is not available" % cls.__name__)
raise cls.skipException(skip_msg)
+ if not CONF.volume_feature_enabled.snapshot:
+ skip_msg = ("Cinder volume snapshots are disabled")
+ raise cls.skipException(skip_msg)
@classmethod
def setup_clients(cls):
@@ -38,25 +46,21 @@
cls.volumes_client = cls.volumes_extensions_client
cls.snapshots_client = cls.snapshots_extensions_client
- @test.idempotent_id('cd4ec87d-7825-450d-8040-6e2068f2da8f')
+ @decorators.idempotent_id('cd4ec87d-7825-450d-8040-6e2068f2da8f')
def test_volume_snapshot_create_get_list_delete(self):
- v_name = data_utils.rand_name('Volume')
- volume = self.volumes_client.create_volume(
- size=CONF.volume.volume_size,
- display_name=v_name)['volume']
+ volume = self.create_volume()
self.addCleanup(self.delete_volume, volume['id'])
- waiters.wait_for_volume_status(self.volumes_client, volume['id'],
- 'available')
- s_name = data_utils.rand_name('Snapshot')
+
+ s_name = data_utils.rand_name(self.__class__.__name__ + '-Snapshot')
# Create snapshot
snapshot = self.snapshots_client.create_snapshot(
- volume['id'],
+ volume_id=volume['id'],
display_name=s_name)['snapshot']
def delete_snapshot(snapshot_id):
- waiters.wait_for_snapshot_status(self.snapshots_client,
- snapshot_id,
- 'available')
+ waiters.wait_for_volume_resource_status(self.snapshots_client,
+ snapshot_id,
+ 'available')
# Delete snapshot
self.snapshots_client.delete_snapshot(snapshot_id)
self.snapshots_client.wait_for_resource_deletion(snapshot_id)
diff --git a/tempest/api/compute/volumes/test_volumes_get.py b/tempest/api/compute/volumes/test_volumes_get.py
index 6074054..d83d49e 100644
--- a/tempest/api/compute/volumes/test_volumes_get.py
+++ b/tempest/api/compute/volumes/test_volumes_get.py
@@ -16,10 +16,9 @@
from testtools import matchers
from tempest.api.compute import base
-from tempest.common.utils import data_utils
-from tempest.common import waiters
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
CONF = config.CONF
@@ -27,6 +26,11 @@
class VolumesGetTestJSON(base.BaseV2ComputeTest):
+ # These tests will fail with a 404 starting from microversion 2.36. For
+ # more information, see:
+ # https://developer.openstack.org/api-ref/compute/#volume-extension-os-volumes-os-snapshots-deprecated
+ max_microversion = '2.35'
+
@classmethod
def skip_checks(cls):
super(VolumesGetTestJSON, cls).skip_checks()
@@ -37,35 +41,32 @@
@classmethod
def setup_clients(cls):
super(VolumesGetTestJSON, cls).setup_clients()
- cls.client = cls.volumes_extensions_client
+ cls.volumes_client = cls.volumes_extensions_client
- @test.idempotent_id('f10f25eb-9775-4d9d-9cbe-1cf54dae9d5f')
+ @decorators.idempotent_id('f10f25eb-9775-4d9d-9cbe-1cf54dae9d5f')
def test_volume_create_get_delete(self):
# CREATE, GET, DELETE Volume
- volume = None
- v_name = data_utils.rand_name('Volume')
+ v_name = data_utils.rand_name(self.__class__.__name__ + '-Volume')
metadata = {'Type': 'work'}
# Create volume
- volume = self.client.create_volume(size=CONF.volume.volume_size,
- display_name=v_name,
- metadata=metadata)['volume']
- self.addCleanup(self.delete_volume, volume['id'])
- self.assertIn('id', volume)
- self.assertIn('displayName', volume)
+ volume = self.create_volume(size=CONF.volume.volume_size,
+ display_name=v_name,
+ metadata=metadata)
self.assertEqual(volume['displayName'], v_name,
"The created volume name is not equal "
"to the requested name")
- self.assertTrue(volume['id'] is not None,
- "Field volume id is empty or not found.")
- # Wait for Volume status to become ACTIVE
- waiters.wait_for_volume_status(self.client, volume['id'], 'available')
# GET Volume
- fetched_volume = self.client.show_volume(volume['id'])['volume']
+ fetched_volume = self.volumes_client.show_volume(
+ volume['id'])['volume']
# Verification of details of fetched Volume
self.assertEqual(v_name,
fetched_volume['displayName'],
'The fetched Volume is different '
'from the created Volume')
+ self.assertEqual(CONF.volume.volume_size,
+ fetched_volume['size'],
+ 'The fetched volume size is different '
+ 'from the created Volume')
self.assertEqual(volume['id'],
fetched_volume['id'],
'The fetched Volume is different '
diff --git a/tempest/api/compute/volumes/test_volumes_list.py b/tempest/api/compute/volumes/test_volumes_list.py
index 990e429..b2aebe7 100644
--- a/tempest/api/compute/volumes/test_volumes_list.py
+++ b/tempest/api/compute/volumes/test_volumes_list.py
@@ -14,10 +14,8 @@
# under the License.
from tempest.api.compute import base
-from tempest.common.utils import data_utils
-from tempest.common import waiters
from tempest import config
-from tempest import test
+from tempest.lib import decorators
CONF = config.CONF
@@ -27,7 +25,12 @@
# ensure that the backing file for the volume group that Nova uses
# has space for at least 3 1G volumes!
# If you are running a Devstack environment, ensure that the
- # VOLUME_BACKING_FILE_SIZE is atleast 4G in your localrc
+ # VOLUME_BACKING_FILE_SIZE is at least 4G in your localrc
+
+ # These tests will fail with a 404 starting from microversion 2.36. For
+ # more information, see:
+ # https://developer.openstack.org/api-ref/compute/#volume-extension-os-volumes-os-snapshots-deprecated
+ max_microversion = '2.35'
@classmethod
def skip_checks(cls):
@@ -46,43 +49,13 @@
super(VolumesTestJSON, cls).resource_setup()
# Create 3 Volumes
cls.volume_list = []
- cls.volume_id_list = []
- for i in range(3):
- v_name = data_utils.rand_name('volume')
+ for _ in range(3):
metadata = {'Type': 'work'}
- try:
- volume = cls.client.create_volume(size=CONF.volume.volume_size,
- display_name=v_name,
- metadata=metadata)['volume']
- waiters.wait_for_volume_status(cls.client,
- volume['id'], 'available')
- volume = cls.client.show_volume(volume['id'])['volume']
- cls.volume_list.append(volume)
- cls.volume_id_list.append(volume['id'])
- except Exception:
- if cls.volume_list:
- # We could not create all the volumes, though we were able
- # to create *some* of the volumes. This is typically
- # because the backing file size of the volume group is
- # too small. So, here, we clean up whatever we did manage
- # to create and raise a SkipTest
- for volume in cls.volume_list:
- cls.delete_volume(volume['id'])
- msg = ("Failed to create ALL necessary volumes to run "
- "test. This typically means that the backing file "
- "size of the nova-volumes group is too small to "
- "create the 3 volumes needed by this test case")
- raise cls.skipException(msg)
- raise
+ volume = cls.create_volume(metadata=metadata)
+ volume = cls.client.show_volume(volume['id'])['volume']
+ cls.volume_list.append(volume)
- @classmethod
- def resource_cleanup(cls):
- # Delete the created Volumes
- for volume in cls.volume_list:
- cls.delete_volume(volume['id'])
- super(VolumesTestJSON, cls).resource_cleanup()
-
- @test.idempotent_id('bc2dd1a0-15af-48e5-9990-f2e75a48325d')
+ @decorators.idempotent_id('bc2dd1a0-15af-48e5-9990-f2e75a48325d')
def test_volume_list(self):
# Should return the list of Volumes
# Fetch all Volumes
@@ -97,7 +70,7 @@
', '.join(m_vol['displayName']
for m_vol in missing_volumes))
- @test.idempotent_id('bad0567a-5a4f-420b-851e-780b55bb867c')
+ @decorators.idempotent_id('bad0567a-5a4f-420b-851e-780b55bb867c')
def test_volume_list_with_details(self):
# Should return the list of Volumes with details
# Fetch all Volumes
@@ -112,7 +85,7 @@
', '.join(m_vol['displayName']
for m_vol in missing_volumes))
- @test.idempotent_id('1048ed81-2baf-487a-b284-c0622b86e7b8')
+ @decorators.idempotent_id('1048ed81-2baf-487a-b284-c0622b86e7b8')
def test_volume_list_param_limit(self):
# Return the list of volumes based on limit set
params = {'limit': 2}
@@ -121,7 +94,7 @@
self.assertEqual(len(fetched_vol_list), params['limit'],
"Failed to list volumes by limit set")
- @test.idempotent_id('33985568-4965-49d5-9bcc-0aa007ca5b7a')
+ @decorators.idempotent_id('33985568-4965-49d5-9bcc-0aa007ca5b7a')
def test_volume_list_with_detail_param_limit(self):
# Return the list of volumes with details based on limit set.
params = {'limit': 2}
@@ -131,7 +104,7 @@
self.assertEqual(len(fetched_vol_list), params['limit'],
"Failed to list volume details by limit set")
- @test.idempotent_id('51c22651-a074-4ea7-af0b-094f9331303e')
+ @decorators.idempotent_id('51c22651-a074-4ea7-af0b-094f9331303e')
def test_volume_list_param_offset_and_limit(self):
# Return the list of volumes based on offset and limit set.
# get all volumes list
@@ -148,7 +121,7 @@
all_vol_list[index + params['offset']]['id'],
"Failed to list volumes by offset and limit")
- @test.idempotent_id('06b6abc4-3f10-48e9-a7a1-3facc98f03e5')
+ @decorators.idempotent_id('06b6abc4-3f10-48e9-a7a1-3facc98f03e5')
def test_volume_list_with_detail_param_offset_and_limit(self):
# Return the list of volumes details based on offset and limit set.
# get all volumes list
diff --git a/tempest/api/compute/volumes/test_volumes_negative.py b/tempest/api/compute/volumes/test_volumes_negative.py
index 01a0baf..87f7d8a 100644
--- a/tempest/api/compute/volumes/test_volumes_negative.py
+++ b/tempest/api/compute/volumes/test_volumes_negative.py
@@ -13,20 +13,22 @@
# License for the specific language governing permissions and limitations
# under the License.
-import uuid
-
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.compute import base
-from tempest.common.utils import data_utils
from tempest import config
-from tempest import test
+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 VolumesNegativeTest(base.BaseV2ComputeTest):
+ # These tests will fail with a 404 starting from microversion 2.36. For
+ # more information, see:
+ # https://developer.openstack.org/api-ref/compute/#volume-extension-os-volumes-os-snapshots-deprecated
+ max_microversion = '2.35'
+
@classmethod
def skip_checks(cls):
super(VolumesNegativeTest, cls).skip_checks()
@@ -39,76 +41,70 @@
super(VolumesNegativeTest, cls).setup_clients()
cls.client = cls.volumes_extensions_client
- @test.attr(type=['negative'])
- @test.idempotent_id('c03ea686-905b-41a2-8748-9635154b7c57')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('c03ea686-905b-41a2-8748-9635154b7c57')
def test_volume_get_nonexistent_volume_id(self):
# Negative: Should not be able to get details of nonexistent volume
# Creating a nonexistent volume id
# Trying to GET a non existent volume
self.assertRaises(lib_exc.NotFound, self.client.show_volume,
- str(uuid.uuid4()))
+ data_utils.rand_uuid())
- @test.attr(type=['negative'])
- @test.idempotent_id('54a34226-d910-4b00-9ef8-8683e6c55846')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('54a34226-d910-4b00-9ef8-8683e6c55846')
def test_volume_delete_nonexistent_volume_id(self):
# Negative: Should not be able to delete nonexistent Volume
# Creating nonexistent volume id
# Trying to DELETE a non existent volume
self.assertRaises(lib_exc.NotFound, self.client.delete_volume,
- str(uuid.uuid4()))
+ data_utils.rand_uuid())
- @test.attr(type=['negative'])
- @test.idempotent_id('5125ae14-152b-40a7-b3c5-eae15e9022ef')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('5125ae14-152b-40a7-b3c5-eae15e9022ef')
def test_create_volume_with_invalid_size(self):
# Negative: Should not be able to create volume with invalid size
# in request
- v_name = data_utils.rand_name('Volume')
+ v_name = data_utils.rand_name(self.__class__.__name__ + '-Volume')
metadata = {'Type': 'work'}
self.assertRaises(lib_exc.BadRequest, self.client.create_volume,
size='#$%', display_name=v_name, metadata=metadata)
- @test.attr(type=['negative'])
- @test.idempotent_id('131cb3a1-75cc-4d40-b4c3-1317f64719b0')
- def test_create_volume_with_out_passing_size(self):
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('131cb3a1-75cc-4d40-b4c3-1317f64719b0')
+ def test_create_volume_without_passing_size(self):
# Negative: Should not be able to create volume without passing size
# in request
- v_name = data_utils.rand_name('Volume')
+ v_name = data_utils.rand_name(self.__class__.__name__ + '-Volume')
metadata = {'Type': 'work'}
self.assertRaises(lib_exc.BadRequest, self.client.create_volume,
size='', display_name=v_name, metadata=metadata)
- @test.attr(type=['negative'])
- @test.idempotent_id('8cce995e-0a83-479a-b94d-e1e40b8a09d1')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('8cce995e-0a83-479a-b94d-e1e40b8a09d1')
def test_create_volume_with_size_zero(self):
# Negative: Should not be able to create volume with size zero
- v_name = data_utils.rand_name('Volume')
+ v_name = data_utils.rand_name(self.__class__.__name__ + '-Volume')
metadata = {'Type': 'work'}
self.assertRaises(lib_exc.BadRequest, self.client.create_volume,
size='0', display_name=v_name, metadata=metadata)
- @test.attr(type=['negative'])
- @test.idempotent_id('f01904f2-e975-4915-98ce-cb5fa27bde4f')
- def test_get_invalid_volume_id(self):
- # Negative: Should not be able to get volume with invalid id
- self.assertRaises(lib_exc.NotFound,
- self.client.show_volume, '#$%%&^&^')
-
- @test.attr(type=['negative'])
- @test.idempotent_id('62bab09a-4c03-4617-8cca-8572bc94af9b')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('62bab09a-4c03-4617-8cca-8572bc94af9b')
def test_get_volume_without_passing_volume_id(self):
# Negative: Should not be able to get volume when empty ID is passed
self.assertRaises(lib_exc.NotFound, self.client.show_volume, '')
- @test.attr(type=['negative'])
- @test.idempotent_id('62972737-124b-4513-b6cf-2f019f178494')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('62972737-124b-4513-b6cf-2f019f178494')
def test_delete_invalid_volume_id(self):
# Negative: Should not be able to delete volume when invalid ID is
# passed
self.assertRaises(lib_exc.NotFound,
- self.client.delete_volume, '!@#$%^&*()')
+ self.client.delete_volume,
+ data_utils.rand_name('invalid'))
- @test.attr(type=['negative'])
- @test.idempotent_id('0d1417c5-4ae8-4c2c-adc5-5f0b864253e5')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('0d1417c5-4ae8-4c2c-adc5-5f0b864253e5')
def test_delete_volume_without_passing_volume_id(self):
# Negative: Should not be able to delete volume when empty ID is passed
self.assertRaises(lib_exc.NotFound, self.client.delete_volume, '')
diff --git a/tempest/api/data_processing/__init__.py b/tempest/api/data_processing/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/api/data_processing/__init__.py
+++ /dev/null
diff --git a/tempest/api/data_processing/base.py b/tempest/api/data_processing/base.py
deleted file mode 100644
index 4e88f65..0000000
--- a/tempest/api/data_processing/base.py
+++ /dev/null
@@ -1,483 +0,0 @@
-# Copyright (c) 2014 Mirantis Inc.
-#
-# 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 collections import OrderedDict
-
-import six
-from tempest_lib import exceptions as lib_exc
-
-from tempest import config
-from tempest import exceptions
-import tempest.test
-
-
-CONF = config.CONF
-
-"""Default templates.
-There should always be at least a master1 and a worker1 node
-group template."""
-DEFAULT_TEMPLATES = {
- 'vanilla': OrderedDict([
- ('2.6.0', {
- 'NODES': {
- 'master1': {
- 'count': 1,
- 'node_processes': ['namenode', 'resourcemanager',
- 'hiveserver']
- },
- 'master2': {
- 'count': 1,
- 'node_processes': ['oozie', 'historyserver',
- 'secondarynamenode']
- },
- 'worker1': {
- 'count': 1,
- 'node_processes': ['datanode', 'nodemanager'],
- 'node_configs': {
- 'MapReduce': {
- 'yarn.app.mapreduce.am.resource.mb': 256,
- 'yarn.app.mapreduce.am.command-opts': '-Xmx256m'
- },
- 'YARN': {
- 'yarn.scheduler.minimum-allocation-mb': 256,
- 'yarn.scheduler.maximum-allocation-mb': 1024,
- 'yarn.nodemanager.vmem-check-enabled': False
- }
- }
- }
- },
- 'cluster_configs': {
- 'HDFS': {
- 'dfs.replication': 1
- }
- }
- }),
- ('1.2.1', {
- 'NODES': {
- 'master1': {
- 'count': 1,
- 'node_processes': ['namenode', 'jobtracker']
- },
- 'worker1': {
- 'count': 1,
- 'node_processes': ['datanode', 'tasktracker'],
- 'node_configs': {
- 'HDFS': {
- 'Data Node Heap Size': 1024
- },
- 'MapReduce': {
- 'Task Tracker Heap Size': 1024
- }
- }
- }
- },
- 'cluster_configs': {
- 'HDFS': {
- 'dfs.replication': 1
- },
- 'MapReduce': {
- 'mapred.map.tasks.speculative.execution': False,
- 'mapred.child.java.opts': '-Xmx500m'
- },
- 'general': {
- 'Enable Swift': False
- }
- }
- })
- ]),
- 'hdp': OrderedDict([
- ('2.0.6', {
- 'NODES': {
- 'master1': {
- 'count': 1,
- 'node_processes': ['NAMENODE', 'SECONDARY_NAMENODE',
- 'ZOOKEEPER_SERVER', 'AMBARI_SERVER',
- 'HISTORYSERVER', 'RESOURCEMANAGER',
- 'GANGLIA_SERVER', 'NAGIOS_SERVER',
- 'OOZIE_SERVER']
- },
- 'worker1': {
- 'count': 1,
- 'node_processes': ['HDFS_CLIENT', 'DATANODE',
- 'YARN_CLIENT', 'ZOOKEEPER_CLIENT',
- 'MAPREDUCE2_CLIENT', 'NODEMANAGER',
- 'PIG', 'OOZIE_CLIENT']
- }
- },
- 'cluster_configs': {
- 'HDFS': {
- 'dfs.replication': 1
- }
- }
- })
- ]),
- 'spark': OrderedDict([
- ('1.0.0', {
- 'NODES': {
- 'master1': {
- 'count': 1,
- 'node_processes': ['namenode', 'master']
- },
- 'worker1': {
- 'count': 1,
- 'node_processes': ['datanode', 'slave']
- }
- },
- 'cluster_configs': {
- 'HDFS': {
- 'dfs.replication': 1
- }
- }
- })
- ]),
- 'cdh': OrderedDict([
- ('5.3.0', {
- 'NODES': {
- 'master1': {
- 'count': 1,
- 'node_processes': ['CLOUDERA_MANAGER']
- },
- 'master2': {
- 'count': 1,
- 'node_processes': ['HDFS_NAMENODE',
- 'YARN_RESOURCEMANAGER']
- },
- 'master3': {
- 'count': 1,
- 'node_processes': ['OOZIE_SERVER', 'YARN_JOBHISTORY',
- 'HDFS_SECONDARYNAMENODE',
- 'HIVE_METASTORE', 'HIVE_SERVER2']
- },
- 'worker1': {
- 'count': 1,
- 'node_processes': ['YARN_NODEMANAGER', 'HDFS_DATANODE']
- }
- },
- 'cluster_configs': {
- 'HDFS': {
- 'dfs_replication': 1
- }
- }
- }),
- ('5', {
- 'NODES': {
- 'master1': {
- 'count': 1,
- 'node_processes': ['CLOUDERA_MANAGER']
- },
- 'master2': {
- 'count': 1,
- 'node_processes': ['HDFS_NAMENODE',
- 'YARN_RESOURCEMANAGER']
- },
- 'master3': {
- 'count': 1,
- 'node_processes': ['OOZIE_SERVER', 'YARN_JOBHISTORY',
- 'HDFS_SECONDARYNAMENODE',
- 'HIVE_METASTORE', 'HIVE_SERVER2']
- },
- 'worker1': {
- 'count': 1,
- 'node_processes': ['YARN_NODEMANAGER', 'HDFS_DATANODE']
- }
- },
- 'cluster_configs': {
- 'HDFS': {
- 'dfs_replication': 1
- }
- }
- })
- ]),
- 'mapr': OrderedDict([
- ('4.0.1.mrv2', {
- 'NODES': {
- 'master1': {
- 'count': 1,
- 'node_processes': ['CLDB', 'FileServer', 'ZooKeeper',
- 'NodeManager', 'ResourceManager',
- 'HistoryServer', 'Oozie']
- },
- 'worker1': {
- 'count': 1,
- 'node_processes': ['FileServer', 'NodeManager', 'Pig']
- }
- },
- 'cluster_configs': {
- 'Hive': {
- 'Hive Version': '0.13',
- }
- }
- })
- ]),
-}
-
-
-class BaseDataProcessingTest(tempest.test.BaseTestCase):
-
- credentials = ['primary']
-
- @classmethod
- def skip_checks(cls):
- super(BaseDataProcessingTest, cls).skip_checks()
- if not CONF.service_available.sahara:
- raise cls.skipException('Sahara support is required')
- cls.default_plugin = cls._get_default_plugin()
-
- @classmethod
- def setup_clients(cls):
- super(BaseDataProcessingTest, cls).setup_clients()
- cls.client = cls.os.data_processing_client
-
- @classmethod
- def resource_setup(cls):
- super(BaseDataProcessingTest, cls).resource_setup()
-
- cls.default_version = cls._get_default_version()
- if cls.default_plugin is not None and cls.default_version is None:
- raise exceptions.InvalidConfiguration(
- message="No known Sahara plugin version was found")
- cls.flavor_ref = CONF.compute.flavor_ref
-
- # add lists for watched resources
- cls._node_group_templates = []
- cls._cluster_templates = []
- cls._data_sources = []
- cls._job_binary_internals = []
- cls._job_binaries = []
- cls._jobs = []
-
- @classmethod
- def resource_cleanup(cls):
- cls.cleanup_resources(getattr(cls, '_cluster_templates', []),
- cls.client.delete_cluster_template)
- cls.cleanup_resources(getattr(cls, '_node_group_templates', []),
- cls.client.delete_node_group_template)
- cls.cleanup_resources(getattr(cls, '_jobs', []), cls.client.delete_job)
- cls.cleanup_resources(getattr(cls, '_job_binaries', []),
- cls.client.delete_job_binary)
- cls.cleanup_resources(getattr(cls, '_job_binary_internals', []),
- cls.client.delete_job_binary_internal)
- cls.cleanup_resources(getattr(cls, '_data_sources', []),
- cls.client.delete_data_source)
- super(BaseDataProcessingTest, cls).resource_cleanup()
-
- @staticmethod
- def cleanup_resources(resource_id_list, method):
- for resource_id in resource_id_list:
- try:
- method(resource_id)
- except lib_exc.NotFound:
- # ignore errors while auto removing created resource
- pass
-
- @classmethod
- def create_node_group_template(cls, name, plugin_name, hadoop_version,
- node_processes, flavor_id,
- node_configs=None, **kwargs):
- """Creates watched node group template with specified params.
-
- It supports passing additional params using kwargs and returns created
- object. All resources created in this method will be automatically
- removed in tearDownClass method.
- """
- resp_body = cls.client.create_node_group_template(name, plugin_name,
- hadoop_version,
- node_processes,
- flavor_id,
- node_configs,
- **kwargs)
- resp_body = resp_body['node_group_template']
- # store id of created node group template
- cls._node_group_templates.append(resp_body['id'])
-
- return resp_body
-
- @classmethod
- def create_cluster_template(cls, name, plugin_name, hadoop_version,
- node_groups, cluster_configs=None, **kwargs):
- """Creates watched cluster template with specified params.
-
- It supports passing additional params using kwargs and returns created
- object. All resources created in this method will be automatically
- removed in tearDownClass method.
- """
- resp_body = cls.client.create_cluster_template(name, plugin_name,
- hadoop_version,
- node_groups,
- cluster_configs,
- **kwargs)
- resp_body = resp_body['cluster_template']
- # store id of created cluster template
- cls._cluster_templates.append(resp_body['id'])
-
- return resp_body
-
- @classmethod
- def create_data_source(cls, name, type, url, **kwargs):
- """Creates watched data source with specified params.
-
- It supports passing additional params using kwargs and returns created
- object. All resources created in this method will be automatically
- removed in tearDownClass method.
- """
- resp_body = cls.client.create_data_source(name, type, url, **kwargs)
- resp_body = resp_body['data_source']
- # store id of created data source
- cls._data_sources.append(resp_body['id'])
-
- return resp_body
-
- @classmethod
- def create_job_binary_internal(cls, name, data):
- """Creates watched job binary internal with specified params.
-
- It returns created object. All resources created in this method will
- be automatically removed in tearDownClass method.
- """
- resp_body = cls.client.create_job_binary_internal(name, data)
- resp_body = resp_body['job_binary_internal']
- # store id of created job binary internal
- cls._job_binary_internals.append(resp_body['id'])
-
- return resp_body
-
- @classmethod
- def create_job_binary(cls, name, url, extra=None, **kwargs):
- """Creates watched job binary with specified params.
-
- It supports passing additional params using kwargs and returns created
- object. All resources created in this method will be automatically
- removed in tearDownClass method.
- """
- resp_body = cls.client.create_job_binary(name, url, extra, **kwargs)
- resp_body = resp_body['job_binary']
- # store id of created job binary
- cls._job_binaries.append(resp_body['id'])
-
- return resp_body
-
- @classmethod
- def create_job(cls, name, job_type, mains, libs=None, **kwargs):
- """Creates watched job with specified params.
-
- It supports passing additional params using kwargs and returns created
- object. All resources created in this method will be automatically
- removed in tearDownClass method.
- """
- resp_body = cls.client.create_job(name,
- job_type, mains, libs, **kwargs)
- resp_body = resp_body['job']
- # store id of created job
- cls._jobs.append(resp_body['id'])
-
- return resp_body
-
- @classmethod
- def _get_default_plugin(cls):
- """Returns the default plugin used for testing."""
- if len(CONF.data_processing_feature_enabled.plugins) == 0:
- return None
-
- for plugin in CONF.data_processing_feature_enabled.plugins:
- if plugin in DEFAULT_TEMPLATES.keys():
- break
- else:
- plugin = ''
- return plugin
-
- @classmethod
- def _get_default_version(cls):
- """Returns the default plugin version used for testing.
-
- This is gathered separately from the plugin to allow
- the usage of plugin name in skip_checks. This method is
- rather invoked into resource_setup, which allows API calls
- and exceptions.
- """
- if not cls.default_plugin:
- return None
- plugin = cls.client.get_plugin(cls.default_plugin)['plugin']
-
- for version in DEFAULT_TEMPLATES[cls.default_plugin].keys():
- if version in plugin['versions']:
- break
- else:
- version = None
-
- return version
-
- @classmethod
- def get_node_group_template(cls, nodegroup='worker1'):
- """Returns a node group template for the default plugin."""
- try:
- plugin_data = (
- DEFAULT_TEMPLATES[cls.default_plugin][cls.default_version]
- )
- nodegroup_data = plugin_data['NODES'][nodegroup]
- node_group_template = {
- 'description': 'Test node group template',
- 'plugin_name': cls.default_plugin,
- 'hadoop_version': cls.default_version,
- 'node_processes': nodegroup_data['node_processes'],
- 'flavor_id': cls.flavor_ref,
- 'node_configs': nodegroup_data.get('node_configs', {}),
- }
- return node_group_template
- except (IndexError, KeyError):
- return None
-
- @classmethod
- def get_cluster_template(cls, node_group_template_ids=None):
- """Returns a cluster template for the default plugin.
-
- node_group_template_defined contains the type and ID of pre-defined
- node group templates that have to be used in the cluster template
- (instead of dynamically defining them with 'node_processes').
- """
- if node_group_template_ids is None:
- node_group_template_ids = {}
- try:
- plugin_data = (
- DEFAULT_TEMPLATES[cls.default_plugin][cls.default_version]
- )
-
- all_node_groups = []
- for ng_name, ng_data in six.iteritems(plugin_data['NODES']):
- node_group = {
- 'name': '%s-node' % (ng_name),
- 'flavor_id': cls.flavor_ref,
- 'count': ng_data['count']
- }
- if ng_name in node_group_template_ids.keys():
- # node group already defined, use it
- node_group['node_group_template_id'] = (
- node_group_template_ids[ng_name]
- )
- else:
- # node_processes list defined on-the-fly
- node_group['node_processes'] = ng_data['node_processes']
- if 'node_configs' in ng_data:
- node_group['node_configs'] = ng_data['node_configs']
- all_node_groups.append(node_group)
-
- cluster_template = {
- 'description': 'Test cluster template',
- 'plugin_name': cls.default_plugin,
- 'hadoop_version': cls.default_version,
- 'cluster_configs': plugin_data.get('cluster_configs', {}),
- 'node_groups': all_node_groups,
- }
- return cluster_template
- except (IndexError, KeyError):
- return None
diff --git a/tempest/api/data_processing/test_cluster_templates.py b/tempest/api/data_processing/test_cluster_templates.py
deleted file mode 100644
index dfd8e27..0000000
--- a/tempest/api/data_processing/test_cluster_templates.py
+++ /dev/null
@@ -1,124 +0,0 @@
-# Copyright (c) 2014 Mirantis Inc.
-#
-# 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.data_processing import base as dp_base
-from tempest.common.utils import data_utils
-from tempest import exceptions
-from tempest import test
-
-
-class ClusterTemplateTest(dp_base.BaseDataProcessingTest):
- # Link to the API documentation is http://docs.openstack.org/developer/
- # sahara/restapi/rest_api_v1.0.html#cluster-templates
-
- @classmethod
- def skip_checks(cls):
- super(ClusterTemplateTest, cls).skip_checks()
- if cls.default_plugin is None:
- raise cls.skipException("No Sahara plugins configured")
-
- @classmethod
- def resource_setup(cls):
- super(ClusterTemplateTest, cls).resource_setup()
-
- # pre-define a node group templates
- node_group_template_w = cls.get_node_group_template('worker1')
- if node_group_template_w is None:
- raise exceptions.InvalidConfiguration(
- message="No known Sahara plugin was found")
-
- node_group_template_w['name'] = data_utils.rand_name(
- 'sahara-ng-template')
- resp_body = cls.create_node_group_template(**node_group_template_w)
- node_group_template_id = resp_body['id']
- configured_node_group_templates = {'worker1': node_group_template_id}
-
- cls.full_cluster_template = cls.get_cluster_template(
- configured_node_group_templates)
-
- # create cls.cluster_template variable to use for comparison to cluster
- # template response body. The 'node_groups' field in the response body
- # has some extra info that post body does not have. The 'node_groups'
- # field in the response body is something like this
- #
- # 'node_groups': [
- # {
- # 'count': 3,
- # 'name': 'worker-node',
- # 'volume_mount_prefix': '/volumes/disk',
- # 'created_at': '2014-05-21 14:31:37',
- # 'updated_at': None,
- # 'floating_ip_pool': None,
- # ...
- # },
- # ...
- # ]
- cls.cluster_template = cls.full_cluster_template.copy()
- del cls.cluster_template['node_groups']
-
- def _create_cluster_template(self, template_name=None):
- """Creates Cluster Template with optional name specified.
-
- It creates template, ensures template name and response body.
- Returns id and name of created template.
- """
- if not template_name:
- # generate random name if it's not specified
- template_name = data_utils.rand_name('sahara-cluster-template')
-
- # create cluster template
- resp_body = self.create_cluster_template(template_name,
- **self.full_cluster_template)
-
- # ensure that template created successfully
- self.assertEqual(template_name, resp_body['name'])
- self.assertDictContainsSubset(self.cluster_template, resp_body)
-
- return resp_body['id'], template_name
-
- @test.attr(type='smoke')
- @test.idempotent_id('3525f1f1-3f9c-407d-891a-a996237e728b')
- def test_cluster_template_create(self):
- self._create_cluster_template()
-
- @test.attr(type='smoke')
- @test.idempotent_id('7a161882-e430-4840-a1c6-1d928201fab2')
- def test_cluster_template_list(self):
- template_info = self._create_cluster_template()
-
- # check for cluster template in list
- templates = self.client.list_cluster_templates()['cluster_templates']
- templates_info = [(template['id'], template['name'])
- for template in templates]
- self.assertIn(template_info, templates_info)
-
- @test.attr(type='smoke')
- @test.idempotent_id('2b75fe22-f731-4b0f-84f1-89ab25f86637')
- def test_cluster_template_get(self):
- template_id, template_name = self._create_cluster_template()
-
- # check cluster template fetch by id
- template = self.client.get_cluster_template(template_id)
- template = template['cluster_template']
- self.assertEqual(template_name, template['name'])
- self.assertDictContainsSubset(self.cluster_template, template)
-
- @test.attr(type='smoke')
- @test.idempotent_id('ff1fd989-171c-4dd7-91fd-9fbc71b09675')
- def test_cluster_template_delete(self):
- template_id, _ = self._create_cluster_template()
-
- # delete the cluster template by id
- self.client.delete_cluster_template(template_id)
- # TODO(ylobankov): check that cluster template is really deleted
diff --git a/tempest/api/data_processing/test_data_sources.py b/tempest/api/data_processing/test_data_sources.py
deleted file mode 100644
index 67d09a0..0000000
--- a/tempest/api/data_processing/test_data_sources.py
+++ /dev/null
@@ -1,161 +0,0 @@
-# Copyright (c) 2014 Mirantis Inc.
-#
-# 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.data_processing import base as dp_base
-from tempest.common.utils import data_utils
-from tempest import test
-
-
-class DataSourceTest(dp_base.BaseDataProcessingTest):
- @classmethod
- def resource_setup(cls):
- super(DataSourceTest, cls).resource_setup()
- cls.swift_data_source_with_creds = {
- 'url': 'swift://sahara-container.sahara/input-source',
- 'description': 'Test data source',
- 'credentials': {
- 'user': cls.os.credentials.username,
- 'password': cls.os.credentials.password
- },
- 'type': 'swift'
- }
- cls.swift_data_source = cls.swift_data_source_with_creds.copy()
- del cls.swift_data_source['credentials']
-
- cls.local_hdfs_data_source = {
- 'url': 'input-source',
- 'description': 'Test data source',
- 'type': 'hdfs'
- }
-
- cls.external_hdfs_data_source = {
- 'url': 'hdfs://172.18.168.2:8020/usr/hadoop/input-source',
- 'description': 'Test data source',
- 'type': 'hdfs'
- }
-
- def _create_data_source(self, source_body, source_name=None):
- """Creates Data Source with optional name specified.
-
- It creates a link to input-source file (it may not exist), ensures
- source name and response body. Returns id and name of created source.
- """
- if not source_name:
- # generate random name if it's not specified
- source_name = data_utils.rand_name('sahara-data-source')
-
- # create data source
- resp_body = self.create_data_source(source_name, **source_body)
-
- # ensure that source created successfully
- self.assertEqual(source_name, resp_body['name'])
- if source_body['type'] == 'swift':
- source_body = self.swift_data_source
- self.assertDictContainsSubset(source_body, resp_body)
-
- return resp_body['id'], source_name
-
- def _list_data_sources(self, source_info):
- # check for data source in list
- sources = self.client.list_data_sources()['data_sources']
- sources_info = [(source['id'], source['name']) for source in sources]
- self.assertIn(source_info, sources_info)
-
- def _get_data_source(self, source_id, source_name, source_body):
- # check data source fetch by id
- source = self.client.get_data_source(source_id)['data_source']
- self.assertEqual(source_name, source['name'])
- self.assertDictContainsSubset(source_body, source)
-
- @test.attr(type='smoke')
- @test.idempotent_id('9e0e836d-c372-4fca-91b7-b66c3e9646c8')
- def test_swift_data_source_create(self):
- self._create_data_source(self.swift_data_source_with_creds)
-
- @test.attr(type='smoke')
- @test.idempotent_id('3cb87a4a-0534-4b97-9edc-8bbc822b68a0')
- def test_swift_data_source_list(self):
- source_info = (
- self._create_data_source(self.swift_data_source_with_creds))
- self._list_data_sources(source_info)
-
- @test.attr(type='smoke')
- @test.idempotent_id('fc07409b-6477-4cb3-9168-e633c46b227f')
- def test_swift_data_source_get(self):
- source_id, source_name = (
- self._create_data_source(self.swift_data_source_with_creds))
- self._get_data_source(source_id, source_name, self.swift_data_source)
-
- @test.attr(type='smoke')
- @test.idempotent_id('df53669c-0cd1-4cf7-b408-4cf215d8beb8')
- def test_swift_data_source_delete(self):
- source_id, _ = (
- self._create_data_source(self.swift_data_source_with_creds))
-
- # delete the data source by id
- self.client.delete_data_source(source_id)
-
- @test.attr(type='smoke')
- @test.idempotent_id('88505d52-db01-4229-8f1d-a1137da5fe2d')
- def test_local_hdfs_data_source_create(self):
- self._create_data_source(self.local_hdfs_data_source)
-
- @test.attr(type='smoke')
- @test.idempotent_id('81d7d42a-d7f6-4d9b-b38c-0801a4dfe3c2')
- def test_local_hdfs_data_source_list(self):
- source_info = self._create_data_source(self.local_hdfs_data_source)
- self._list_data_sources(source_info)
-
- @test.attr(type='smoke')
- @test.idempotent_id('ec0144c6-db1e-4169-bb06-7abae14a8443')
- def test_local_hdfs_data_source_get(self):
- source_id, source_name = (
- self._create_data_source(self.local_hdfs_data_source))
- self._get_data_source(
- source_id, source_name, self.local_hdfs_data_source)
-
- @test.attr(type='smoke')
- @test.idempotent_id('e398308b-4230-4f86-ba10-9b0b60a59c8d')
- def test_local_hdfs_data_source_delete(self):
- source_id, _ = self._create_data_source(self.local_hdfs_data_source)
-
- # delete the data source by id
- self.client.delete_data_source(source_id)
-
- @test.attr(type='smoke')
- @test.idempotent_id('bfd91128-e642-4d95-a973-3e536962180c')
- def test_external_hdfs_data_source_create(self):
- self._create_data_source(self.external_hdfs_data_source)
-
- @test.attr(type='smoke')
- @test.idempotent_id('92e2be72-f7ab-499d-ae01-fb9943c90d8e')
- def test_external_hdfs_data_source_list(self):
- source_info = self._create_data_source(self.external_hdfs_data_source)
- self._list_data_sources(source_info)
-
- @test.attr(type='smoke')
- @test.idempotent_id('a31edb1b-6bc6-4f42-871f-70cd243184ac')
- def test_external_hdfs_data_source_get(self):
- source_id, source_name = (
- self._create_data_source(self.external_hdfs_data_source))
- self._get_data_source(
- source_id, source_name, self.external_hdfs_data_source)
-
- @test.attr(type='smoke')
- @test.idempotent_id('295924cd-a085-4b45-aea8-0707cdb2da7e')
- def test_external_hdfs_data_source_delete(self):
- source_id, _ = self._create_data_source(self.external_hdfs_data_source)
-
- # delete the data source by id
- self.client.delete_data_source(source_id)
diff --git a/tempest/api/data_processing/test_job_binaries.py b/tempest/api/data_processing/test_job_binaries.py
deleted file mode 100644
index a47ddbc..0000000
--- a/tempest/api/data_processing/test_job_binaries.py
+++ /dev/null
@@ -1,148 +0,0 @@
-# Copyright (c) 2014 Mirantis Inc.
-#
-# 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.data_processing import base as dp_base
-from tempest.common.utils import data_utils
-from tempest import test
-
-
-class JobBinaryTest(dp_base.BaseDataProcessingTest):
- # Link to the API documentation is http://docs.openstack.org/developer/
- # sahara/restapi/rest_api_v1.1_EDP.html#job-binaries
-
- @classmethod
- def resource_setup(cls):
- super(JobBinaryTest, cls).resource_setup()
- cls.swift_job_binary_with_extra = {
- 'url': 'swift://sahara-container.sahara/example.jar',
- 'description': 'Test job binary',
- 'extra': {
- 'user': cls.os.credentials.username,
- 'password': cls.os.credentials.password
- }
- }
- # Create extra cls.swift_job_binary variable to use for comparison to
- # job binary response body because response body has no 'extra' field.
- cls.swift_job_binary = cls.swift_job_binary_with_extra.copy()
- del cls.swift_job_binary['extra']
-
- name = data_utils.rand_name('sahara-internal-job-binary')
- cls.job_binary_data = 'Some script may be data'
- job_binary_internal = (
- cls.create_job_binary_internal(name, cls.job_binary_data))
- cls.internal_db_job_binary = {
- 'url': 'internal-db://%s' % job_binary_internal['id'],
- 'description': 'Test job binary',
- }
-
- def _create_job_binary(self, binary_body, binary_name=None):
- """Creates Job Binary with optional name specified.
-
- It creates a link to data (jar, pig files, etc.), ensures job binary
- name and response body. Returns id and name of created job binary.
- Data may not exist when using Swift as data storage.
- In other cases data must exist in storage.
- """
- if not binary_name:
- # generate random name if it's not specified
- binary_name = data_utils.rand_name('sahara-job-binary')
-
- # create job binary
- resp_body = self.create_job_binary(binary_name, **binary_body)
-
- # ensure that binary created successfully
- self.assertEqual(binary_name, resp_body['name'])
- if 'swift' in binary_body['url']:
- binary_body = self.swift_job_binary
- self.assertDictContainsSubset(binary_body, resp_body)
-
- return resp_body['id'], binary_name
-
- @test.attr(type='smoke')
- @test.idempotent_id('c00d43f8-4360-45f8-b280-af1a201b12d3')
- def test_swift_job_binary_create(self):
- self._create_job_binary(self.swift_job_binary_with_extra)
-
- @test.attr(type='smoke')
- @test.idempotent_id('f8809352-e79d-4748-9359-ce1efce89f2a')
- def test_swift_job_binary_list(self):
- binary_info = self._create_job_binary(self.swift_job_binary_with_extra)
-
- # check for job binary in list
- binaries = self.client.list_job_binaries()['binaries']
- binaries_info = [(binary['id'], binary['name']) for binary in binaries]
- self.assertIn(binary_info, binaries_info)
-
- @test.attr(type='smoke')
- @test.idempotent_id('2d4a670f-e8f1-413c-b5ac-50c1bfe9e1b1')
- def test_swift_job_binary_get(self):
- binary_id, binary_name = (
- self._create_job_binary(self.swift_job_binary_with_extra))
-
- # check job binary fetch by id
- binary = self.client.get_job_binary(binary_id)['job_binary']
- self.assertEqual(binary_name, binary['name'])
- self.assertDictContainsSubset(self.swift_job_binary, binary)
-
- @test.attr(type='smoke')
- @test.idempotent_id('9b0e8f38-04f3-4616-b399-cfa7eb2677ed')
- def test_swift_job_binary_delete(self):
- binary_id, _ = (
- self._create_job_binary(self.swift_job_binary_with_extra))
-
- # delete the job binary by id
- self.client.delete_job_binary(binary_id)
-
- @test.attr(type='smoke')
- @test.idempotent_id('63662f6d-8291-407e-a6fc-f654522ebab6')
- def test_internal_db_job_binary_create(self):
- self._create_job_binary(self.internal_db_job_binary)
-
- @test.attr(type='smoke')
- @test.idempotent_id('38731e7b-6d9d-4ffa-8fd1-193c453e88b1')
- def test_internal_db_job_binary_list(self):
- binary_info = self._create_job_binary(self.internal_db_job_binary)
-
- # check for job binary in list
- binaries = self.client.list_job_binaries()['binaries']
- binaries_info = [(binary['id'], binary['name']) for binary in binaries]
- self.assertIn(binary_info, binaries_info)
-
- @test.attr(type='smoke')
- @test.idempotent_id('1b32199b-c3f5-43e1-a37a-3797e57b7066')
- def test_internal_db_job_binary_get(self):
- binary_id, binary_name = (
- self._create_job_binary(self.internal_db_job_binary))
-
- # check job binary fetch by id
- binary = self.client.get_job_binary(binary_id)['job_binary']
- self.assertEqual(binary_name, binary['name'])
- self.assertDictContainsSubset(self.internal_db_job_binary, binary)
-
- @test.attr(type='smoke')
- @test.idempotent_id('3c42b0c3-3e03-46a5-adf0-df0650271a4e')
- def test_internal_db_job_binary_delete(self):
- binary_id, _ = self._create_job_binary(self.internal_db_job_binary)
-
- # delete the job binary by id
- self.client.delete_job_binary(binary_id)
-
- @test.attr(type='smoke')
- @test.idempotent_id('d5d47659-7e2c-4ea7-b292-5b3e559e8587')
- def test_job_binary_get_data(self):
- binary_id, _ = self._create_job_binary(self.internal_db_job_binary)
-
- # get data of job binary by id
- _, data = self.client.get_job_binary_data(binary_id)
- self.assertEqual(data, self.job_binary_data)
diff --git a/tempest/api/data_processing/test_job_binary_internals.py b/tempest/api/data_processing/test_job_binary_internals.py
deleted file mode 100644
index b4f0769..0000000
--- a/tempest/api/data_processing/test_job_binary_internals.py
+++ /dev/null
@@ -1,88 +0,0 @@
-# Copyright (c) 2014 Mirantis Inc.
-#
-# 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.data_processing import base as dp_base
-from tempest.common.utils import data_utils
-from tempest import test
-
-
-class JobBinaryInternalTest(dp_base.BaseDataProcessingTest):
- # Link to the API documentation is http://docs.openstack.org/developer/
- # sahara/restapi/rest_api_v1.1_EDP.html#job-binary-internals
-
- @classmethod
- def resource_setup(cls):
- super(JobBinaryInternalTest, cls).resource_setup()
- cls.job_binary_internal_data = 'Some script may be data'
-
- def _create_job_binary_internal(self, binary_name=None):
- """Creates Job Binary Internal with optional name specified.
-
- It puts data into Sahara database and ensures job binary internal name.
- Returns id and name of created job binary internal.
- """
- if not binary_name:
- # generate random name if it's not specified
- binary_name = data_utils.rand_name('sahara-job-binary-internal')
-
- # create job binary internal
- resp_body = (
- self.create_job_binary_internal(binary_name,
- self.job_binary_internal_data))
-
- # ensure that job binary internal created successfully
- self.assertEqual(binary_name, resp_body['name'])
-
- return resp_body['id'], binary_name
-
- @test.attr(type='smoke')
- @test.idempotent_id('249c4dc2-946f-4939-83e6-212ddb6ea0be')
- def test_job_binary_internal_create(self):
- self._create_job_binary_internal()
-
- @test.attr(type='smoke')
- @test.idempotent_id('1e3c2ecd-5673-499d-babe-4fe2fcdf64ee')
- def test_job_binary_internal_list(self):
- binary_info = self._create_job_binary_internal()
-
- # check for job binary internal in list
- binaries = self.client.list_job_binary_internals()['binaries']
- binaries_info = [(binary['id'], binary['name']) for binary in binaries]
- self.assertIn(binary_info, binaries_info)
-
- @test.attr(type='smoke')
- @test.idempotent_id('a2046a53-386c-43ab-be35-df54b19db776')
- def test_job_binary_internal_get(self):
- binary_id, binary_name = self._create_job_binary_internal()
-
- # check job binary internal fetch by id
- binary = self.client.get_job_binary_internal(binary_id)
- self.assertEqual(binary_name, binary['job_binary_internal']['name'])
-
- @test.attr(type='smoke')
- @test.idempotent_id('b3568c33-4eed-40d5-aae4-6ff3b2ac58f5')
- def test_job_binary_internal_delete(self):
- binary_id, _ = self._create_job_binary_internal()
-
- # delete the job binary internal by id
- self.client.delete_job_binary_internal(binary_id)
-
- @test.attr(type='smoke')
- @test.idempotent_id('8871f2b0-5782-4d66-9bb9-6f95bcb839ea')
- def test_job_binary_internal_get_data(self):
- binary_id, _ = self._create_job_binary_internal()
-
- # get data of job binary internal by id
- _, data = self.client.get_job_binary_internal_data(binary_id)
- self.assertEqual(data, self.job_binary_internal_data)
diff --git a/tempest/api/data_processing/test_jobs.py b/tempest/api/data_processing/test_jobs.py
deleted file mode 100644
index 8503320..0000000
--- a/tempest/api/data_processing/test_jobs.py
+++ /dev/null
@@ -1,93 +0,0 @@
-# Copyright (c) 2014 Mirantis Inc.
-#
-# 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.data_processing import base as dp_base
-from tempest.common.utils import data_utils
-from tempest import test
-
-
-class JobTest(dp_base.BaseDataProcessingTest):
- # NOTE: Link to the API documentation: http://docs.openstack.org/developer/
- # sahara/restapi/rest_api_v1.1_EDP.html#jobs
-
- @classmethod
- def resource_setup(cls):
- super(JobTest, cls).resource_setup()
- # create job binary
- job_binary = {
- 'name': data_utils.rand_name('sahara-job-binary'),
- 'url': 'swift://sahara-container.sahara/example.jar',
- 'description': 'Test job binary',
- 'extra': {
- 'user': cls.os.credentials.username,
- 'password': cls.os.credentials.password
- }
- }
- resp_body = cls.create_job_binary(**job_binary)
- job_binary_id = resp_body['id']
-
- cls.job = {
- 'job_type': 'Pig',
- 'mains': [job_binary_id]
- }
-
- def _create_job(self, job_name=None):
- """Creates Job with optional name specified.
-
- It creates job and ensures job name. Returns id and name of created
- job.
- """
- if not job_name:
- # generate random name if it's not specified
- job_name = data_utils.rand_name('sahara-job')
-
- # create job
- resp_body = self.create_job(job_name, **self.job)
-
- # ensure that job created successfully
- self.assertEqual(job_name, resp_body['name'])
-
- return resp_body['id'], job_name
-
- @test.attr(type='smoke')
- @test.idempotent_id('8cf785ca-adf4-473d-8281-fb9a5efa3073')
- def test_job_create(self):
- self._create_job()
-
- @test.attr(type='smoke')
- @test.idempotent_id('41e253fe-b02a-41a0-b186-5ff1f0463ba3')
- def test_job_list(self):
- job_info = self._create_job()
-
- # check for job in list
- jobs = self.client.list_jobs()['jobs']
- jobs_info = [(job['id'], job['name']) for job in jobs]
- self.assertIn(job_info, jobs_info)
-
- @test.attr(type='smoke')
- @test.idempotent_id('3faf17fa-bc94-4a60-b1c3-79e53674c16c')
- def test_job_get(self):
- job_id, job_name = self._create_job()
-
- # check job fetch by id
- job = self.client.get_job(job_id)['job']
- self.assertEqual(job_name, job['name'])
-
- @test.attr(type='smoke')
- @test.idempotent_id('dff85e62-7dda-4ad8-b1ee-850adecb0c6e')
- def test_job_delete(self):
- job_id, _ = self._create_job()
-
- # delete the job by id
- self.client.delete_job(job_id)
diff --git a/tempest/api/data_processing/test_node_group_templates.py b/tempest/api/data_processing/test_node_group_templates.py
deleted file mode 100644
index 388bb58..0000000
--- a/tempest/api/data_processing/test_node_group_templates.py
+++ /dev/null
@@ -1,90 +0,0 @@
-# Copyright (c) 2014 Mirantis Inc.
-#
-# 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.data_processing import base as dp_base
-from tempest.common.utils import data_utils
-from tempest import test
-
-
-class NodeGroupTemplateTest(dp_base.BaseDataProcessingTest):
-
- @classmethod
- def skip_checks(cls):
- super(NodeGroupTemplateTest, cls).skip_checks()
- if cls.default_plugin is None:
- raise cls.skipException("No Sahara plugins configured")
-
- @classmethod
- def resource_setup(cls):
- super(NodeGroupTemplateTest, cls).resource_setup()
-
- def _create_node_group_template(self, template_name=None):
- """Creates Node Group Template with optional name specified.
-
- It creates template, ensures template name and response body.
- Returns id and name of created template.
- """
- self.node_group_template = self.get_node_group_template()
- self.assertIsNotNone(self.node_group_template,
- "No known Sahara plugin was found")
-
- if not template_name:
- # generate random name if it's not specified
- template_name = data_utils.rand_name('sahara-ng-template')
-
- # create node group template
- resp_body = self.create_node_group_template(template_name,
- **self.node_group_template)
-
- # ensure that template created successfully
- self.assertEqual(template_name, resp_body['name'])
- self.assertDictContainsSubset(self.node_group_template, resp_body)
-
- return resp_body['id'], template_name
-
- @test.attr(type='smoke')
- @test.idempotent_id('63164051-e46d-4387-9741-302ef4791cbd')
- def test_node_group_template_create(self):
- self._create_node_group_template()
-
- @test.attr(type='smoke')
- @test.idempotent_id('eb39801d-2612-45e5-88b1-b5d70b329185')
- def test_node_group_template_list(self):
- template_info = self._create_node_group_template()
-
- # check for node group template in list
- templates = self.client.list_node_group_templates()
- templates = templates['node_group_templates']
- templates_info = [(template['id'], template['name'])
- for template in templates]
- self.assertIn(template_info, templates_info)
-
- @test.attr(type='smoke')
- @test.idempotent_id('6ee31539-a708-466f-9c26-4093ce09a836')
- def test_node_group_template_get(self):
- template_id, template_name = self._create_node_group_template()
-
- # check node group template fetch by id
- template = self.client.get_node_group_template(template_id)
- template = template['node_group_template']
- self.assertEqual(template_name, template['name'])
- self.assertDictContainsSubset(self.node_group_template, template)
-
- @test.attr(type='smoke')
- @test.idempotent_id('f4f5cb82-708d-4031-81c4-b0618a706a2f')
- def test_node_group_template_delete(self):
- template_id, _ = self._create_node_group_template()
-
- # delete the node group template by id
- self.client.delete_node_group_template(template_id)
diff --git a/tempest/api/data_processing/test_plugins.py b/tempest/api/data_processing/test_plugins.py
deleted file mode 100644
index 14594e4..0000000
--- a/tempest/api/data_processing/test_plugins.py
+++ /dev/null
@@ -1,56 +0,0 @@
-# Copyright (c) 2014 Mirantis Inc.
-#
-# 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.data_processing import base as dp_base
-from tempest import config
-from tempest import test
-
-CONF = config.CONF
-
-
-class PluginsTest(dp_base.BaseDataProcessingTest):
- def _list_all_plugin_names(self):
- """Returns all enabled plugin names.
-
- It ensures main plugins availability.
- """
- plugins = self.client.list_plugins()['plugins']
- plugins_names = [plugin['name'] for plugin in plugins]
- for enabled_plugin in CONF.data_processing_feature_enabled.plugins:
- self.assertIn(enabled_plugin, plugins_names)
-
- return plugins_names
-
- @test.attr(type='smoke')
- @test.idempotent_id('01a005a3-426c-4c0b-9617-d09475403e09')
- def test_plugin_list(self):
- self._list_all_plugin_names()
-
- @test.attr(type='smoke')
- @test.idempotent_id('53cf6487-2cfb-4a6f-8671-97c542c6e901')
- def test_plugin_get(self):
- for plugin_name in self._list_all_plugin_names():
- plugin = self.client.get_plugin(plugin_name)['plugin']
- self.assertEqual(plugin_name, plugin['name'])
-
- for plugin_version in plugin['versions']:
- detailed_plugin = self.client.get_plugin(plugin_name,
- plugin_version)
- detailed_plugin = detailed_plugin['plugin']
- self.assertEqual(plugin_name, detailed_plugin['name'])
-
- # check that required image tags contains name and version
- image_tags = detailed_plugin['required_image_tags']
- self.assertIn(plugin_name, image_tags)
- self.assertIn(plugin_version, image_tags)
diff --git a/tempest/api/database/__init__.py b/tempest/api/database/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/api/database/__init__.py
+++ /dev/null
diff --git a/tempest/api/database/base.py b/tempest/api/database/base.py
deleted file mode 100644
index f4c1881..0000000
--- a/tempest/api/database/base.py
+++ /dev/null
@@ -1,51 +0,0 @@
-# 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_log import log as logging
-
-from tempest import config
-import tempest.test
-
-CONF = config.CONF
-LOG = logging.getLogger(__name__)
-
-
-class BaseDatabaseTest(tempest.test.BaseTestCase):
- """Base test case class for all Database API tests."""
-
- credentials = ['primary']
-
- @classmethod
- def skip_checks(cls):
- super(BaseDatabaseTest, cls).skip_checks()
- if not CONF.service_available.trove:
- skip_msg = ("%s skipped as trove is not available" % cls.__name__)
- raise cls.skipException(skip_msg)
-
- @classmethod
- def setup_clients(cls):
- super(BaseDatabaseTest, cls).setup_clients()
- cls.database_flavors_client = cls.os.database_flavors_client
- cls.os_flavors_client = cls.os.flavors_client
- cls.database_limits_client = cls.os.database_limits_client
- cls.database_versions_client = cls.os.database_versions_client
-
- @classmethod
- def resource_setup(cls):
- super(BaseDatabaseTest, cls).resource_setup()
-
- cls.catalog_type = CONF.database.catalog_type
- cls.db_flavor_ref = CONF.database.db_flavor_ref
- cls.db_current_version = CONF.database.db_current_version
diff --git a/tempest/api/database/flavors/__init__.py b/tempest/api/database/flavors/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/api/database/flavors/__init__.py
+++ /dev/null
diff --git a/tempest/api/database/flavors/test_flavors.py b/tempest/api/database/flavors/test_flavors.py
deleted file mode 100644
index f75b867..0000000
--- a/tempest/api/database/flavors/test_flavors.py
+++ /dev/null
@@ -1,73 +0,0 @@
-# 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 tempest.api.database import base
-from tempest import test
-
-
-class DatabaseFlavorsTest(base.BaseDatabaseTest):
-
- @classmethod
- def setup_clients(cls):
- super(DatabaseFlavorsTest, cls).setup_clients()
- cls.client = cls.database_flavors_client
-
- @test.attr(type='smoke')
- @test.idempotent_id('c94b825e-0132-4686-8049-8a4a2bc09525')
- def test_get_db_flavor(self):
- # The expected flavor details should be returned
- flavor = (self.client.show_db_flavor(self.db_flavor_ref)
- ['flavor'])
- self.assertEqual(self.db_flavor_ref, str(flavor['id']))
- self.assertIn('ram', flavor)
- self.assertIn('links', flavor)
- self.assertIn('name', flavor)
-
- @test.attr(type='smoke')
- @test.idempotent_id('685025d6-0cec-4673-8a8d-995cb8e0d3bb')
- def test_list_db_flavors(self):
- flavor = (self.client.show_db_flavor(self.db_flavor_ref)
- ['flavor'])
- # List of all flavors should contain the expected flavor
- flavors = self.client.list_db_flavors()['flavors']
- self.assertIn(flavor, flavors)
-
- def _check_values(self, names, db_flavor, os_flavor, in_db=True):
- for name in names:
- self.assertIn(name, os_flavor)
- if in_db:
- self.assertIn(name, db_flavor)
- self.assertEqual(str(db_flavor[name]), str(os_flavor[name]),
- "DB flavor differs from OS on '%s' value"
- % name)
- else:
- self.assertNotIn(name, db_flavor)
-
- @test.attr(type='smoke')
- @test.idempotent_id('afb2667f-4ec2-4925-bcb7-313fdcffb80d')
- @test.services('compute')
- def test_compare_db_flavors_with_os(self):
- db_flavors = self.client.list_db_flavors()['flavors']
- os_flavors = (self.os_flavors_client.list_flavors(detail=True)
- ['flavors'])
- self.assertEqual(len(os_flavors), len(db_flavors),
- "OS flavors %s do not match DB flavors %s" %
- (os_flavors, db_flavors))
- for os_flavor in os_flavors:
- db_flavor =\
- self.client.show_db_flavor(os_flavor['id'])['flavor']
- self._check_values(['id', 'name', 'ram'], db_flavor, os_flavor)
- self._check_values(['disk', 'vcpus', 'swap'], db_flavor, os_flavor,
- in_db=False)
diff --git a/tempest/api/database/flavors/test_flavors_negative.py b/tempest/api/database/flavors/test_flavors_negative.py
deleted file mode 100644
index 3dee96f..0000000
--- a/tempest/api/database/flavors/test_flavors_negative.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# 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 tempest_lib import exceptions as lib_exc
-
-from tempest.api.database import base
-from tempest import test
-
-
-class DatabaseFlavorsNegativeTest(base.BaseDatabaseTest):
-
- @classmethod
- def setup_clients(cls):
- super(DatabaseFlavorsNegativeTest, cls).setup_clients()
- cls.client = cls.database_flavors_client
-
- @test.attr(type=['negative'])
- @test.idempotent_id('f8e7b721-373f-4a64-8e9c-5327e975af3e')
- def test_get_non_existent_db_flavor(self):
- # flavor details are not returned for non-existent flavors
- self.assertRaises(lib_exc.NotFound,
- self.client.show_db_flavor, -1)
diff --git a/tempest/api/database/limits/__init__.py b/tempest/api/database/limits/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/api/database/limits/__init__.py
+++ /dev/null
diff --git a/tempest/api/database/limits/test_limits.py b/tempest/api/database/limits/test_limits.py
deleted file mode 100644
index ee51b1d..0000000
--- a/tempest/api/database/limits/test_limits.py
+++ /dev/null
@@ -1,45 +0,0 @@
-# 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 tempest.api.database import base
-from tempest import test
-
-
-class DatabaseLimitsTest(base.BaseDatabaseTest):
-
- @classmethod
- def resource_setup(cls):
- super(DatabaseLimitsTest, cls).resource_setup()
- cls.client = cls.database_limits_client
-
- @test.attr(type='smoke')
- @test.idempotent_id('73024538-f316-4829-b3e9-b459290e137a')
- def test_absolute_limits(self):
- # Test to verify if all absolute limit parameters are
- # present when verb is ABSOLUTE
- limits = self.client.list_db_limits()['limits']
- expected_abs_limits = ['max_backups', 'max_volumes',
- 'max_instances', 'verb']
- absolute_limit = [l for l in limits
- if l['verb'] == 'ABSOLUTE']
- self.assertEqual(1, len(absolute_limit), "One ABSOLUTE limit "
- "verb is allowed. Fetched %s"
- % len(absolute_limit))
- actual_abs_limits = absolute_limit[0].keys()
- missing_abs_limit = set(expected_abs_limits) - set(actual_abs_limits)
- self.assertEmpty(missing_abs_limit,
- "Failed to find the following absolute limit(s)"
- " in a fetched list: %s" %
- ', '.join(str(a) for a in missing_abs_limit))
diff --git a/tempest/api/database/versions/__init__.py b/tempest/api/database/versions/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/api/database/versions/__init__.py
+++ /dev/null
diff --git a/tempest/api/database/versions/test_versions.py b/tempest/api/database/versions/test_versions.py
deleted file mode 100644
index ae568b1..0000000
--- a/tempest/api/database/versions/test_versions.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# 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 tempest.api.database import base
-from tempest import test
-
-
-class DatabaseVersionsTest(base.BaseDatabaseTest):
-
- @classmethod
- def setup_clients(cls):
- super(DatabaseVersionsTest, cls).setup_clients()
- cls.client = cls.database_versions_client
-
- @test.attr(type='smoke')
- @test.idempotent_id('6952cd77-90cd-4dca-bb60-8e2c797940cf')
- def test_list_db_versions(self):
- versions = self.client.list_db_versions()['versions']
- self.assertTrue(len(versions) > 0, "No database versions found")
- # List of all versions should contain the current version, and there
- # should only be one 'current' version
- current_versions = list()
- for version in versions:
- if 'CURRENT' == version['status']:
- current_versions.append(version['id'])
- self.assertEqual(1, len(current_versions))
- self.assertIn(self.db_current_version, current_versions)
diff --git a/tempest/api/identity/admin/v2/test_endpoints.py b/tempest/api/identity/admin/v2/test_endpoints.py
index bff4f91..947706e 100644
--- a/tempest/api/identity/admin/v2/test_endpoints.py
+++ b/tempest/api/identity/admin/v2/test_endpoints.py
@@ -14,8 +14,8 @@
# under the License.
from tempest.api.identity import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
class EndPointsTestJSON(base.BaseIdentityV2AdminTest):
@@ -23,67 +23,64 @@
@classmethod
def resource_setup(cls):
super(EndPointsTestJSON, cls).resource_setup()
- cls.service_ids = list()
s_name = data_utils.rand_name('service')
s_type = data_utils.rand_name('type')
s_description = data_utils.rand_name('description')
- cls.service_data = cls.client.create_service(
- s_name, s_type, description=s_description)['OS-KSADM:service']
- cls.service_id = cls.service_data['id']
- cls.service_ids.append(cls.service_id)
+ service_data = cls.services_client.create_service(
+ name=s_name, type=s_type,
+ description=s_description)['OS-KSADM:service']
+ cls.addClassResourceCleanup(cls.services_client.delete_service,
+ service_data['id'])
+ cls.service_id = service_data['id']
# Create endpoints so as to use for LIST and GET test cases
cls.setup_endpoints = list()
- for i in range(2):
+ for _ in range(2):
region = data_utils.rand_name('region')
url = data_utils.rand_url()
- endpoint = cls.client.create_endpoint(cls.service_id,
- region,
- publicurl=url,
- adminurl=url,
- internalurl=url)['endpoint']
+ endpoint = cls.endpoints_client.create_endpoint(
+ service_id=cls.service_id,
+ region=region,
+ publicurl=url,
+ adminurl=url,
+ internalurl=url)['endpoint']
+ cls.addClassResourceCleanup(cls.endpoints_client.delete_endpoint,
+ endpoint['id'])
# list_endpoints() will return 'enabled' field
endpoint['enabled'] = True
cls.setup_endpoints.append(endpoint)
- @classmethod
- def resource_cleanup(cls):
- for e in cls.setup_endpoints:
- cls.client.delete_endpoint(e['id'])
- for s in cls.service_ids:
- cls.client.delete_service(s)
- super(EndPointsTestJSON, cls).resource_cleanup()
-
- @test.idempotent_id('11f590eb-59d8-4067-8b2b-980c7f387f51')
+ @decorators.idempotent_id('11f590eb-59d8-4067-8b2b-980c7f387f51')
def test_list_endpoints(self):
# Get a list of endpoints
- fetched_endpoints = self.client.list_endpoints()['endpoints']
+ fetched_endpoints = self.endpoints_client.list_endpoints()['endpoints']
# Asserting LIST endpoints
missing_endpoints =\
[e for e in self.setup_endpoints if e not in fetched_endpoints]
- self.assertEqual(0, len(missing_endpoints),
+ self.assertEmpty(missing_endpoints,
"Failed to find endpoint %s in fetched list" %
', '.join(str(e) for e in missing_endpoints))
- @test.idempotent_id('9974530a-aa28-4362-8403-f06db02b26c1')
+ @decorators.idempotent_id('9974530a-aa28-4362-8403-f06db02b26c1')
def test_create_list_delete_endpoint(self):
region = data_utils.rand_name('region')
url = data_utils.rand_url()
- endpoint = self.client.create_endpoint(self.service_id,
- region,
- publicurl=url,
- adminurl=url,
- internalurl=url)['endpoint']
+ endpoint = self.endpoints_client.create_endpoint(
+ service_id=self.service_id,
+ region=region,
+ publicurl=url,
+ adminurl=url,
+ internalurl=url)['endpoint']
# Asserting Create Endpoint response body
self.assertIn('id', endpoint)
self.assertEqual(region, endpoint['region'])
self.assertEqual(url, endpoint['publicurl'])
# Checking if created endpoint is present in the list of endpoints
- fetched_endpoints = self.client.list_endpoints()['endpoints']
+ fetched_endpoints = self.endpoints_client.list_endpoints()['endpoints']
fetched_endpoints_id = [e['id'] for e in fetched_endpoints]
self.assertIn(endpoint['id'], fetched_endpoints_id)
# Deleting the endpoint created in this method
- self.client.delete_endpoint(endpoint['id'])
+ self.endpoints_client.delete_endpoint(endpoint['id'])
# Checking whether endpoint is deleted successfully
- fetched_endpoints = self.client.list_endpoints()['endpoints']
+ fetched_endpoints = self.endpoints_client.list_endpoints()['endpoints']
fetched_endpoints_id = [e['id'] for e in fetched_endpoints]
self.assertNotIn(endpoint['id'], fetched_endpoints_id)
diff --git a/tempest/api/identity/admin/v2/test_roles.py b/tempest/api/identity/admin/v2/test_roles.py
index 3f3d16e..9736a76 100644
--- a/tempest/api/identity/admin/v2/test_roles.py
+++ b/tempest/api/identity/admin/v2/test_roles.py
@@ -13,11 +13,10 @@
# License for the specific language governing permissions and limitations
# under the License.
-from six import moves
-
from tempest.api.identity import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
class RolesTestJSON(base.BaseIdentityV2AdminTest):
@@ -25,17 +24,19 @@
@classmethod
def resource_setup(cls):
super(RolesTestJSON, cls).resource_setup()
- for _ in moves.xrange(5):
+ cls.roles = list()
+ for _ in range(5):
role_name = data_utils.rand_name(name='role')
role = cls.roles_client.create_role(name=role_name)['role']
- cls.data.roles.append(role)
+ cls.addClassResourceCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ cls.roles_client.delete_role, role['id'])
+ cls.roles.append(role)
def _get_role_params(self):
- self.data.setup_test_user()
- self.data.setup_test_role()
- user = self.get_user_by_name(self.data.test_user)
- tenant = self.get_tenant_by_name(self.data.test_tenant)
- role = self.get_role_by_name(self.data.test_role)
+ user = self.setup_test_user()
+ tenant = self.tenants_client.show_tenant(user['tenantId'])['tenant']
+ role = self.setup_test_role()
return (user, tenant, role)
def assert_role_in_role_list(self, role, roles):
@@ -45,67 +46,71 @@
found = True
self.assertTrue(found, "assigned role was not in list")
- @test.idempotent_id('75d9593f-50b7-4fcf-bd64-e3fb4a278e23')
+ @decorators.idempotent_id('75d9593f-50b7-4fcf-bd64-e3fb4a278e23')
def test_list_roles(self):
"""Return a list of all roles."""
body = self.roles_client.list_roles()['roles']
- found = [role for role in body if role in self.data.roles]
- self.assertTrue(any(found))
- self.assertEqual(len(found), len(self.data.roles))
+ found = [role for role in body if role in self.roles]
+ self.assertNotEmpty(found)
+ self.assertEqual(len(found), len(self.roles))
- @test.idempotent_id('c62d909d-6c21-48c0-ae40-0a0760e6db5e')
+ @decorators.idempotent_id('c62d909d-6c21-48c0-ae40-0a0760e6db5e')
def test_role_create_delete(self):
"""Role should be created, verified, and deleted."""
role_name = data_utils.rand_name(name='role-test')
body = self.roles_client.create_role(name=role_name)['role']
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.roles_client.delete_role, body['id'])
self.assertEqual(role_name, body['name'])
body = self.roles_client.list_roles()['roles']
found = [role for role in body if role['name'] == role_name]
- self.assertTrue(any(found))
+ self.assertNotEmpty(found)
body = self.roles_client.delete_role(found[0]['id'])
body = self.roles_client.list_roles()['roles']
found = [role for role in body if role['name'] == role_name]
- self.assertFalse(any(found))
+ self.assertEmpty(found)
- @test.idempotent_id('db6870bd-a6ed-43be-a9b1-2f10a5c9994f')
+ @decorators.idempotent_id('db6870bd-a6ed-43be-a9b1-2f10a5c9994f')
def test_get_role_by_id(self):
"""Get a role by its id."""
- self.data.setup_test_role()
- role_id = self.data.role['id']
- role_name = self.data.role['name']
+ role = self.setup_test_role()
+ role_id = role['id']
+ role_name = role['name']
body = self.roles_client.show_role(role_id)['role']
self.assertEqual(role_id, body['id'])
self.assertEqual(role_name, body['name'])
- @test.idempotent_id('0146f675-ffbd-4208-b3a4-60eb628dbc5e')
+ @decorators.idempotent_id('0146f675-ffbd-4208-b3a4-60eb628dbc5e')
def test_assign_user_role(self):
"""Assign a role to a user on a tenant."""
(user, tenant, role) = self._get_role_params()
- self.roles_client.assign_user_role(tenant['id'], user['id'],
- role['id'])
- roles = self.roles_client.list_user_roles(tenant['id'],
- user['id'])['roles']
+ self.roles_client.create_user_role_on_project(tenant['id'],
+ user['id'],
+ role['id'])
+ roles = self.roles_client.list_user_roles_on_project(
+ tenant['id'], user['id'])['roles']
self.assert_role_in_role_list(role, roles)
- @test.idempotent_id('f0b9292c-d3ba-4082-aa6c-440489beef69')
+ @decorators.idempotent_id('f0b9292c-d3ba-4082-aa6c-440489beef69')
def test_remove_user_role(self):
"""Remove a role assigned to a user on a tenant."""
(user, tenant, role) = self._get_role_params()
- user_role = self.roles_client.assign_user_role(tenant['id'],
- user['id'],
- role['id'])['role']
- self.roles_client.delete_user_role(tenant['id'], user['id'],
- user_role['id'])
+ user_role = self.roles_client.create_user_role_on_project(
+ tenant['id'], user['id'], role['id'])['role']
+ self.roles_client.delete_role_from_user_on_project(tenant['id'],
+ user['id'],
+ user_role['id'])
- @test.idempotent_id('262e1e3e-ed71-4edd-a0e5-d64e83d66d05')
+ @decorators.idempotent_id('262e1e3e-ed71-4edd-a0e5-d64e83d66d05')
def test_list_user_roles(self):
"""List roles assigned to a user on tenant."""
(user, tenant, role) = self._get_role_params()
- self.roles_client.assign_user_role(tenant['id'], user['id'],
- role['id'])
- roles = self.roles_client.list_user_roles(tenant['id'],
- user['id'])['roles']
+ self.roles_client.create_user_role_on_project(tenant['id'],
+ user['id'],
+ role['id'])
+ roles = self.roles_client.list_user_roles_on_project(
+ tenant['id'], user['id'])['roles']
self.assert_role_in_role_list(role, roles)
diff --git a/tempest/api/identity/admin/v2/test_roles_negative.py b/tempest/api/identity/admin/v2/test_roles_negative.py
index c9af7c6..f3b7494 100644
--- a/tempest/api/identity/admin/v2/test_roles_negative.py
+++ b/tempest/api/identity/admin/v2/test_roles_negative.py
@@ -13,34 +13,29 @@
# License for the specific language governing permissions and limitations
# under the License.
-import uuid
-
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.identity import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class RolesNegativeTestJSON(base.BaseIdentityV2AdminTest):
def _get_role_params(self):
- self.data.setup_test_user()
- self.data.setup_test_role()
- user = self.get_user_by_name(self.data.test_user)
- tenant = self.get_tenant_by_name(self.data.test_tenant)
- role = self.get_role_by_name(self.data.test_role)
+ user = self.setup_test_user()
+ tenant = self.tenants_client.show_tenant(user['tenantId'])['tenant']
+ role = self.setup_test_role()
return (user, tenant, role)
- @test.attr(type=['negative'])
- @test.idempotent_id('d5d5f1df-f8ca-4de0-b2ef-259c1cc67025')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('d5d5f1df-f8ca-4de0-b2ef-259c1cc67025')
def test_list_roles_by_unauthorized_user(self):
# Non-administrator user should not be able to list roles
self.assertRaises(lib_exc.Forbidden,
self.non_admin_roles_client.list_roles)
- @test.attr(type=['negative'])
- @test.idempotent_id('11a3c7da-df6c-40c2-abc2-badd682edf9f')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('11a3c7da-df6c-40c2-abc2-badd682edf9f')
def test_list_roles_request_without_token(self):
# Request to list roles without a valid token should fail
token = self.client.auth_provider.get_token()
@@ -48,15 +43,15 @@
self.assertRaises(lib_exc.Unauthorized, self.roles_client.list_roles)
self.client.auth_provider.clear_auth()
- @test.attr(type=['negative'])
- @test.idempotent_id('c0b89e56-accc-4c73-85f8-9c0f866104c1')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('c0b89e56-accc-4c73-85f8-9c0f866104c1')
def test_role_create_blank_name(self):
# Should not be able to create a role with a blank name
self.assertRaises(lib_exc.BadRequest, self.roles_client.create_role,
name='')
- @test.attr(type=['negative'])
- @test.idempotent_id('585c8998-a8a4-4641-a5dd-abef7a8ced00')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('585c8998-a8a4-4641-a5dd-abef7a8ced00')
def test_create_role_by_unauthorized_user(self):
# Non-administrator user should not be able to create role
role_name = data_utils.rand_name(name='role')
@@ -64,8 +59,8 @@
self.non_admin_roles_client.create_role,
name=role_name)
- @test.attr(type=['negative'])
- @test.idempotent_id('a7edd17a-e34a-4aab-8bb7-fa6f498645b8')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('a7edd17a-e34a-4aab-8bb7-fa6f498645b8')
def test_create_role_request_without_token(self):
# Request to create role without a valid token should fail
token = self.client.auth_provider.get_token()
@@ -75,8 +70,8 @@
self.roles_client.create_role, name=role_name)
self.client.auth_provider.clear_auth()
- @test.attr(type=['negative'])
- @test.idempotent_id('c0cde2c8-81c1-4bb0-8fe2-cf615a3547a8')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('c0cde2c8-81c1-4bb0-8fe2-cf615a3547a8')
def test_role_create_duplicate(self):
# Role names should be unique
role_name = data_utils.rand_name(name='role-dup')
@@ -86,24 +81,24 @@
self.assertRaises(lib_exc.Conflict, self.roles_client.create_role,
name=role_name)
- @test.attr(type=['negative'])
- @test.idempotent_id('15347635-b5b1-4a87-a280-deb2bd6d865e')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('15347635-b5b1-4a87-a280-deb2bd6d865e')
def test_delete_role_by_unauthorized_user(self):
# Non-administrator user should not be able to delete role
role_name = data_utils.rand_name(name='role')
body = self.roles_client.create_role(name=role_name)['role']
- self.data.roles.append(body)
+ self.addCleanup(self.roles_client.delete_role, body['id'])
role_id = body.get('id')
self.assertRaises(lib_exc.Forbidden,
self.non_admin_roles_client.delete_role, role_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('44b60b20-70de-4dac-beaf-a3fc2650a16b')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('44b60b20-70de-4dac-beaf-a3fc2650a16b')
def test_delete_role_request_without_token(self):
# Request to delete role without a valid token should fail
role_name = data_utils.rand_name(name='role')
body = self.roles_client.create_role(name=role_name)['role']
- self.data.roles.append(body)
+ self.addCleanup(self.roles_client.delete_role, body['id'])
role_id = body.get('id')
token = self.client.auth_provider.get_token()
self.client.delete_token(token)
@@ -112,138 +107,150 @@
role_id)
self.client.auth_provider.clear_auth()
- @test.attr(type=['negative'])
- @test.idempotent_id('38373691-8551-453a-b074-4260ad8298ef')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('38373691-8551-453a-b074-4260ad8298ef')
def test_delete_role_non_existent(self):
# Attempt to delete a non existent role should fail
- non_existent_role = str(uuid.uuid4().hex)
+ non_existent_role = data_utils.rand_uuid_hex()
self.assertRaises(lib_exc.NotFound, self.roles_client.delete_role,
non_existent_role)
- @test.attr(type=['negative'])
- @test.idempotent_id('391df5cf-3ec3-46c9-bbe5-5cb58dd4dc41')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('391df5cf-3ec3-46c9-bbe5-5cb58dd4dc41')
def test_assign_user_role_by_unauthorized_user(self):
# Non-administrator user should not be authorized to
# assign a role to user
(user, tenant, role) = self._get_role_params()
- self.assertRaises(lib_exc.Forbidden,
- self.non_admin_roles_client.assign_user_role,
- tenant['id'], user['id'], role['id'])
+ self.assertRaises(
+ lib_exc.Forbidden,
+ self.non_admin_roles_client.create_user_role_on_project,
+ tenant['id'], user['id'], role['id'])
- @test.attr(type=['negative'])
- @test.idempotent_id('f0d2683c-5603-4aee-95d7-21420e87cfd8')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('f0d2683c-5603-4aee-95d7-21420e87cfd8')
def test_assign_user_role_request_without_token(self):
# Request to assign a role to a user without a valid token
(user, tenant, role) = self._get_role_params()
token = self.client.auth_provider.get_token()
self.client.delete_token(token)
- self.assertRaises(lib_exc.Unauthorized,
- self.roles_client.assign_user_role, tenant['id'],
- user['id'], role['id'])
+ self.assertRaises(
+ lib_exc.Unauthorized,
+ self.roles_client.create_user_role_on_project, tenant['id'],
+ user['id'], role['id'])
self.client.auth_provider.clear_auth()
- @test.attr(type=['negative'])
- @test.idempotent_id('99b297f6-2b5d-47c7-97a9-8b6bb4f91042')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('99b297f6-2b5d-47c7-97a9-8b6bb4f91042')
def test_assign_user_role_for_non_existent_role(self):
# Attempt to assign a non existent role to user should fail
- (user, tenant, role) = self._get_role_params()
- non_existent_role = str(uuid.uuid4().hex)
- self.assertRaises(lib_exc.NotFound, self.roles_client.assign_user_role,
+ (user, tenant, _) = self._get_role_params()
+ non_existent_role = data_utils.rand_uuid_hex()
+ self.assertRaises(lib_exc.NotFound,
+ self.roles_client.create_user_role_on_project,
tenant['id'], user['id'], non_existent_role)
- @test.attr(type=['negative'])
- @test.idempotent_id('b2285aaa-9e76-4704-93a9-7a8acd0a6c8f')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('b2285aaa-9e76-4704-93a9-7a8acd0a6c8f')
def test_assign_user_role_for_non_existent_tenant(self):
# Attempt to assign a role on a non existent tenant should fail
- (user, tenant, role) = self._get_role_params()
- non_existent_tenant = str(uuid.uuid4().hex)
- self.assertRaises(lib_exc.NotFound, self.roles_client.assign_user_role,
+ (user, _, role) = self._get_role_params()
+ non_existent_tenant = data_utils.rand_uuid_hex()
+ self.assertRaises(lib_exc.NotFound,
+ self.roles_client.create_user_role_on_project,
non_existent_tenant, user['id'], role['id'])
- @test.attr(type=['negative'])
- @test.idempotent_id('5c3132cd-c4c8-4402-b5ea-71eb44e97793')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('5c3132cd-c4c8-4402-b5ea-71eb44e97793')
def test_assign_duplicate_user_role(self):
# Duplicate user role should not get assigned
(user, tenant, role) = self._get_role_params()
- self.roles_client.assign_user_role(tenant['id'], user['id'],
- role['id'])
- self.assertRaises(lib_exc.Conflict, self.roles_client.assign_user_role,
+ self.roles_client.create_user_role_on_project(tenant['id'],
+ user['id'],
+ role['id'])
+ self.assertRaises(lib_exc.Conflict,
+ self.roles_client.create_user_role_on_project,
tenant['id'], user['id'], role['id'])
- @test.attr(type=['negative'])
- @test.idempotent_id('d0537987-0977-448f-a435-904c15de7298')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('d0537987-0977-448f-a435-904c15de7298')
def test_remove_user_role_by_unauthorized_user(self):
# Non-administrator user should not be authorized to
# remove a user's role
(user, tenant, role) = self._get_role_params()
- self.roles_client.assign_user_role(tenant['id'],
- user['id'],
- role['id'])
- self.assertRaises(lib_exc.Forbidden,
- self.non_admin_roles_client.delete_user_role,
- tenant['id'], user['id'], role['id'])
+ self.roles_client.create_user_role_on_project(tenant['id'],
+ user['id'],
+ role['id'])
+ self.assertRaises(
+ lib_exc.Forbidden,
+ self.non_admin_roles_client.delete_role_from_user_on_project,
+ tenant['id'], user['id'], role['id'])
- @test.attr(type=['negative'])
- @test.idempotent_id('cac81cf4-c1d2-47dc-90d3-f2b7eb572286')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('cac81cf4-c1d2-47dc-90d3-f2b7eb572286')
def test_remove_user_role_request_without_token(self):
# Request to remove a user's role without a valid token
(user, tenant, role) = self._get_role_params()
- self.roles_client.assign_user_role(tenant['id'],
- user['id'],
- role['id'])
+ self.roles_client.create_user_role_on_project(tenant['id'],
+ user['id'],
+ role['id'])
token = self.client.auth_provider.get_token()
self.client.delete_token(token)
self.assertRaises(lib_exc.Unauthorized,
- self.roles_client.delete_user_role, tenant['id'],
- user['id'], role['id'])
+ self.roles_client.delete_role_from_user_on_project,
+ tenant['id'], user['id'], role['id'])
self.client.auth_provider.clear_auth()
- @test.attr(type=['negative'])
- @test.idempotent_id('ab32d759-cd16-41f1-a86e-44405fa9f6d2')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('ab32d759-cd16-41f1-a86e-44405fa9f6d2')
def test_remove_user_role_non_existent_role(self):
# Attempt to delete a non existent role from a user should fail
(user, tenant, role) = self._get_role_params()
- self.roles_client.assign_user_role(tenant['id'],
- user['id'],
- role['id'])
- non_existent_role = str(uuid.uuid4().hex)
- self.assertRaises(lib_exc.NotFound, self.roles_client.delete_user_role,
+ self.roles_client.create_user_role_on_project(tenant['id'],
+ user['id'],
+ role['id'])
+ non_existent_role = data_utils.rand_uuid_hex()
+ self.assertRaises(lib_exc.NotFound,
+ self.roles_client.delete_role_from_user_on_project,
tenant['id'], user['id'], non_existent_role)
- @test.attr(type=['negative'])
- @test.idempotent_id('67a679ec-03dd-4551-bbfc-d1c93284f023')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('67a679ec-03dd-4551-bbfc-d1c93284f023')
def test_remove_user_role_non_existent_tenant(self):
# Attempt to remove a role from a non existent tenant should fail
(user, tenant, role) = self._get_role_params()
- self.roles_client.assign_user_role(tenant['id'],
- user['id'],
- role['id'])
- non_existent_tenant = str(uuid.uuid4().hex)
- self.assertRaises(lib_exc.NotFound, self.roles_client.delete_user_role,
+ self.roles_client.create_user_role_on_project(tenant['id'],
+ user['id'],
+ role['id'])
+ non_existent_tenant = data_utils.rand_uuid_hex()
+ self.assertRaises(lib_exc.NotFound,
+ self.roles_client.delete_role_from_user_on_project,
non_existent_tenant, user['id'], role['id'])
- @test.attr(type=['negative'])
- @test.idempotent_id('7391ab4c-06f3-477a-a64a-c8e55ce89837')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('7391ab4c-06f3-477a-a64a-c8e55ce89837')
def test_list_user_roles_by_unauthorized_user(self):
# Non-administrator user should not be authorized to list
# a user's roles
(user, tenant, role) = self._get_role_params()
- self.roles_client.assign_user_role(tenant['id'], user['id'],
- role['id'])
- self.assertRaises(lib_exc.Forbidden,
- self.non_admin_roles_client.list_user_roles,
- tenant['id'], user['id'])
+ self.roles_client.create_user_role_on_project(tenant['id'],
+ user['id'],
+ role['id'])
+ self.assertRaises(
+ lib_exc.Forbidden,
+ self.non_admin_roles_client.list_user_roles_on_project,
+ tenant['id'], user['id'])
- @test.attr(type=['negative'])
- @test.idempotent_id('682adfb2-fd5f-4b0a-a9ca-322e9bebb907')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('682adfb2-fd5f-4b0a-a9ca-322e9bebb907')
def test_list_user_roles_request_without_token(self):
# Request to list user's roles without a valid token should fail
- (user, tenant, role) = self._get_role_params()
+ (user, tenant, _) = self._get_role_params()
token = self.client.auth_provider.get_token()
self.client.delete_token(token)
try:
self.assertRaises(lib_exc.Unauthorized,
- self.roles_client.list_user_roles, tenant['id'],
+ self.roles_client.list_user_roles_on_project,
+ tenant['id'],
user['id'])
finally:
self.client.auth_provider.clear_auth()
diff --git a/tempest/api/identity/admin/v2/test_services.py b/tempest/api/identity/admin/v2/test_services.py
index 04e15d2..e2ed5ef 100644
--- a/tempest/api/identity/admin/v2/test_services.py
+++ b/tempest/api/identity/admin/v2/test_services.py
@@ -13,45 +13,44 @@
# License for the specific language governing permissions and limitations
# under the License.
-from six import moves
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.identity import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class ServicesTestJSON(base.BaseIdentityV2AdminTest):
def _del_service(self, service_id):
# Deleting the service created in this method
- self.client.delete_service(service_id)
+ self.services_client.delete_service(service_id)
# Checking whether service is deleted successfully
- self.assertRaises(lib_exc.NotFound, self.client.show_service,
+ self.assertRaises(lib_exc.NotFound, self.services_client.show_service,
service_id)
- @test.idempotent_id('84521085-c6e6-491c-9a08-ec9f70f90110')
+ @decorators.idempotent_id('84521085-c6e6-491c-9a08-ec9f70f90110')
def test_create_get_delete_service(self):
# GET Service
# Creating a Service
name = data_utils.rand_name('service')
- type = data_utils.rand_name('type')
+ s_type = data_utils.rand_name('type')
description = data_utils.rand_name('description')
- service_data = self.client.create_service(
- name, type, description=description)['OS-KSADM:service']
- self.assertFalse(service_data['id'] is None)
+ service_data = self.services_client.create_service(
+ name=name, type=s_type,
+ description=description)['OS-KSADM:service']
+ self.assertIsNotNone(service_data['id'])
self.addCleanup(self._del_service, service_data['id'])
# Verifying response body of create service
- self.assertIn('id', service_data)
self.assertIn('name', service_data)
self.assertEqual(name, service_data['name'])
self.assertIn('type', service_data)
- self.assertEqual(type, service_data['type'])
+ self.assertEqual(s_type, service_data['type'])
self.assertIn('description', service_data)
self.assertEqual(description, service_data['description'])
# Get service
- fetched_service = (self.client.show_service(service_data['id'])
- ['OS-KSADM:service'])
+ fetched_service = (
+ self.services_client.show_service(service_data['id'])
+ ['OS-KSADM:service'])
# verifying the existence of service created
self.assertIn('id', fetched_service)
self.assertEqual(fetched_service['id'], service_data['id'])
@@ -63,39 +62,42 @@
self.assertEqual(fetched_service['description'],
service_data['description'])
- @test.idempotent_id('5d3252c8-e555-494b-a6c8-e11d7335da42')
+ @decorators.idempotent_id('5d3252c8-e555-494b-a6c8-e11d7335da42')
def test_create_service_without_description(self):
# Create a service only with name and type
name = data_utils.rand_name('service')
- type = data_utils.rand_name('type')
- service = self.client.create_service(name, type)['OS-KSADM:service']
+ s_type = data_utils.rand_name('type')
+ service = self.services_client.create_service(
+ name=name, type=s_type)['OS-KSADM:service']
self.assertIn('id', service)
self.addCleanup(self._del_service, service['id'])
self.assertIn('name', service)
self.assertEqual(name, service['name'])
self.assertIn('type', service)
- self.assertEqual(type, service['type'])
+ self.assertEqual(s_type, service['type'])
- @test.attr(type='smoke')
- @test.idempotent_id('34ea6489-012d-4a86-9038-1287cadd5eca')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('34ea6489-012d-4a86-9038-1287cadd5eca')
def test_list_services(self):
# Create, List, Verify and Delete Services
services = []
- for _ in moves.xrange(3):
+ for _ in range(3):
name = data_utils.rand_name('service')
- type = data_utils.rand_name('type')
+ s_type = data_utils.rand_name('type')
description = data_utils.rand_name('description')
- service = self.client.create_service(
- name, type, description=description)['OS-KSADM:service']
+
+ service = self.services_client.create_service(
+ name=name, type=s_type,
+ description=description)['OS-KSADM:service']
services.append(service)
- service_ids = map(lambda x: x['id'], services)
+ service_ids = [svc['id'] for svc in services]
def delete_services():
for service_id in service_ids:
- self.client.delete_service(service_id)
+ self.services_client.delete_service(service_id)
self.addCleanup(delete_services)
# List and Verify Services
- body = self.client.list_services()['OS-KSADM:services']
+ body = self.services_client.list_services()['OS-KSADM:services']
found = [serv for serv in body if serv['id'] in service_ids]
self.assertEqual(len(found), len(services), 'Services not found')
diff --git a/tempest/api/identity/admin/v2/test_tenant_negative.py b/tempest/api/identity/admin/v2/test_tenant_negative.py
index a02dbc1..49bb949 100644
--- a/tempest/api/identity/admin/v2/test_tenant_negative.py
+++ b/tempest/api/identity/admin/v2/test_tenant_negative.py
@@ -13,26 +13,23 @@
# License for the specific language governing permissions and limitations
# under the License.
-import uuid
-
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.identity import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class TenantsNegativeTestJSON(base.BaseIdentityV2AdminTest):
- @test.attr(type=['negative'])
- @test.idempotent_id('ca9bb202-63dd-4240-8a07-8ef9c19c04bb')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('ca9bb202-63dd-4240-8a07-8ef9c19c04bb')
def test_list_tenants_by_unauthorized_user(self):
# Non-administrator user should not be able to list tenants
self.assertRaises(lib_exc.Forbidden,
self.non_admin_tenants_client.list_tenants)
- @test.attr(type=['negative'])
- @test.idempotent_id('df33926c-1c96-4d8d-a762-79cc6b0c3cf4')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('df33926c-1c96-4d8d-a762-79cc6b0c3cf4')
def test_list_tenant_request_without_token(self):
# Request to list tenants without a valid token should fail
token = self.client.auth_provider.get_token()
@@ -41,24 +38,20 @@
self.tenants_client.list_tenants)
self.client.auth_provider.clear_auth()
- @test.attr(type=['negative'])
- @test.idempotent_id('162ba316-f18b-4987-8c0c-fd9140cd63ed')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('162ba316-f18b-4987-8c0c-fd9140cd63ed')
def test_tenant_delete_by_unauthorized_user(self):
# Non-administrator user should not be able to delete a tenant
- tenant_name = data_utils.rand_name(name='tenant')
- tenant = self.tenants_client.create_tenant(tenant_name)['tenant']
- self.data.tenants.append(tenant)
+ tenant = self.setup_test_tenant()
self.assertRaises(lib_exc.Forbidden,
self.non_admin_tenants_client.delete_tenant,
tenant['id'])
- @test.attr(type=['negative'])
- @test.idempotent_id('e450db62-2e9d-418f-893a-54772d6386b1')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('e450db62-2e9d-418f-893a-54772d6386b1')
def test_tenant_delete_request_without_token(self):
# Request to delete a tenant without a valid token should fail
- tenant_name = data_utils.rand_name(name='tenant')
- tenant = self.tenants_client.create_tenant(tenant_name)['tenant']
- self.data.tenants.append(tenant)
+ tenant = self.setup_test_tenant()
token = self.client.auth_provider.get_token()
self.client.delete_token(token)
self.assertRaises(lib_exc.Unauthorized,
@@ -66,39 +59,33 @@
tenant['id'])
self.client.auth_provider.clear_auth()
- @test.attr(type=['negative'])
- @test.idempotent_id('9c9a2aed-6e3c-467a-8f5c-89da9d1b516b')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('9c9a2aed-6e3c-467a-8f5c-89da9d1b516b')
def test_delete_non_existent_tenant(self):
# Attempt to delete a non existent tenant should fail
self.assertRaises(lib_exc.NotFound, self.tenants_client.delete_tenant,
- str(uuid.uuid4().hex))
+ data_utils.rand_uuid_hex())
- @test.attr(type=['negative'])
- @test.idempotent_id('af16f44b-a849-46cb-9f13-a751c388f739')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('af16f44b-a849-46cb-9f13-a751c388f739')
def test_tenant_create_duplicate(self):
# Tenant names should be unique
tenant_name = data_utils.rand_name(name='tenant')
- body = self.tenants_client.create_tenant(tenant_name)['tenant']
- tenant = body
- self.data.tenants.append(tenant)
- tenant1_id = body.get('id')
-
- self.addCleanup(self.tenants_client.delete_tenant, tenant1_id)
- self.addCleanup(self.data.tenants.remove, tenant)
+ self.setup_test_tenant(name=tenant_name)
self.assertRaises(lib_exc.Conflict, self.tenants_client.create_tenant,
- tenant_name)
+ name=tenant_name)
- @test.attr(type=['negative'])
- @test.idempotent_id('d26b278a-6389-4702-8d6e-5980d80137e0')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('d26b278a-6389-4702-8d6e-5980d80137e0')
def test_create_tenant_by_unauthorized_user(self):
# Non-administrator user should not be authorized to create a tenant
tenant_name = data_utils.rand_name(name='tenant')
self.assertRaises(lib_exc.Forbidden,
self.non_admin_tenants_client.create_tenant,
- tenant_name)
+ name=tenant_name)
- @test.attr(type=['negative'])
- @test.idempotent_id('a3ee9d7e-6920-4dd5-9321-d4b2b7f0a638')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('a3ee9d7e-6920-4dd5-9321-d4b2b7f0a638')
def test_create_tenant_request_without_token(self):
# Create tenant request without a token should not be authorized
tenant_name = data_utils.rand_name(name='tenant')
@@ -106,51 +93,47 @@
self.client.delete_token(token)
self.assertRaises(lib_exc.Unauthorized,
self.tenants_client.create_tenant,
- tenant_name)
+ name=tenant_name)
self.client.auth_provider.clear_auth()
- @test.attr(type=['negative'])
- @test.idempotent_id('5a2e4ca9-b0c0-486c-9c48-64a94fba2395')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('5a2e4ca9-b0c0-486c-9c48-64a94fba2395')
def test_create_tenant_with_empty_name(self):
# Tenant name should not be empty
self.assertRaises(lib_exc.BadRequest,
self.tenants_client.create_tenant,
name='')
- @test.attr(type=['negative'])
- @test.idempotent_id('2ff18d1e-dfe3-4359-9dc3-abf582c196b9')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('2ff18d1e-dfe3-4359-9dc3-abf582c196b9')
def test_create_tenants_name_length_over_64(self):
# Tenant name length should not be greater than 64 characters
tenant_name = 'a' * 65
self.assertRaises(lib_exc.BadRequest,
self.tenants_client.create_tenant,
- tenant_name)
+ name=tenant_name)
- @test.attr(type=['negative'])
- @test.idempotent_id('bd20dc2a-9557-4db7-b755-f48d952ad706')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('bd20dc2a-9557-4db7-b755-f48d952ad706')
def test_update_non_existent_tenant(self):
# Attempt to update a non existent tenant should fail
self.assertRaises(lib_exc.NotFound, self.tenants_client.update_tenant,
- str(uuid.uuid4().hex))
+ data_utils.rand_uuid_hex())
- @test.attr(type=['negative'])
- @test.idempotent_id('41704dc5-c5f7-4f79-abfa-76e6fedc570b')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('41704dc5-c5f7-4f79-abfa-76e6fedc570b')
def test_tenant_update_by_unauthorized_user(self):
# Non-administrator user should not be able to update a tenant
- tenant_name = data_utils.rand_name(name='tenant')
- tenant = self.tenants_client.create_tenant(tenant_name)['tenant']
- self.data.tenants.append(tenant)
+ tenant = self.setup_test_tenant()
self.assertRaises(lib_exc.Forbidden,
self.non_admin_tenants_client.update_tenant,
tenant['id'])
- @test.attr(type=['negative'])
- @test.idempotent_id('7a421573-72c7-4c22-a98e-ce539219c657')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('7a421573-72c7-4c22-a98e-ce539219c657')
def test_tenant_update_request_without_token(self):
# Request to update a tenant without a valid token should fail
- tenant_name = data_utils.rand_name(name='tenant')
- tenant = self.tenants_client.create_tenant(tenant_name)['tenant']
- self.data.tenants.append(tenant)
+ tenant = self.setup_test_tenant()
token = self.client.auth_provider.get_token()
self.client.delete_token(token)
self.assertRaises(lib_exc.Unauthorized,
diff --git a/tempest/api/identity/admin/v2/test_tenants.py b/tempest/api/identity/admin/v2/test_tenants.py
index 8d0b9b1..cda721c 100644
--- a/tempest/api/identity/admin/v2/test_tenants.py
+++ b/tempest/api/identity/admin/v2/test_tenants.py
@@ -13,46 +13,37 @@
# License for the specific language governing permissions and limitations
# under the License.
-from six import moves
-
from tempest.api.identity import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
class TenantsTestJSON(base.BaseIdentityV2AdminTest):
- @test.idempotent_id('16c6e05c-6112-4b0e-b83f-5e43f221b6b0')
+ @decorators.idempotent_id('16c6e05c-6112-4b0e-b83f-5e43f221b6b0')
def test_tenant_list_delete(self):
# Create several tenants and delete them
tenants = []
- for _ in moves.xrange(3):
- tenant_name = data_utils.rand_name(name='tenant-new')
- tenant = self.tenants_client.create_tenant(tenant_name)['tenant']
- self.data.tenants.append(tenant)
+ for _ in range(3):
+ tenant = self.setup_test_tenant()
tenants.append(tenant)
- tenant_ids = map(lambda x: x['id'], tenants)
+ tenant_ids = [tn['id'] for tn in tenants]
body = self.tenants_client.list_tenants()['tenants']
found = [t for t in body if t['id'] in tenant_ids]
self.assertEqual(len(found), len(tenants), 'Tenants not created')
for tenant in tenants:
self.tenants_client.delete_tenant(tenant['id'])
- self.data.tenants.remove(tenant)
body = self.tenants_client.list_tenants()['tenants']
found = [tenant for tenant in body if tenant['id'] in tenant_ids]
- self.assertFalse(any(found), 'Tenants failed to delete')
+ self.assertEmpty(found, 'Tenants failed to delete')
- @test.idempotent_id('d25e9f24-1310-4d29-b61b-d91299c21d6d')
+ @decorators.idempotent_id('d25e9f24-1310-4d29-b61b-d91299c21d6d')
def test_tenant_create_with_description(self):
# Create tenant with a description
- tenant_name = data_utils.rand_name(name='tenant')
tenant_desc = data_utils.rand_name(name='desc')
- body = self.tenants_client.create_tenant(tenant_name,
- description=tenant_desc)
- tenant = body['tenant']
- self.data.tenants.append(tenant)
+ tenant = self.setup_test_tenant(description=tenant_desc)
tenant_id = tenant['id']
desc1 = tenant['description']
self.assertEqual(desc1, tenant_desc, 'Description should have '
@@ -62,52 +53,36 @@
self.assertEqual(desc2, tenant_desc, 'Description does not appear'
'to be set')
self.tenants_client.delete_tenant(tenant_id)
- self.data.tenants.remove(tenant)
- @test.idempotent_id('670bdddc-1cd7-41c7-b8e2-751cfb67df50')
+ @decorators.idempotent_id('670bdddc-1cd7-41c7-b8e2-751cfb67df50')
def test_tenant_create_enabled(self):
# Create a tenant that is enabled
- tenant_name = data_utils.rand_name(name='tenant')
- body = self.tenants_client.create_tenant(tenant_name, enabled=True)
- tenant = body['tenant']
- self.data.tenants.append(tenant)
+ 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)
- self.data.tenants.remove(tenant)
- @test.idempotent_id('3be22093-b30f-499d-b772-38340e5e16fb')
+ @decorators.idempotent_id('3be22093-b30f-499d-b772-38340e5e16fb')
def test_tenant_create_not_enabled(self):
# Create a tenant that is not enabled
- tenant_name = data_utils.rand_name(name='tenant')
- body = self.tenants_client.create_tenant(tenant_name, enabled=False)
- tenant = body['tenant']
- self.data.tenants.append(tenant)
+ 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)
- self.data.tenants.remove(tenant)
- @test.idempotent_id('781f2266-d128-47f3-8bdb-f70970add238')
+ @decorators.idempotent_id('781f2266-d128-47f3-8bdb-f70970add238')
def test_tenant_update_name(self):
# Update name attribute of a tenant
t_name1 = data_utils.rand_name(name='tenant')
- body = self.tenants_client.create_tenant(t_name1)['tenant']
- tenant = body
- self.data.tenants.append(tenant)
-
- t_id = body['id']
- resp1_name = body['name']
+ tenant = self.setup_test_tenant(name=t_name1)
+ t_id = tenant['id']
+ resp1_name = tenant['name']
t_name2 = data_utils.rand_name(name='tenant2')
body = self.tenants_client.update_tenant(t_id, name=t_name2)['tenant']
@@ -122,17 +97,12 @@
self.assertEqual(resp2_name, resp3_name)
self.tenants_client.delete_tenant(t_id)
- self.data.tenants.remove(tenant)
- @test.idempotent_id('859fcfe1-3a03-41ef-86f9-b19a47d1cd87')
+ @decorators.idempotent_id('859fcfe1-3a03-41ef-86f9-b19a47d1cd87')
def test_tenant_update_desc(self):
# Update description attribute of a tenant
- t_name = data_utils.rand_name(name='tenant')
t_desc = data_utils.rand_name(name='desc')
- body = self.tenants_client.create_tenant(t_name, description=t_desc)
- tenant = body['tenant']
- self.data.tenants.append(tenant)
-
+ tenant = self.setup_test_tenant(description=t_desc)
t_id = tenant['id']
resp1_desc = tenant['description']
@@ -150,17 +120,12 @@
self.assertEqual(resp2_desc, resp3_desc)
self.tenants_client.delete_tenant(t_id)
- self.data.tenants.remove(tenant)
- @test.idempotent_id('8fc8981f-f12d-4c66-9972-2bdcf2bc2e1a')
+ @decorators.idempotent_id('8fc8981f-f12d-4c66-9972-2bdcf2bc2e1a')
def test_tenant_update_enable(self):
# Update the enabled attribute of a tenant
- t_name = data_utils.rand_name(name='tenant')
t_en = False
- body = self.tenants_client.create_tenant(t_name, enabled=t_en)
- tenant = body['tenant']
- self.data.tenants.append(tenant)
-
+ tenant = self.setup_test_tenant(enabled=t_en)
t_id = tenant['id']
resp1_en = tenant['enabled']
@@ -174,8 +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)
- self.data.tenants.remove(tenant)
diff --git a/tempest/api/identity/admin/v2/test_tokens.py b/tempest/api/identity/admin/v2/test_tokens.py
index fdb5a5a..6ce1a8b 100644
--- a/tempest/api/identity/admin/v2/test_tokens.py
+++ b/tempest/api/identity/admin/v2/test_tokens.py
@@ -14,25 +14,28 @@
# under the License.
from tempest.api.identity import base
-from tempest.common.utils import data_utils
-from tempest import test
+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 TokensTestJSON(base.BaseIdentityV2AdminTest):
- @test.idempotent_id('453ad4d5-e486-4b2f-be72-cffc8149e586')
- def test_create_get_delete_token(self):
+ @decorators.idempotent_id('453ad4d5-e486-4b2f-be72-cffc8149e586')
+ def test_create_check_get_delete_token(self):
# get a token by username and password
user_name = data_utils.rand_name(name='user')
user_password = data_utils.rand_password()
# first:create a tenant
- tenant_name = data_utils.rand_name(name='tenant')
- tenant = self.tenants_client.create_tenant(tenant_name)['tenant']
- self.data.tenants.append(tenant)
+ tenant = self.setup_test_tenant()
# second:create a user
- user = self.client.create_user(user_name, user_password,
- tenant['id'], '')['user']
- self.data.users.append(user)
+ user = self.create_test_user(name=user_name,
+ password=user_password,
+ tenantId=tenant['id'],
+ email='')
# then get a token for the user
body = self.token_client.auth(user_name,
user_password,
@@ -41,6 +44,7 @@
tenant['name'])
# Perform GET Token
token_id = body['token']['id']
+ self.client.check_token_existence(token_id)
token_details = self.client.show_token(token_id)['access']
self.assertEqual(token_id, token_details['token']['id'])
self.assertEqual(user['id'], token_details['user']['id'])
@@ -49,8 +53,11 @@
token_details['token']['tenant']['name'])
# then delete the token
self.client.delete_token(token_id)
+ self.assertRaises(lib_exc.NotFound,
+ self.client.check_token_existence,
+ token_id)
- @test.idempotent_id('25ba82ee-8a32-4ceb-8f50-8b8c71e8765e')
+ @decorators.idempotent_id('25ba82ee-8a32-4ceb-8f50-8b8c71e8765e')
def test_rescope_token(self):
"""An unscoped token can be requested
@@ -61,31 +68,29 @@
user_name = data_utils.rand_name(name='user')
user_password = data_utils.rand_password()
tenant_id = None # No default tenant so will get unscoped token.
- email = ''
- user = self.client.create_user(user_name, user_password,
- tenant_id, email)['user']
- self.data.users.append(user)
+ user = self.create_test_user(name=user_name,
+ password=user_password,
+ tenantId=tenant_id,
+ email='')
# Create a couple tenants.
tenant1_name = data_utils.rand_name(name='tenant')
- tenant1 = self.tenants_client.create_tenant(tenant1_name)['tenant']
- self.data.tenants.append(tenant1)
+ tenant1 = self.setup_test_tenant(name=tenant1_name)
tenant2_name = data_utils.rand_name(name='tenant')
- tenant2 = self.tenants_client.create_tenant(tenant2_name)['tenant']
- self.data.tenants.append(tenant2)
+ tenant2 = self.setup_test_tenant(name=tenant2_name)
# Create a role
- role_name = data_utils.rand_name(name='role')
- role = self.roles_client.create_role(name=role_name)['role']
- self.data.roles.append(role)
+ role = self.setup_test_role()
# Grant the user the role on the tenants.
- self.roles_client.assign_user_role(tenant1['id'], user['id'],
- role['id'])
+ self.roles_client.create_user_role_on_project(tenant1['id'],
+ user['id'],
+ role['id'])
- self.roles_client.assign_user_role(tenant2['id'], user['id'],
- role['id'])
+ self.roles_client.create_user_role_on_project(tenant2['id'],
+ user['id'],
+ role['id'])
# Get an unscoped token.
body = self.token_client.auth(user_name, user_password)
@@ -104,3 +109,28 @@
# Use the unscoped token to get a token scoped to tenant2
body = self.token_client.auth_token(token_id,
tenant=tenant2_name)
+
+ @decorators.idempotent_id('ca3ea6f7-ed08-4a61-adbd-96906456ad31')
+ def test_list_endpoints_for_token(self):
+ tempest_services = ['keystone', 'nova', 'neutron', 'swift', 'cinder',
+ 'neutron']
+ # get a token for the user
+ creds = self.os_primary.credentials
+ username = creds.username
+ password = creds.password
+ tenant_name = creds.tenant_name
+ token = self.token_client.auth(username,
+ password,
+ tenant_name)['token']
+ endpoints = self.client.list_endpoints_for_token(
+ token['id'])['endpoints']
+ self.assertIsInstance(endpoints, list)
+ # Store list of service names
+ service_names = [e['name'] for e in endpoints]
+ # Get the list of available services. Keystone is always available.
+ available_services = [s[0] for s in list(
+ CONF.service_available.items()) if s[1] is True] + ['keystone']
+ # Verify that all available services are present.
+ for service in tempest_services:
+ if service in available_services:
+ self.assertIn(service, service_names)
diff --git a/tempest/api/identity/admin/v2/test_tokens_negative.py b/tempest/api/identity/admin/v2/test_tokens_negative.py
new file mode 100644
index 0000000..eb3e365
--- /dev/null
+++ b/tempest/api/identity/admin/v2/test_tokens_negative.py
@@ -0,0 +1,38 @@
+# Copyright 2017 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 tempest.api.identity import base
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+
+class TokensAdminTestNegative(base.BaseIdentityV2AdminTest):
+
+ credentials = ['primary', 'admin', 'alt']
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('a0a0a600-4292-4364-99c5-922c834fdf05')
+ def test_check_token_existence_negative(self):
+ creds = self.os_primary.credentials
+ creds_alt = self.os_alt.credentials
+ username = creds.username
+ password = creds.password
+ tenant_name = creds.tenant_name
+ alt_tenant_name = creds_alt.tenant_name
+ body = self.token_client.auth(username, password, tenant_name)
+ self.assertRaises(lib_exc.Unauthorized,
+ self.client.check_token_existence,
+ body['token']['id'],
+ belongsTo=alt_tenant_name)
diff --git a/tempest/api/identity/admin/v2/test_users.py b/tempest/api/identity/admin/v2/test_users.py
index d886524..0d98af5 100644
--- a/tempest/api/identity/admin/v2/test_users.py
+++ b/tempest/api/identity/admin/v2/test_users.py
@@ -13,11 +13,13 @@
# License for the specific language governing permissions and limitations
# under the License.
+import time
+
from testtools import matchers
from tempest.api.identity import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
class UsersTestJSON(base.BaseIdentityV2AdminTest):
@@ -26,187 +28,166 @@
def resource_setup(cls):
super(UsersTestJSON, cls).resource_setup()
cls.alt_user = data_utils.rand_name('test_user')
- cls.alt_password = data_utils.rand_password()
cls.alt_email = cls.alt_user + '@testmail.tm'
- @test.attr(type='smoke')
- @test.idempotent_id('2d55a71e-da1d-4b43-9c03-d269fd93d905')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('2d55a71e-da1d-4b43-9c03-d269fd93d905')
def test_create_user(self):
# Create a user
- self.data.setup_test_tenant()
- user = self.client.create_user(self.alt_user, self.alt_password,
- self.data.tenant['id'],
- self.alt_email)['user']
- self.data.users.append(user)
+ tenant = self.setup_test_tenant()
+ user = self.create_test_user(name=self.alt_user, tenantId=tenant['id'])
self.assertEqual(self.alt_user, user['name'])
- @test.idempotent_id('89d9fdb8-15c2-4304-a429-48715d0af33d')
+ @decorators.idempotent_id('89d9fdb8-15c2-4304-a429-48715d0af33d')
def test_create_user_with_enabled(self):
# Create a user with enabled : False
- self.data.setup_test_tenant()
+ tenant = self.setup_test_tenant()
name = data_utils.rand_name('test_user')
- user = self.client.create_user(name, self.alt_password,
- self.data.tenant['id'],
- self.alt_email, enabled=False)['user']
- self.data.users.append(user)
+ user = self.create_test_user(name=name,
+ tenantId=tenant['id'],
+ email=self.alt_email,
+ enabled=False)
self.assertEqual(name, user['name'])
self.assertEqual(False, user['enabled'])
self.assertEqual(self.alt_email, user['email'])
- @test.idempotent_id('39d05857-e8a5-4ed4-ba83-0b52d3ab97ee')
+ @decorators.idempotent_id('39d05857-e8a5-4ed4-ba83-0b52d3ab97ee')
def test_update_user(self):
# Test case to check if updating of user attributes is successful.
- test_user = data_utils.rand_name('test_user')
- self.data.setup_test_tenant()
- user = self.client.create_user(test_user, self.alt_password,
- self.data.tenant['id'],
- self.alt_email)['user']
- # Delete the User at the end of this method
- self.addCleanup(self.client.delete_user, user['id'])
+ tenant = self.setup_test_tenant()
+ user = self.create_test_user(tenantId=tenant['id'])
+
# Updating user details with new values
u_name2 = data_utils.rand_name('user2')
u_email2 = u_name2 + '@testmail.tm'
- update_user = self.client.update_user(user['id'], name=u_name2,
- email=u_email2,
- enabled=False)['user']
+ update_user = self.users_client.update_user(user['id'], name=u_name2,
+ email=u_email2,
+ enabled=False)['user']
self.assertEqual(u_name2, update_user['name'])
self.assertEqual(u_email2, update_user['email'])
self.assertEqual(False, update_user['enabled'])
# GET by id after updating
- updated_user = self.client.show_user(user['id'])['user']
+ updated_user = self.users_client.show_user(user['id'])['user']
# Assert response body of GET after updating
self.assertEqual(u_name2, updated_user['name'])
self.assertEqual(u_email2, updated_user['email'])
self.assertEqual(False, update_user['enabled'])
- @test.idempotent_id('29ed26f4-a74e-4425-9a85-fdb49fa269d2')
+ @decorators.idempotent_id('29ed26f4-a74e-4425-9a85-fdb49fa269d2')
def test_delete_user(self):
# Delete a user
- test_user = data_utils.rand_name('test_user')
- self.data.setup_test_tenant()
- user = self.client.create_user(test_user, self.alt_password,
- self.data.tenant['id'],
- self.alt_email)['user']
- self.client.delete_user(user['id'])
+ tenant = self.setup_test_tenant()
+ user = self.create_test_user(tenantId=tenant['id'])
+ self.users_client.delete_user(user['id'])
- @test.idempotent_id('aca696c3-d645-4f45-b728-63646045beb1')
+ @decorators.idempotent_id('aca696c3-d645-4f45-b728-63646045beb1')
def test_user_authentication(self):
# Valid user's token is authenticated
- self.data.setup_test_user()
+ password = data_utils.rand_password()
+ user = self.setup_test_user(password)
+ tenant = self.tenants_client.show_tenant(user['tenantId'])['tenant']
# Get a token
- self.token_client.auth(self.data.test_user, self.data.test_password,
- self.data.test_tenant)
+ self.token_client.auth(user['name'],
+ password,
+ tenant['name'])
# Re-auth
- self.token_client.auth(self.data.test_user,
- self.data.test_password,
- self.data.test_tenant)
+ self.token_client.auth(user['name'],
+ password,
+ tenant['name'])
- @test.idempotent_id('5d1fa498-4c2d-4732-a8fe-2b054598cfdd')
+ @decorators.idempotent_id('5d1fa498-4c2d-4732-a8fe-2b054598cfdd')
def test_authentication_request_without_token(self):
# Request for token authentication with a valid token in header
- self.data.setup_test_user()
- self.token_client.auth(self.data.test_user, self.data.test_password,
- self.data.test_tenant)
+ password = data_utils.rand_password()
+ user = self.setup_test_user(password)
+ tenant = self.tenants_client.show_tenant(user['tenantId'])['tenant']
+ self.token_client.auth(user['name'],
+ password,
+ tenant['name'])
# Get the token of the current client
token = self.client.auth_provider.get_token()
# Delete the token from database
self.client.delete_token(token)
# Re-auth
- self.token_client.auth(self.data.test_user,
- self.data.test_password,
- self.data.test_tenant)
+ self.token_client.auth(user['name'],
+ password,
+ tenant['name'])
self.client.auth_provider.clear_auth()
- @test.idempotent_id('a149c02e-e5e0-4b89-809e-7e8faf33ccda')
+ @decorators.idempotent_id('a149c02e-e5e0-4b89-809e-7e8faf33ccda')
def test_get_users(self):
# Get a list of users and find the test user
- self.data.setup_test_user()
- users = self.client.list_users()['users']
+ user = self.setup_test_user()
+ users = self.users_client.list_users()['users']
self.assertThat([u['name'] for u in users],
- matchers.Contains(self.data.test_user),
- "Could not find %s" % self.data.test_user)
+ matchers.Contains(user['name']),
+ "Could not find %s" % user['name'])
- @test.idempotent_id('6e317209-383a-4bed-9f10-075b7c82c79a')
+ @decorators.idempotent_id('6e317209-383a-4bed-9f10-075b7c82c79a')
def test_list_users_for_tenant(self):
# Return a list of all users for a tenant
- self.data.setup_test_tenant()
+ tenant = self.setup_test_tenant()
user_ids = list()
fetched_user_ids = list()
- password1 = data_utils.rand_password()
- alt_tenant_user1 = data_utils.rand_name('tenant_user1')
- user1 = self.client.create_user(alt_tenant_user1, password1,
- self.data.tenant['id'],
- 'user1@123')['user']
+ user1 = self.create_test_user(tenantId=tenant['id'])
user_ids.append(user1['id'])
- self.data.users.append(user1)
- password2 = data_utils.rand_password()
- alt_tenant_user2 = data_utils.rand_name('tenant_user2')
- user2 = self.client.create_user(alt_tenant_user2, password2,
- self.data.tenant['id'],
- 'user2@123')['user']
+ user2 = self.create_test_user(tenantId=tenant['id'])
user_ids.append(user2['id'])
- self.data.users.append(user2)
# List of users for the respective tenant ID
- body = (self.tenants_client.list_tenant_users(self.data.tenant['id'])
+ body = (self.tenants_client.list_tenant_users(tenant['id'])
['users'])
for i in body:
fetched_user_ids.append(i['id'])
# verifying the user Id in the list
missing_users =\
[user for user in user_ids if user not in fetched_user_ids]
- self.assertEqual(0, len(missing_users),
+ self.assertEmpty(missing_users,
"Failed to find user %s in fetched list" %
', '.join(m_user for m_user in missing_users))
- @test.idempotent_id('a8b54974-40e1-41c0-b812-50fc90827971')
+ @decorators.idempotent_id('a8b54974-40e1-41c0-b812-50fc90827971')
def test_list_users_with_roles_for_tenant(self):
# Return list of users on tenant when roles are assigned to users
- self.data.setup_test_user()
- self.data.setup_test_role()
- user = self.get_user_by_name(self.data.test_user)
- tenant = self.get_tenant_by_name(self.data.test_tenant)
- role = self.get_role_by_name(self.data.test_role)
+ user = self.setup_test_user()
+ tenant = self.tenants_client.show_tenant(user['tenantId'])['tenant']
+ role = self.setup_test_role()
# Assigning roles to two users
user_ids = list()
fetched_user_ids = list()
user_ids.append(user['id'])
- role = self.roles_client.assign_user_role(tenant['id'], user['id'],
- role['id'])['role']
+ role = self.roles_client.create_user_role_on_project(
+ tenant['id'], user['id'], role['id'])['role']
- alt_user2 = data_utils.rand_name('second_user')
- alt_password2 = data_utils.rand_password()
- second_user = self.client.create_user(alt_user2, alt_password2,
- self.data.tenant['id'],
- 'user2@123')['user']
+ second_user = self.create_test_user(tenantId=tenant['id'])
user_ids.append(second_user['id'])
- self.data.users.append(second_user)
- role = self.roles_client.assign_user_role(tenant['id'],
- second_user['id'],
- role['id'])['role']
+ role = self.roles_client.create_user_role_on_project(
+ tenant['id'], second_user['id'], role['id'])['role']
# List of users with roles for the respective tenant ID
- body = (self.tenants_client.list_tenant_users(self.data.tenant['id'])
- ['users'])
+ body = (self.tenants_client.list_tenant_users(tenant['id'])['users'])
for i in body:
fetched_user_ids.append(i['id'])
# verifying the user Id in the list
missing_users = [missing_user for missing_user in user_ids
if missing_user not in fetched_user_ids]
- self.assertEqual(0, len(missing_users),
+ self.assertEmpty(missing_users,
"Failed to find user %s in fetched list" %
', '.join(m_user for m_user in missing_users))
- @test.idempotent_id('1aeb25ac-6ec5-4d8b-97cb-7ac3567a989f')
+ @decorators.idempotent_id('1aeb25ac-6ec5-4d8b-97cb-7ac3567a989f')
def test_update_user_password(self):
# Test case to check if updating of user password is successful.
- self.data.setup_test_user()
+ user = self.setup_test_user()
+ tenant = self.tenants_client.show_tenant(user['tenantId'])['tenant']
# Updating the user with new password
new_pass = data_utils.rand_password()
- update_user = self.client.update_user_password(
- self.data.user['id'], password=new_pass)['user']
- self.assertEqual(update_user['id'], self.data.user['id'])
-
- # Validate the updated password
- # Get a token
- body = self.token_client.auth(self.data.test_user, new_pass,
- self.data.test_tenant)
- self.assertTrue('id' in body['token'])
+ update_user = self.users_client.update_user_password(
+ user['id'], password=new_pass)['user']
+ self.assertEqual(update_user['id'], user['id'])
+ # NOTE(morganfainberg): Fernet tokens are not subsecond aware and
+ # Keystone should only be precise to the second. Sleep to ensure
+ # we are passing the second boundary.
+ time.sleep(1)
+ # Validate the updated password through getting a token.
+ body = self.token_client.auth(user['name'], new_pass,
+ tenant['name'])
+ self.assertIn('id', body['token'])
diff --git a/tempest/api/identity/admin/v2/test_users_negative.py b/tempest/api/identity/admin/v2/test_users_negative.py
index 8fa5a36..4f47e41 100644
--- a/tempest/api/identity/admin/v2/test_users_negative.py
+++ b/tempest/api/identity/admin/v2/test_users_negative.py
@@ -13,13 +13,10 @@
# License for the specific language governing permissions and limitations
# under the License.
-import uuid
-
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.identity import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class UsersNegativeTestJSON(base.BaseIdentityV2AdminTest):
@@ -31,89 +28,100 @@
cls.alt_password = data_utils.rand_password()
cls.alt_email = cls.alt_user + '@testmail.tm'
- @test.attr(type=['negative'])
- @test.idempotent_id('60a1f5fa-5744-4cdf-82bf-60b7de2d29a4')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('60a1f5fa-5744-4cdf-82bf-60b7de2d29a4')
def test_create_user_by_unauthorized_user(self):
# Non-administrator should not be authorized to create a user
- self.data.setup_test_tenant()
+ tenant = self.setup_test_tenant()
self.assertRaises(lib_exc.Forbidden,
- self.non_admin_client.create_user, self.alt_user,
- self.alt_password, self.data.tenant['id'],
- self.alt_email)
+ self.non_admin_users_client.create_user,
+ name=self.alt_user, password=self.alt_password,
+ tenantId=tenant['id'],
+ email=self.alt_email)
- @test.attr(type=['negative'])
- @test.idempotent_id('d80d0c2f-4514-4d1e-806d-0930dfc5a187')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('d80d0c2f-4514-4d1e-806d-0930dfc5a187')
def test_create_user_with_empty_name(self):
# User with an empty name should not be created
- self.data.setup_test_tenant()
- self.assertRaises(lib_exc.BadRequest, self.client.create_user, '',
- self.alt_password, self.data.tenant['id'],
- self.alt_email)
+ tenant = self.setup_test_tenant()
+ self.assertRaises(lib_exc.BadRequest, self.users_client.create_user,
+ name='', password=self.alt_password,
+ tenantId=tenant['id'],
+ email=self.alt_email)
- @test.attr(type=['negative'])
- @test.idempotent_id('7704b4f3-3b75-4b82-87cc-931d41c8f780')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('7704b4f3-3b75-4b82-87cc-931d41c8f780')
def test_create_user_with_name_length_over_255(self):
# Length of user name filed should be restricted to 255 characters
- self.data.setup_test_tenant()
- self.assertRaises(lib_exc.BadRequest, self.client.create_user,
- 'a' * 256, self.alt_password,
- self.data.tenant['id'], self.alt_email)
+ tenant = self.setup_test_tenant()
+ self.assertRaises(lib_exc.BadRequest, self.users_client.create_user,
+ name='a' * 256, password=self.alt_password,
+ tenantId=tenant['id'],
+ email=self.alt_email)
- @test.attr(type=['negative'])
- @test.idempotent_id('57ae8558-120c-4723-9308-3751474e7ecf')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('57ae8558-120c-4723-9308-3751474e7ecf')
def test_create_user_with_duplicate_name(self):
# Duplicate user should not be created
- self.data.setup_test_user()
- self.assertRaises(lib_exc.Conflict, self.client.create_user,
- self.data.test_user, self.data.test_password,
- self.data.tenant['id'], self.data.test_email)
+ password = data_utils.rand_password()
+ user = self.setup_test_user(password)
+ tenant = self.tenants_client.show_tenant(user['tenantId'])['tenant']
+ self.assertRaises(lib_exc.Conflict, self.users_client.create_user,
+ name=user['name'],
+ password=password,
+ tenantId=tenant['id'],
+ email=user['email'])
- @test.attr(type=['negative'])
- @test.idempotent_id('0132cc22-7c4f-42e1-9e50-ac6aad31d59a')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('0132cc22-7c4f-42e1-9e50-ac6aad31d59a')
def test_create_user_for_non_existent_tenant(self):
# Attempt to create a user in a non-existent tenant should fail
- self.assertRaises(lib_exc.NotFound, self.client.create_user,
- self.alt_user, self.alt_password, '49ffgg99999',
- self.alt_email)
+ self.assertRaises(lib_exc.NotFound, self.users_client.create_user,
+ name=self.alt_user,
+ password=self.alt_password,
+ tenantId='49ffgg99999',
+ email=self.alt_email)
- @test.attr(type=['negative'])
- @test.idempotent_id('55bbb103-d1ae-437b-989b-bcdf8175c1f4')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('55bbb103-d1ae-437b-989b-bcdf8175c1f4')
def test_create_user_request_without_a_token(self):
# Request to create a user without a valid token should fail
- self.data.setup_test_tenant()
+ tenant = self.setup_test_tenant()
# Get the token of the current client
token = self.client.auth_provider.get_token()
# Delete the token from database
self.client.delete_token(token)
- self.assertRaises(lib_exc.Unauthorized, self.client.create_user,
- self.alt_user, self.alt_password,
- self.data.tenant['id'], self.alt_email)
# Unset the token to allow further tests to generate a new token
- self.client.auth_provider.clear_auth()
+ self.addCleanup(self.client.auth_provider.clear_auth)
- @test.attr(type=['negative'])
- @test.idempotent_id('23a2f3da-4a1a-41da-abdd-632328a861ad')
+ self.assertRaises(lib_exc.Unauthorized, self.users_client.create_user,
+ name=self.alt_user, password=self.alt_password,
+ tenantId=tenant['id'],
+ email=self.alt_email)
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('23a2f3da-4a1a-41da-abdd-632328a861ad')
def test_create_user_with_enabled_non_bool(self):
# Attempt to create a user with valid enabled para should fail
- self.data.setup_test_tenant()
+ tenant = self.setup_test_tenant()
name = data_utils.rand_name('test_user')
- self.assertRaises(lib_exc.BadRequest, self.client.create_user,
- name, self.alt_password,
- self.data.tenant['id'],
- self.alt_email, enabled=3)
+ self.assertRaises(lib_exc.BadRequest, self.users_client.create_user,
+ name=name, password=self.alt_password,
+ tenantId=tenant['id'],
+ email=self.alt_email, enabled=3)
- @test.attr(type=['negative'])
- @test.idempotent_id('3d07e294-27a0-4144-b780-a2a1bf6fee19')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('3d07e294-27a0-4144-b780-a2a1bf6fee19')
def test_update_user_for_non_existent_user(self):
# Attempt to update a user non-existent user should fail
user_name = data_utils.rand_name('user')
- non_existent_id = str(uuid.uuid4())
- self.assertRaises(lib_exc.NotFound, self.client.update_user,
+ non_existent_id = data_utils.rand_uuid()
+ self.assertRaises(lib_exc.NotFound, self.users_client.update_user,
non_existent_id, name=user_name)
- @test.attr(type=['negative'])
- @test.idempotent_id('3cc2a64b-83aa-4b02-88f0-d6ab737c4466')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('3cc2a64b-83aa-4b02-88f0-d6ab737c4466')
def test_update_user_request_without_a_token(self):
# Request to update a user without a valid token should fail
@@ -121,38 +129,39 @@
token = self.client.auth_provider.get_token()
# Delete the token from database
self.client.delete_token(token)
- self.assertRaises(lib_exc.Unauthorized, self.client.update_user,
- self.alt_user)
# Unset the token to allow further tests to generate a new token
- self.client.auth_provider.clear_auth()
+ self.addCleanup(self.client.auth_provider.clear_auth)
- @test.attr(type=['negative'])
- @test.idempotent_id('424868d5-18a7-43e1-8903-a64f95ee3aac')
+ self.assertRaises(lib_exc.Unauthorized, self.users_client.update_user,
+ self.alt_user)
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('424868d5-18a7-43e1-8903-a64f95ee3aac')
def test_update_user_by_unauthorized_user(self):
# Non-administrator should not be authorized to update user
- self.data.setup_test_tenant()
self.assertRaises(lib_exc.Forbidden,
- self.non_admin_client.update_user, self.alt_user)
+ self.non_admin_users_client.update_user,
+ self.alt_user)
- @test.attr(type=['negative'])
- @test.idempotent_id('d45195d5-33ed-41b9-a452-7d0d6a00f6e9')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('d45195d5-33ed-41b9-a452-7d0d6a00f6e9')
def test_delete_users_by_unauthorized_user(self):
# Non-administrator user should not be authorized to delete a user
- self.data.setup_test_user()
+ user = self.setup_test_user()
self.assertRaises(lib_exc.Forbidden,
- self.non_admin_client.delete_user,
- self.data.user['id'])
+ self.non_admin_users_client.delete_user,
+ user['id'])
- @test.attr(type=['negative'])
- @test.idempotent_id('7cc82f7e-9998-4f89-abae-23df36495867')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('7cc82f7e-9998-4f89-abae-23df36495867')
def test_delete_non_existent_user(self):
# Attempt to delete a non-existent user should fail
- self.assertRaises(lib_exc.NotFound, self.client.delete_user,
+ self.assertRaises(lib_exc.NotFound, self.users_client.delete_user,
'junk12345123')
- @test.attr(type=['negative'])
- @test.idempotent_id('57fe1df8-0aa7-46c0-ae9f-c2e785c7504a')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('57fe1df8-0aa7-46c0-ae9f-c2e785c7504a')
def test_delete_user_request_without_a_token(self):
# Request to delete a user without a valid token should fail
@@ -160,81 +169,90 @@
token = self.client.auth_provider.get_token()
# Delete the token from database
self.client.delete_token(token)
- self.assertRaises(lib_exc.Unauthorized, self.client.delete_user,
- self.alt_user)
# Unset the token to allow further tests to generate a new token
- self.client.auth_provider.clear_auth()
+ self.addCleanup(self.client.auth_provider.clear_auth)
- @test.attr(type=['negative'])
- @test.idempotent_id('593a4981-f6d4-460a-99a1-57a78bf20829')
+ self.assertRaises(lib_exc.Unauthorized, self.users_client.delete_user,
+ self.alt_user)
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('593a4981-f6d4-460a-99a1-57a78bf20829')
def test_authentication_for_disabled_user(self):
# Disabled user's token should not get authenticated
- self.data.setup_test_user()
- self.disable_user(self.data.test_user)
+ password = data_utils.rand_password()
+ user = self.setup_test_user(password)
+ tenant = self.tenants_client.show_tenant(user['tenantId'])['tenant']
+ self.disable_user(user['name'])
self.assertRaises(lib_exc.Unauthorized, self.token_client.auth,
- self.data.test_user,
- self.data.test_password,
- self.data.test_tenant)
+ user['name'],
+ password,
+ tenant['name'])
- @test.attr(type=['negative'])
- @test.idempotent_id('440a7a8d-9328-4b7b-83e0-d717010495e4')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('440a7a8d-9328-4b7b-83e0-d717010495e4')
def test_authentication_when_tenant_is_disabled(self):
# User's token for a disabled tenant should not be authenticated
- self.data.setup_test_user()
- self.disable_tenant(self.data.test_tenant)
+ password = data_utils.rand_password()
+ user = self.setup_test_user(password)
+ tenant = self.tenants_client.show_tenant(user['tenantId'])['tenant']
+ self.disable_tenant(tenant['name'])
self.assertRaises(lib_exc.Unauthorized, self.token_client.auth,
- self.data.test_user,
- self.data.test_password,
- self.data.test_tenant)
+ user['name'],
+ password,
+ tenant['name'])
- @test.attr(type=['negative'])
- @test.idempotent_id('921f1ad6-7907-40b8-853f-637e7ee52178')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('921f1ad6-7907-40b8-853f-637e7ee52178')
def test_authentication_with_invalid_tenant(self):
# User's token for an invalid tenant should not be authenticated
- self.data.setup_test_user()
+ password = data_utils.rand_password()
+ user = self.setup_test_user(password)
self.assertRaises(lib_exc.Unauthorized, self.token_client.auth,
- self.data.test_user,
- self.data.test_password,
+ user['name'],
+ password,
'junktenant1234')
- @test.attr(type=['negative'])
- @test.idempotent_id('bde9aecd-3b1c-4079-858f-beb5deaa5b5e')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('bde9aecd-3b1c-4079-858f-beb5deaa5b5e')
def test_authentication_with_invalid_username(self):
# Non-existent user's token should not get authenticated
- self.data.setup_test_user()
+ password = data_utils.rand_password()
+ user = self.setup_test_user(password)
+ tenant = self.tenants_client.show_tenant(user['tenantId'])['tenant']
self.assertRaises(lib_exc.Unauthorized, self.token_client.auth,
- 'junkuser123', self.data.test_password,
- self.data.test_tenant)
+ 'junkuser123', password, tenant['name'])
- @test.attr(type=['negative'])
- @test.idempotent_id('d5308b33-3574-43c3-8d87-1c090c5e1eca')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('d5308b33-3574-43c3-8d87-1c090c5e1eca')
def test_authentication_with_invalid_password(self):
# User's token with invalid password should not be authenticated
- self.data.setup_test_user()
+ user = self.setup_test_user()
+ tenant = self.tenants_client.show_tenant(user['tenantId'])['tenant']
self.assertRaises(lib_exc.Unauthorized, self.token_client.auth,
- self.data.test_user, 'junkpass1234',
- self.data.test_tenant)
+ user['name'], 'junkpass1234', tenant['name'])
- @test.attr(type=['negative'])
- @test.idempotent_id('284192ce-fb7c-4909-a63b-9a502e0ddd11')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('284192ce-fb7c-4909-a63b-9a502e0ddd11')
def test_get_users_by_unauthorized_user(self):
# Non-administrator user should not be authorized to get user list
- self.data.setup_test_user()
self.assertRaises(lib_exc.Forbidden,
- self.non_admin_client.list_users)
+ self.non_admin_users_client.list_users)
- @test.attr(type=['negative'])
- @test.idempotent_id('a73591ec-1903-4ffe-be42-282b39fefc9d')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('a73591ec-1903-4ffe-be42-282b39fefc9d')
def test_get_users_request_without_token(self):
# Request to get list of users without a valid token should fail
token = self.client.auth_provider.get_token()
self.client.delete_token(token)
- self.assertRaises(lib_exc.Unauthorized, self.client.list_users)
- self.client.auth_provider.clear_auth()
- @test.attr(type=['negative'])
- @test.idempotent_id('f5d39046-fc5f-425c-b29e-bac2632da28e')
+ # Unset the token to allow further tests to generate a new token
+ self.addCleanup(self.client.auth_provider.clear_auth)
+
+ self.assertRaises(lib_exc.Unauthorized, self.users_client.list_users)
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('f5d39046-fc5f-425c-b29e-bac2632da28e')
def test_list_users_with_invalid_tenant(self):
# Should not be able to return a list of all
# users for a non-existent tenant
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_credentials.py b/tempest/api/identity/admin/v3/test_credentials.py
index b81bff7..ba19ff7 100644
--- a/tempest/api/identity/admin/v3/test_credentials.py
+++ b/tempest/api/identity/admin/v3/test_credentials.py
@@ -12,10 +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 oslo_serialization import jsonutils as json
from tempest.api.identity import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
class CredentialsTestJSON(base.BaseIdentityV3AdminTest):
@@ -30,28 +31,25 @@
u_desc = '%s description' % u_name
u_email = '%s@testmail.tm' % u_name
u_password = data_utils.rand_password()
- for i in range(2):
- cls.project = cls.client.create_project(
+ for _ in range(2):
+ project = cls.projects_client.create_project(
data_utils.rand_name('project'),
description=data_utils.rand_name('project-desc'))['project']
- cls.projects.append(cls.project['id'])
+ cls.addClassResourceCleanup(
+ cls.projects_client.delete_project, project['id'])
+ cls.projects.append(project['id'])
- cls.user_body = cls.client.create_user(
- u_name, description=u_desc, password=u_password,
+ cls.user_body = cls.users_client.create_user(
+ name=u_name, description=u_desc, password=u_password,
email=u_email, project_id=cls.projects[0])['user']
-
- @classmethod
- def resource_cleanup(cls):
- cls.client.delete_user(cls.user_body['id'])
- for p in cls.projects:
- cls.client.delete_project(p)
- super(CredentialsTestJSON, cls).resource_cleanup()
+ cls.addClassResourceCleanup(
+ cls.users_client.delete_user, cls.user_body['id'])
def _delete_credential(self, cred_id):
self.creds_client.delete_credential(cred_id)
- @test.attr(type='smoke')
- @test.idempotent_id('7cd59bf9-bda4-4c72-9467-d21cab278355')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('7cd59bf9-bda4-4c72-9467-d21cab278355')
def test_credentials_create_get_update_delete(self):
blob = '{"access": "%s", "secret": "%s"}' % (
data_utils.rand_name('Access'), data_utils.rand_name('Secret'))
@@ -70,6 +68,7 @@
update_body = self.creds_client.update_credential(
cred['id'], blob=blob, project_id=self.projects[1],
type='ec2')['credential']
+ update_body['blob'] = json.loads(update_body['blob'])
self.assertEqual(cred['id'], update_body['id'])
self.assertEqual(self.projects[1], update_body['project_id'])
self.assertEqual(self.user_body['id'], update_body['user_id'])
@@ -77,6 +76,7 @@
self.assertEqual(update_body['blob']['secret'], new_keys[1])
get_body = self.creds_client.show_credential(cred['id'])['credential']
+ get_body['blob'] = json.loads(get_body['blob'])
for value1 in self.creds_list[0]:
self.assertEqual(update_body[value1],
get_body[value1])
@@ -84,12 +84,12 @@
self.assertEqual(update_body['blob'][value2],
get_body['blob'][value2])
- @test.idempotent_id('13202c00-0021-42a1-88d4-81b44d448aab')
+ @decorators.idempotent_id('13202c00-0021-42a1-88d4-81b44d448aab')
def test_credentials_list_delete(self):
created_cred_ids = list()
fetched_cred_ids = list()
- for i in range(2):
+ for _ in range(2):
blob = '{"access": "%s", "secret": "%s"}' % (
data_utils.rand_name('Access'), data_utils.rand_name('Secret'))
cred = self.creds_client.create_credential(
@@ -104,6 +104,6 @@
fetched_cred_ids.append(i['id'])
missing_creds = [c for c in created_cred_ids
if c not in fetched_cred_ids]
- self.assertEqual(0, len(missing_creds),
+ self.assertEmpty(missing_creds,
"Failed to find cred %s in fetched list" %
', '.join(m_cred for m_cred in missing_creds))
diff --git a/tempest/api/identity/admin/v3/test_default_project_id.py b/tempest/api/identity/admin/v3/test_default_project_id.py
index 53861ca..302a0e5 100644
--- a/tempest/api/identity/admin/v3/test_default_project_id.py
+++ b/tempest/api/identity/admin/v3/test_default_project_id.py
@@ -9,15 +9,12 @@
# 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 import auth
-
from tempest.api.identity import base
from tempest import clients
-from tempest.common.utils import data_utils
from tempest import config
-from tempest import manager
-from tempest import test
+from tempest.lib import auth
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
CONF = config.CONF
@@ -32,35 +29,35 @@
def _delete_domain(self, domain_id):
# It is necessary to disable the domain before deleting,
# or else it would result in unauthorized error
- self.client.update_domain(domain_id, enabled=False)
- self.client.delete_domain(domain_id)
+ self.domains_client.update_domain(domain_id, enabled=False)
+ self.domains_client.delete_domain(domain_id)
- @test.idempotent_id('d6110661-6a71-49a7-a453-b5e26640ff6d')
+ @decorators.idempotent_id('d6110661-6a71-49a7-a453-b5e26640ff6d')
def test_default_project_id(self):
# create a domain
dom_name = data_utils.rand_name('dom')
- domain_body = self.client.create_domain(dom_name)['domain']
+ domain_body = self.domains_client.create_domain(
+ name=dom_name)['domain']
dom_id = domain_body['id']
self.addCleanup(self._delete_domain, dom_id)
# create a project in the domain
- proj_name = data_utils.rand_name('proj')
- proj_body = self.client.create_project(proj_name,
- domain_id=dom_id)['project']
+ proj_body = self.setup_test_project(domain_id=dom_id)
proj_id = proj_body['id']
- self.addCleanup(self.client.delete_project, proj_id)
self.assertEqual(proj_body['domain_id'], dom_id,
- "project " + proj_name +
+ "project " + proj_body['name'] +
"doesn't have domain id " + dom_id)
# create a user in the domain, with the previous project as his
# default project
user_name = data_utils.rand_name('user')
- user_body = self.client.create_user(user_name, password=user_name,
- domain_id=dom_id,
- default_project_id=proj_id)['user']
+ user_body = self.users_client.create_user(
+ name=user_name,
+ password=user_name,
+ domain_id=dom_id,
+ default_project_id=proj_id)['user']
user_id = user_body['id']
- self.addCleanup(self.client.delete_user, user_id)
+ self.addCleanup(self.users_client.delete_user, user_id)
self.assertEqual(user_body['domain_id'], dom_id,
"user " + user_name +
"doesn't have domain id " + dom_id)
@@ -70,19 +67,19 @@
admin_role_id = admin_role['id']
# grant the admin role to the user on his project
- self.client.assign_user_role_on_project(proj_id, user_id,
- admin_role_id)
+ self.roles_client.create_user_role_on_project(proj_id, user_id,
+ admin_role_id)
# create a new client with user's credentials (NOTE: unscoped token!)
creds = auth.KeystoneV3Credentials(username=user_name,
password=user_name,
user_domain_name=dom_name)
- auth_provider = manager.get_auth_provider(creds)
+ auth_provider = clients.get_auth_provider(creds)
creds = auth_provider.fill_credentials()
admin_client = clients.Manager(credentials=creds)
# verify the user's token and see that it is scoped to the project
- token, auth_data = admin_client.auth_provider.get_auth()
+ token, _ = admin_client.auth_provider.get_auth()
result = admin_client.identity_v3_client.show_token(token)['token']
self.assertEqual(result['project']['domain']['id'], dom_id)
self.assertEqual(result['project']['id'], proj_id)
diff --git a/tempest/api/identity/admin/v3/test_domain_configuration.py b/tempest/api/identity/admin/v3/test_domain_configuration.py
new file mode 100644
index 0000000..c4e0622
--- /dev/null
+++ b/tempest/api/identity/admin/v3/test_domain_configuration.py
@@ -0,0 +1,172 @@
+# Copyright 2017 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 tempest.api.identity import base
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+
+class DomainConfigurationTestJSON(base.BaseIdentityV3AdminTest):
+
+ custom_config = {
+ "identity": {
+ "driver": "ldap"
+ },
+ "ldap": {
+ "url": "ldap://myldap.com:389/",
+ "user_tree_dn": "ou=Users,dc=my_new_root,dc=org"
+ }
+ }
+
+ @classmethod
+ def setup_clients(cls):
+ super(DomainConfigurationTestJSON, cls).setup_clients()
+ cls.client = cls.domain_config_client
+
+ def _create_domain_and_config(self, config):
+ domain = self.setup_test_domain()
+ config = self.client.create_domain_config(domain['id'], **config)[
+ 'config']
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.client.delete_domain_config, domain['id'])
+ return domain, config
+
+ @decorators.idempotent_id('11a02bf0-6f94-4380-b3b0-c8dc18fc0d22')
+ def test_show_default_group_config_and_options(self):
+ # The API supports only the identity and ldap groups. For the ldap
+ # group, a valid value is url or user_tree_dn. For the identity group,
+ # a valid value is driver.
+
+ # Check that the default config has the identity and ldap groups.
+ config = self.client.show_default_config_settings()['config']
+ self.assertIsInstance(config, dict)
+ self.assertIn('identity', config)
+ self.assertIn('ldap', config)
+
+ # Check that the identity group is correct.
+ identity_config = self.client.show_default_group_config('identity')[
+ 'config']
+
+ self.assertIsInstance(identity_config, dict)
+ self.assertIn('identity', identity_config)
+ self.assertIn('driver', identity_config['identity'])
+ self.assertIn('list_limit', identity_config['identity'])
+
+ # Show each option for the default domain and identity group.
+ for config_opt_name in ['driver', 'list_limit']:
+ retrieved_config_opt = self.client.show_default_group_option(
+ 'identity', config_opt_name)['config']
+ self.assertIn(config_opt_name, retrieved_config_opt)
+
+ # Check that the ldap group is correct.
+ ldap_config = self.client.show_default_group_config('ldap')['config']
+
+ self.assertIsInstance(ldap_config, dict)
+ self.assertIn('ldap', ldap_config)
+
+ # Several valid options exist for ldap group.
+ valid_options = ldap_config['ldap'].keys()
+
+ # Show each option for the default domain and ldap group.
+ for config_opt_name in valid_options:
+ retrieved_config_opt = self.client.show_default_group_option(
+ 'ldap', config_opt_name)['config']
+ self.assertIn(config_opt_name, retrieved_config_opt)
+
+ @decorators.idempotent_id('9e3ff13c-f597-4f01-9377-d6c06c2a1477')
+ def test_create_domain_config_and_show_config_groups_and_options(self):
+ domain, created_config = self._create_domain_and_config(
+ self.custom_config)
+
+ # Check that the entire configuration is correct.
+ self.assertEqual(self.custom_config, created_config)
+
+ # Check that each configuration group is correct.
+ for group_name in self.custom_config.keys():
+ group_cfg = self.client.show_domain_group_config(
+ domain['id'], group_name)['config']
+ self.assertIn(group_name, group_cfg)
+ self.assertEqual(self.custom_config[group_name],
+ group_cfg[group_name])
+
+ # Check that each configuration option is correct.
+ for opt_name in self.custom_config[group_name].keys():
+ group_opt = self.client.show_domain_group_option_config(
+ domain['id'], group_name, opt_name)['config']
+ self.assertIn(opt_name, group_opt)
+ self.assertEqual(self.custom_config[group_name][opt_name],
+ group_opt[opt_name])
+
+ @decorators.idempotent_id('7161023e-5dd0-4612-9da0-1bac6ac30b63')
+ def test_create_update_and_delete_domain_config(self):
+ domain, created_config = self._create_domain_and_config(
+ self.custom_config)
+
+ new_config = created_config
+ new_config['ldap']['url'] = data_utils.rand_url()
+
+ # Check that the altered configuration is reflected in updated_config.
+ updated_config = self.client.update_domain_config(
+ domain['id'], **new_config)['config']
+ self.assertEqual(new_config, updated_config)
+
+ # Check that showing the domain config shows the altered configuration.
+ retrieved_config = self.client.show_domain_config(domain['id'])[
+ 'config']
+ self.assertEqual(new_config, retrieved_config)
+
+ # Check that deleting a configuration works.
+ self.client.delete_domain_config(domain['id'])
+ self.assertRaises(lib_exc.NotFound, self.client.show_domain_config,
+ domain['id'])
+
+ @decorators.idempotent_id('c7510fa2-6661-4170-9c6b-4783a80651e9')
+ def test_create_update_and_delete_domain_config_groups_and_opts(self):
+ domain, _ = self._create_domain_and_config(self.custom_config)
+
+ # Check that updating configuration groups work.
+ new_driver = data_utils.rand_name('driver')
+ new_limit = data_utils.rand_int_id(0, 100)
+ new_group_config = {'identity': {'driver': new_driver,
+ 'list_limit': new_limit}}
+
+ updated_config = self.client.update_domain_group_config(
+ domain['id'], 'identity', **new_group_config)['config']
+
+ self.assertEqual(new_driver, updated_config['identity']['driver'])
+ self.assertEqual(new_limit, updated_config['identity']['list_limit'])
+
+ # Check that updating individual configuration group options work.
+ new_driver = data_utils.rand_name('driver')
+
+ updated_config = self.client.update_domain_group_option_config(
+ domain['id'], 'identity', 'driver', driver=new_driver)['config']
+
+ self.assertEqual(new_driver, updated_config['identity']['driver'])
+
+ # Check that deleting individual configuration group options work.
+ self.client.delete_domain_group_option_config(
+ domain['id'], 'identity', 'driver')
+ self.assertRaises(lib_exc.NotFound,
+ self.client.show_domain_group_option_config,
+ domain['id'], 'identity', 'driver')
+
+ # Check that deleting configuration groups work.
+ self.client.delete_domain_group_config(domain['id'], 'identity')
+ self.assertRaises(lib_exc.NotFound,
+ self.client.show_domain_group_config,
+ domain['id'], 'identity')
diff --git a/tempest/api/identity/admin/v3/test_domains.py b/tempest/api/identity/admin/v3/test_domains.py
index 15bea28..97a1f36 100644
--- a/tempest/api/identity/admin/v3/test_domains.py
+++ b/tempest/api/identity/admin/v3/test_domains.py
@@ -14,54 +14,73 @@
# under the License.
from tempest.api.identity import base
-from tempest.common.utils import data_utils
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions
CONF = config.CONF
class DomainsTestJSON(base.BaseIdentityV3AdminTest):
- def _delete_domain(self, domain_id):
- # It is necessary to disable the domain before deleting,
- # or else it would result in unauthorized error
- self.client.update_domain(domain_id, enabled=False)
- self.client.delete_domain(domain_id)
- # Asserting that the domain is not found in the list
- # after deletion
- body = self.client.list_domains()['domains']
- domains_list = [d['id'] for d in body]
- self.assertNotIn(domain_id, domains_list)
+ @classmethod
+ def resource_setup(cls):
+ super(DomainsTestJSON, cls).resource_setup()
+ # Create some test domains to be used during tests
+ # One of those domains will be disabled
+ cls.setup_domains = list()
+ for i in range(3):
+ domain = cls.create_domain(enabled=i < 2)
+ cls.setup_domains.append(domain)
- @test.idempotent_id('8cf516ef-2114-48f1-907b-d32726c734d4')
+ @decorators.idempotent_id('8cf516ef-2114-48f1-907b-d32726c734d4')
def test_list_domains(self):
# Test to list domains
- domain_ids = list()
fetched_ids = list()
- for _ in range(3):
- domain = self.client.create_domain(
- data_utils.rand_name('domain'),
- description=data_utils.rand_name('domain-desc'))['domain']
- # Delete the domain at the end of this method
- self.addCleanup(self._delete_domain, domain['id'])
- domain_ids.append(domain['id'])
# List and Verify Domains
- body = self.client.list_domains()['domains']
+ body = self.domains_client.list_domains()['domains']
for d in body:
fetched_ids.append(d['id'])
- missing_doms = [d for d in domain_ids if d not in fetched_ids]
- self.assertEqual(0, len(missing_doms))
+ missing_doms = [d for d in self.setup_domains
+ if d['id'] not in fetched_ids]
+ self.assertEmpty(missing_doms)
- @test.attr(type='smoke')
- @test.idempotent_id('f2f5b44a-82e8-4dad-8084-0661ea3b18cf')
+ @decorators.idempotent_id('c6aee07b-4981-440c-bb0b-eb598f58ffe9')
+ def test_list_domains_filter_by_name(self):
+ # List domains filtering by name
+ params = {'name': self.setup_domains[0]['name']}
+ fetched_domains = self.domains_client.list_domains(
+ **params)['domains']
+ # Verify the filtered list is correct, domain names are unique
+ # so exactly one domain should be found with the provided name
+ self.assertEqual(1, len(fetched_domains))
+ self.assertEqual(self.setup_domains[0]['name'],
+ fetched_domains[0]['name'])
+
+ @decorators.idempotent_id('3fd19840-65c1-43f8-b48c-51bdd066dff9')
+ def test_list_domains_filter_by_enabled(self):
+ # List domains filtering by enabled domains
+ params = {'enabled': True}
+ fetched_domains = self.domains_client.list_domains(
+ **params)['domains']
+ # Verify the filtered list is correct
+ self.assertIn(self.setup_domains[0], fetched_domains)
+ self.assertIn(self.setup_domains[1], fetched_domains)
+ for domain in fetched_domains:
+ self.assertEqual(True, domain['enabled'])
+
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('f2f5b44a-82e8-4dad-8084-0661ea3b18cf')
def test_create_update_delete_domain(self):
+ # Create domain
d_name = data_utils.rand_name('domain')
d_desc = data_utils.rand_name('domain-desc')
- domain = self.client.create_domain(
- d_name, description=d_desc)['domain']
- self.addCleanup(self._delete_domain, domain['id'])
- self.assertIn('id', domain)
+ domain = self.domains_client.create_domain(
+ name=d_name, description=d_desc)['domain']
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.delete_domain, domain['id'])
self.assertIn('description', domain)
self.assertIn('name', domain)
self.assertIn('enabled', domain)
@@ -70,11 +89,12 @@
self.assertEqual(d_name, domain['name'])
self.assertEqual(d_desc, domain['description'])
self.assertEqual(True, domain['enabled'])
+ # Update domain
new_desc = data_utils.rand_name('new-desc')
new_name = data_utils.rand_name('new-name')
-
- updated_domain = self.client.update_domain(
- domain['id'], name=new_name, description=new_desc)['domain']
+ updated_domain = self.domains_client.update_domain(
+ domain['id'], name=new_name, description=new_desc,
+ enabled=False)['domain']
self.assertIn('id', updated_domain)
self.assertIn('description', updated_domain)
self.assertIn('name', updated_domain)
@@ -83,34 +103,59 @@
self.assertIsNotNone(updated_domain['id'])
self.assertEqual(new_name, updated_domain['name'])
self.assertEqual(new_desc, updated_domain['description'])
- self.assertEqual(True, updated_domain['enabled'])
-
- fetched_domain = self.client.get_domain(domain['id'])['domain']
+ self.assertEqual(False, updated_domain['enabled'])
+ # Show domain
+ fetched_domain = self.domains_client.show_domain(
+ domain['id'])['domain']
self.assertEqual(new_name, fetched_domain['name'])
self.assertEqual(new_desc, fetched_domain['description'])
- self.assertEqual(True, fetched_domain['enabled'])
+ self.assertEqual(False, fetched_domain['enabled'])
+ # Delete domain
+ self.domains_client.delete_domain(domain['id'])
+ body = self.domains_client.list_domains()['domains']
+ domains_list = [d['id'] for d in body]
+ self.assertNotIn(domain['id'], domains_list)
- @test.idempotent_id('036df86e-bb5d-42c0-a7c2-66b9db3a6046')
+ @decorators.idempotent_id('d8d318b7-d1b3-4c37-94c5-3c5ba0b121ea')
+ def test_domain_delete_cascades_content(self):
+ # Create a domain with a user and a group in it
+ domain = self.setup_test_domain()
+ user = self.create_test_user(domain_id=domain['id'])
+ group = self.groups_client.create_group(
+ name=data_utils.rand_name('group'),
+ domain_id=domain['id'])['group']
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.groups_client.delete_group, group['id'])
+ # Delete the domain
+ self.delete_domain(domain['id'])
+ # Check the domain, its users and groups are gone
+ self.assertRaises(exceptions.NotFound,
+ self.domains_client.show_domain, domain['id'])
+ self.assertRaises(exceptions.NotFound,
+ self.users_client.show_user, user['id'])
+ self.assertRaises(exceptions.NotFound,
+ self.groups_client.show_group, group['id'])
+
+ @decorators.idempotent_id('036df86e-bb5d-42c0-a7c2-66b9db3a6046')
def test_create_domain_with_disabled_status(self):
# Create domain with enabled status as false
d_name = data_utils.rand_name('domain')
d_desc = data_utils.rand_name('domain-desc')
- domain = self.client.create_domain(
- d_name, description=d_desc, enabled=False)['domain']
- self.addCleanup(self.client.delete_domain, domain['id'])
+ domain = self.domains_client.create_domain(
+ name=d_name, description=d_desc, enabled=False)['domain']
+ self.addCleanup(self.domains_client.delete_domain, domain['id'])
self.assertEqual(d_name, domain['name'])
self.assertFalse(domain['enabled'])
self.assertEqual(d_desc, domain['description'])
- @test.idempotent_id('2abf8764-309a-4fa9-bc58-201b799817ad')
+ @decorators.idempotent_id('2abf8764-309a-4fa9-bc58-201b799817ad')
def test_create_domain_without_description(self):
# Create domain only with name
d_name = data_utils.rand_name('domain')
- domain = self.client.create_domain(d_name)['domain']
- self.addCleanup(self._delete_domain, domain['id'])
- self.assertIn('id', domain)
+ domain = self.domains_client.create_domain(name=d_name)['domain']
+ self.addCleanup(self.delete_domain, domain['id'])
expected_data = {'name': d_name, 'enabled': True}
- self.assertIsNone(domain['description'])
+ self.assertEqual('', domain['description'])
self.assertDictContainsSubset(expected_data, domain)
@@ -121,9 +166,9 @@
cls.domain_id = CONF.identity.default_domain_id
super(DefaultDomainTestJSON, cls).resource_setup()
- @test.attr(type='smoke')
- @test.idempotent_id('17a5de24-e6a0-4e4a-a9ee-d85b6e5612b5')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('17a5de24-e6a0-4e4a-a9ee-d85b6e5612b5')
def test_default_domain_exists(self):
- domain = self.client.get_domain(self.domain_id)['domain']
+ domain = self.domains_client.show_domain(self.domain_id)['domain']
self.assertTrue(domain['enabled'])
diff --git a/tempest/api/identity/admin/v3/test_domains_negative.py b/tempest/api/identity/admin/v3/test_domains_negative.py
index 9eb3149..56f7d32 100644
--- a/tempest/api/identity/admin/v3/test_domains_negative.py
+++ b/tempest/api/identity/admin/v3/test_domains_negative.py
@@ -14,59 +14,56 @@
# under the License.
from tempest.api.identity import base
-from tempest import test
-
-from tempest_lib.common.utils import data_utils
-from tempest_lib import exceptions as lib_exc
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class DomainsNegativeTestJSON(base.BaseIdentityV3AdminTest):
- _interface = 'json'
- @test.attr(type=['negative', 'gate'])
- @test.idempotent_id('1f3fbff5-4e44-400d-9ca1-d953f05f609b')
+ @decorators.attr(type=['negative', 'gate'])
+ @decorators.idempotent_id('1f3fbff5-4e44-400d-9ca1-d953f05f609b')
def test_delete_active_domain(self):
- d_name = data_utils.rand_name('domain')
- d_desc = data_utils.rand_name('domain-desc')
- domain = self.client.create_domain(d_name,
- description=d_desc)['domain']
+ domain = self.create_domain()
domain_id = domain['id']
self.addCleanup(self.delete_domain, domain_id)
# domain need to be disabled before deleting
- self.assertRaises(lib_exc.Forbidden, self.client.delete_domain,
+ self.assertRaises(lib_exc.Forbidden, self.domains_client.delete_domain,
domain_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('9018461d-7d24-408d-b3fe-ae37e8cd5c9e')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('9018461d-7d24-408d-b3fe-ae37e8cd5c9e')
def test_create_domain_with_empty_name(self):
# Domain name should not be empty
- self.assertRaises(lib_exc.BadRequest, self.client.create_domain,
- name='')
+ self.assertRaises(lib_exc.BadRequest,
+ self.domains_client.create_domain, name='')
- @test.attr(type=['negative'])
- @test.idempotent_id('37b1bbf2-d664-4785-9a11-333438586eae')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('37b1bbf2-d664-4785-9a11-333438586eae')
def test_create_domain_with_name_length_over_64(self):
# Domain name length should not ne greater than 64 characters
d_name = 'a' * 65
- self.assertRaises(lib_exc.BadRequest, self.client.create_domain,
- d_name)
+ self.assertRaises(lib_exc.BadRequest,
+ self.domains_client.create_domain,
+ name=d_name)
- @test.attr(type=['negative'])
- @test.idempotent_id('43781c07-764f-4cf2-a405-953c1916f605')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('43781c07-764f-4cf2-a405-953c1916f605')
def test_delete_non_existent_domain(self):
# Attempt to delete a non existent domain should fail
- self.assertRaises(lib_exc.NotFound, self.client.delete_domain,
+ self.assertRaises(lib_exc.NotFound, self.domains_client.delete_domain,
data_utils.rand_uuid_hex())
- @test.attr(type=['negative'])
- @test.idempotent_id('e6f9e4a2-4f36-4be8-bdbc-4e199ae29427')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('e6f9e4a2-4f36-4be8-bdbc-4e199ae29427')
def test_domain_create_duplicate(self):
domain_name = data_utils.rand_name('domain-dup')
- domain = self.client.create_domain(domain_name)['domain']
+ domain = self.domains_client.create_domain(name=domain_name)['domain']
domain_id = domain['id']
self.addCleanup(self.delete_domain, domain_id)
# Domain name should be unique
self.assertRaises(
- lib_exc.Conflict, self.client.create_domain, domain_name)
+ lib_exc.Conflict, self.domains_client.create_domain,
+ name=domain_name)
diff --git a/tempest/api/identity/admin/v3/test_endpoint_groups.py b/tempest/api/identity/admin/v3/test_endpoint_groups.py
new file mode 100644
index 0000000..eef93c2
--- /dev/null
+++ b/tempest/api/identity/admin/v3/test_endpoint_groups.py
@@ -0,0 +1,156 @@
+# Copyright 2017 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 tempest.api.identity import base
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+
+
+class EndPointGroupsTest(base.BaseIdentityV3AdminTest):
+
+ @classmethod
+ def setup_clients(cls):
+ super(EndPointGroupsTest, cls).setup_clients()
+ cls.client = cls.endpoint_groups_client
+
+ @classmethod
+ def resource_setup(cls):
+ super(EndPointGroupsTest, cls).resource_setup()
+ cls.endpoint_groups = list()
+
+ # Create endpoint group so as to use it for LIST test
+ service_id = cls._create_service()
+ cls.addClassResourceCleanup(
+ cls.services_client.delete_service, service_id)
+
+ name = data_utils.rand_name('service_group')
+ description = data_utils.rand_name('description')
+ filters = {'service_id': service_id}
+
+ endpoint_group = cls.client.create_endpoint_group(
+ name=name,
+ description=description,
+ filters=filters)['endpoint_group']
+ cls.addClassResourceCleanup(
+ cls.client.delete_endpoint_group, endpoint_group['id'])
+
+ cls.endpoint_groups.append(endpoint_group)
+
+ @classmethod
+ def _create_service(cls):
+ s_name = data_utils.rand_name('service')
+ s_type = data_utils.rand_name('type')
+ s_description = data_utils.rand_name('description')
+ service_data = (
+ cls.services_client.create_service(name=s_name,
+ type=s_type,
+ description=s_description))
+
+ service_id = service_data['service']['id']
+ return service_id
+
+ @decorators.idempotent_id('7c69e7a1-f865-402d-a2ea-44493017315a')
+ def test_create_list_show_check_delete_endpoint_group(self):
+ service_id = self._create_service()
+ name = data_utils.rand_name('service_group')
+ description = data_utils.rand_name('description')
+ filters = {'service_id': service_id}
+
+ endpoint_group = self.client.create_endpoint_group(
+ name=name,
+ description=description,
+ filters=filters)['endpoint_group']
+ self.addCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ self.client.delete_endpoint_group, endpoint_group['id'])
+
+ self.endpoint_groups.append(endpoint_group)
+
+ # Asserting created endpoint group response body
+ self.assertIn('id', endpoint_group)
+ self.assertEqual(name, endpoint_group['name'])
+ self.assertEqual(description, endpoint_group['description'])
+
+ # Checking if endpoint groups are present in the list of endpoints
+ # Note that there are two endpoint groups in the list, one created
+ # in the resource setup, one created in this test case.
+ fetched_endpoints = \
+ self.client.list_endpoint_groups()['endpoint_groups']
+
+ missing_endpoints = \
+ [e for e in self.endpoint_groups if e not in fetched_endpoints]
+
+ # Asserting LIST endpoints
+ self.assertEmpty(missing_endpoints,
+ "Failed to find endpoint %s in fetched list" %
+ ', '.join(str(e) for e in missing_endpoints))
+
+ # Show endpoint group
+ fetched_endpoint = self.client.show_endpoint_group(
+ endpoint_group['id'])['endpoint_group']
+
+ # Asserting if the attributes of endpoint group are the same
+ self.assertEqual(service_id,
+ fetched_endpoint['filters']['service_id'])
+ for attr in ('id', 'name', 'description'):
+ self.assertEqual(endpoint_group[attr], fetched_endpoint[attr])
+
+ # Check endpoint group
+ self.client.check_endpoint_group(endpoint_group['id'])
+
+ # Deleting the endpoint group created in this method
+ self.client.delete_endpoint_group(endpoint_group['id'])
+
+ # Checking whether endpoint group is deleted successfully
+ fetched_endpoints = \
+ self.client.list_endpoint_groups()['endpoint_groups']
+ fetched_endpoint_ids = [e['id'] for e in fetched_endpoints]
+ self.assertNotIn(endpoint_group['id'], fetched_endpoint_ids)
+
+ @decorators.idempotent_id('51c8fc38-fa84-4e76-b5b6-6fc37770fb26')
+ def test_update_endpoint_group(self):
+ # Creating an endpoint group so as to check update endpoint group
+ # with new values
+ service1_id = self._create_service()
+ name = data_utils.rand_name('service_group')
+ description = data_utils.rand_name('description')
+ filters = {'service_id': service1_id}
+
+ endpoint_group = self.client.create_endpoint_group(
+ name=name,
+ description=description,
+ filters=filters)['endpoint_group']
+ self.addCleanup(self.client.delete_endpoint_group,
+ endpoint_group['id'])
+
+ # Creating new attr values to update endpoint group
+ service2_id = self._create_service()
+ self.addCleanup(self.services_client.delete_service, service2_id)
+ name2 = data_utils.rand_name('service_group2')
+ description2 = data_utils.rand_name('description2')
+ filters = {'service_id': service2_id}
+
+ # Updating endpoint group with new attr values
+ updated_endpoint_group = self.client.update_endpoint_group(
+ endpoint_group['id'],
+ name=name2,
+ description=description2,
+ filters=filters)['endpoint_group']
+
+ self.assertEqual(name2, updated_endpoint_group['name'])
+ self.assertEqual(description2, updated_endpoint_group['description'])
+ self.assertEqual(service2_id,
+ updated_endpoint_group['filters']['service_id'])
diff --git a/tempest/api/identity/admin/v3/test_endpoints.py b/tempest/api/identity/admin/v3/test_endpoints.py
index 2ac832e..874aaa4 100644
--- a/tempest/api/identity/admin/v3/test_endpoints.py
+++ b/tempest/api/identity/admin/v3/test_endpoints.py
@@ -14,8 +14,9 @@
# under the License.
from tempest.api.identity import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
class EndPointsTestJSON(base.BaseIdentityV3AdminTest):
@@ -23,100 +24,149 @@
@classmethod
def setup_clients(cls):
super(EndPointsTestJSON, cls).setup_clients()
- cls.identity_client = cls.client
cls.client = cls.endpoints_client
@classmethod
def resource_setup(cls):
super(EndPointsTestJSON, cls).resource_setup()
cls.service_ids = list()
- s_name = data_utils.rand_name('service')
- s_type = data_utils.rand_name('type')
- s_description = data_utils.rand_name('description')
- cls.service_data = (
- cls.services_client.create_service(name=s_name, type=s_type,
- description=s_description))
- cls.service_data = cls.service_data['service']
- cls.service_id = cls.service_data['id']
- cls.service_ids.append(cls.service_id)
+
# Create endpoints so as to use for LIST and GET test cases
- cls.setup_endpoints = list()
+ interfaces = ['public', 'internal']
+ cls.setup_endpoint_ids = list()
for i in range(2):
+ service = cls._create_service()
+ cls.service_ids.append(service['id'])
+ cls.addClassResourceCleanup(
+ cls.services_client.delete_service, service['id'])
+
region = data_utils.rand_name('region')
url = data_utils.rand_url()
- interface = 'public'
- endpoint = cls.client.create_endpoint(service_id=cls.service_id,
- interface=interface,
- url=url, region=region,
- enabled=True)['endpoint']
- cls.setup_endpoints.append(endpoint)
+ endpoint = cls.client.create_endpoint(
+ service_id=cls.service_ids[i], interface=interfaces[i],
+ url=url, region=region, enabled=True)['endpoint']
+ cls.addClassResourceCleanup(
+ cls.client.delete_endpoint, endpoint['id'])
+ cls.setup_endpoint_ids.append(endpoint['id'])
@classmethod
- def resource_cleanup(cls):
- for e in cls.setup_endpoints:
- cls.client.delete_endpoint(e['id'])
- for s in cls.service_ids:
- cls.services_client.delete_service(s)
- super(EndPointsTestJSON, cls).resource_cleanup()
+ def _create_service(cls, s_name=None, s_type=None, s_description=None):
+ if s_name is None:
+ s_name = data_utils.rand_name('service')
+ if s_type is None:
+ s_type = data_utils.rand_name('type')
+ if s_description is None:
+ s_description = data_utils.rand_name('description')
+ service_data = (
+ cls.services_client.create_service(name=s_name, type=s_type,
+ description=s_description))
+ return service_data['service']
- @test.idempotent_id('c19ecf90-240e-4e23-9966-21cee3f6a618')
+ @decorators.idempotent_id('c19ecf90-240e-4e23-9966-21cee3f6a618')
def test_list_endpoints(self):
- # Get a list of endpoints
+ # Get the list of all the endpoints.
fetched_endpoints = self.client.list_endpoints()['endpoints']
- # Asserting LIST endpoints
+ fetched_endpoint_ids = [e['id'] for e in fetched_endpoints]
+ # Check that all the created endpoints are present in
+ # "fetched_endpoints".
missing_endpoints =\
- [e for e in self.setup_endpoints if e not in fetched_endpoints]
+ [e for e in self.setup_endpoint_ids
+ if e not in fetched_endpoint_ids]
self.assertEqual(0, len(missing_endpoints),
"Failed to find endpoint %s in fetched list" %
', '.join(str(e) for e in missing_endpoints))
- @test.idempotent_id('0e2446d2-c1fd-461b-a729-b9e73e3e3b37')
- def test_create_list_delete_endpoint(self):
+ # Check that filtering endpoints by service_id works.
+ fetched_endpoints_for_service = self.client.list_endpoints(
+ service_id=self.service_ids[0])['endpoints']
+ fetched_endpoints_for_alt_service = self.client.list_endpoints(
+ service_id=self.service_ids[1])['endpoints']
+
+ # Assert that both filters returned the correct result.
+ self.assertEqual(1, len(fetched_endpoints_for_service))
+ self.assertEqual(1, len(fetched_endpoints_for_alt_service))
+ self.assertEqual(set(self.setup_endpoint_ids),
+ set([fetched_endpoints_for_service[0]['id'],
+ fetched_endpoints_for_alt_service[0]['id']]))
+
+ # Check that filtering endpoints by interface works.
+ fetched_public_endpoints = self.client.list_endpoints(
+ interface='public')['endpoints']
+ fetched_internal_endpoints = self.client.list_endpoints(
+ interface='internal')['endpoints']
+
+ # Check that the expected endpoint_id is present per filter. [0] is
+ # public and [1] is internal.
+ self.assertIn(self.setup_endpoint_ids[0],
+ [e['id'] for e in fetched_public_endpoints])
+ self.assertIn(self.setup_endpoint_ids[1],
+ [e['id'] for e in fetched_internal_endpoints])
+
+ @decorators.idempotent_id('0e2446d2-c1fd-461b-a729-b9e73e3e3b37')
+ def test_create_list_show_delete_endpoint(self):
region = data_utils.rand_name('region')
url = data_utils.rand_url()
interface = 'public'
- endpoint = self.client.create_endpoint(service_id=self.service_id,
+ endpoint = self.client.create_endpoint(service_id=self.service_ids[0],
interface=interface,
url=url, region=region,
enabled=True)['endpoint']
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.client.delete_endpoint, endpoint['id'])
# Asserting Create Endpoint response body
- self.assertIn('id', endpoint)
self.assertEqual(region, endpoint['region'])
self.assertEqual(url, endpoint['url'])
+
# Checking if created endpoint is present in the list of endpoints
fetched_endpoints = self.client.list_endpoints()['endpoints']
fetched_endpoints_id = [e['id'] for e in fetched_endpoints]
self.assertIn(endpoint['id'], fetched_endpoints_id)
+
+ # Show endpoint
+ fetched_endpoint = (
+ self.client.show_endpoint(endpoint['id'])['endpoint'])
+ # Asserting if the attributes of endpoint are the same
+ self.assertEqual(self.service_ids[0], fetched_endpoint['service_id'])
+ self.assertEqual(interface, fetched_endpoint['interface'])
+ self.assertEqual(url, fetched_endpoint['url'])
+ self.assertEqual(region, fetched_endpoint['region'])
+ self.assertEqual(True, fetched_endpoint['enabled'])
+
# Deleting the endpoint created in this method
self.client.delete_endpoint(endpoint['id'])
+
# Checking whether endpoint is deleted successfully
fetched_endpoints = self.client.list_endpoints()['endpoints']
fetched_endpoints_id = [e['id'] for e in fetched_endpoints]
self.assertNotIn(endpoint['id'], fetched_endpoints_id)
- @test.attr(type='smoke')
- @test.idempotent_id('37e8f15e-ee7c-4657-a1e7-f6b61e375eff')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('37e8f15e-ee7c-4657-a1e7-f6b61e375eff')
def test_update_endpoint(self):
- # Creating an endpoint so as to check update endpoint
- # with new values
+ # NOTE(zhufl) Service2 should be created before endpoint_for_update
+ # is created, because Service2 must be deleted after
+ # endpoint_for_update is deleted, otherwise we will get a 404 error
+ # when deleting endpoint_for_update if endpoint's service is deleted.
+
+ # Creating service for updating endpoint with new service ID
+ s_name = data_utils.rand_name('service')
+ s_type = data_utils.rand_name('type')
+ s_description = data_utils.rand_name('description')
+ service2 = self._create_service(s_name=s_name, s_type=s_type,
+ s_description=s_description)
+ self.addCleanup(self.services_client.delete_service, service2['id'])
+
+ # Creating an endpoint so as to check update endpoint with new values
region1 = data_utils.rand_name('region')
url1 = data_utils.rand_url()
interface1 = 'public'
endpoint_for_update = (
- self.client.create_endpoint(service_id=self.service_id,
+ self.client.create_endpoint(service_id=self.service_ids[0],
interface=interface1,
url=url1, region=region1,
enabled=True)['endpoint'])
self.addCleanup(self.client.delete_endpoint, endpoint_for_update['id'])
- # Creating service so as update endpoint with new service ID
- s_name = data_utils.rand_name('service')
- s_type = data_utils.rand_name('type')
- s_description = data_utils.rand_name('description')
- service2 = (
- self.services_client.create_service(name=s_name, type=s_type,
- description=s_description))
- service2 = service2['service']
- self.service_ids.append(service2['id'])
+
# Updating endpoint with new values
region2 = data_utils.rand_name('region')
url2 = data_utils.rand_url()
diff --git a/tempest/api/identity/admin/v3/test_endpoints_negative.py b/tempest/api/identity/admin/v3/test_endpoints_negative.py
index 372254f..d54e222 100644
--- a/tempest/api/identity/admin/v3/test_endpoints_negative.py
+++ b/tempest/api/identity/admin/v3/test_endpoints_negative.py
@@ -14,11 +14,10 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.identity import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class EndpointsNegativeTestJSON(base.BaseIdentityV3AdminTest):
@@ -26,31 +25,25 @@
@classmethod
def setup_clients(cls):
super(EndpointsNegativeTestJSON, cls).setup_clients()
- cls.identity_client = cls.client
cls.client = cls.endpoints_client
@classmethod
def resource_setup(cls):
super(EndpointsNegativeTestJSON, cls).resource_setup()
- cls.service_ids = list()
s_name = data_utils.rand_name('service')
s_type = data_utils.rand_name('type')
s_description = data_utils.rand_name('description')
- cls.service_data = (
+ service_data = (
cls.services_client.create_service(name=s_name, type=s_type,
description=s_description)
['service'])
- cls.service_id = cls.service_data['id']
- cls.service_ids.append(cls.service_id)
+ cls.addClassResourceCleanup(cls.services_client.delete_service,
+ service_data['id'])
- @classmethod
- def resource_cleanup(cls):
- for s in cls.service_ids:
- cls.services_client.delete_service(s)
- super(EndpointsNegativeTestJSON, cls).resource_cleanup()
+ cls.service_id = service_data['id']
- @test.attr(type=['negative'])
- @test.idempotent_id('ac6c137e-4d3d-448f-8c83-4f13d0942651')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('ac6c137e-4d3d-448f-8c83-4f13d0942651')
def test_create_with_enabled_False(self):
# Enabled should be a boolean, not a string like 'False'
interface = 'public'
@@ -60,8 +53,8 @@
service_id=self.service_id, interface=interface,
url=url, region=region, enabled='False')
- @test.attr(type=['negative'])
- @test.idempotent_id('9c43181e-0627-484a-8c79-923e8a59598b')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('9c43181e-0627-484a-8c79-923e8a59598b')
def test_create_with_enabled_True(self):
# Enabled should be a boolean, not a string like 'True'
interface = 'public'
@@ -87,14 +80,14 @@
self.assertRaises(lib_exc.BadRequest, self.client.update_endpoint,
endpoint_for_update['id'], enabled=enabled)
- @test.attr(type=['negative'])
- @test.idempotent_id('65e41f32-5eb7-498f-a92a-a6ccacf7439a')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('65e41f32-5eb7-498f-a92a-a6ccacf7439a')
def test_update_with_enabled_False(self):
# Enabled should be a boolean, not a string like 'False'
self._assert_update_raises_bad_request('False')
- @test.attr(type=['negative'])
- @test.idempotent_id('faba3587-f066-4757-a48e-b4a3f01803bb')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('faba3587-f066-4757-a48e-b4a3f01803bb')
def test_update_with_enabled_True(self):
# Enabled should be a boolean, not a string like 'True'
self._assert_update_raises_bad_request('True')
diff --git a/tempest/api/identity/admin/v3/test_groups.py b/tempest/api/identity/admin/v3/test_groups.py
index 21fc62a..507810b 100644
--- a/tempest/api/identity/admin/v3/test_groups.py
+++ b/tempest/api/identity/admin/v3/test_groups.py
@@ -14,18 +14,27 @@
# under the License.
from tempest.api.identity import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+
+CONF = config.CONF
class GroupsV3TestJSON(base.BaseIdentityV3AdminTest):
- @test.idempotent_id('2e80343b-6c81-4ac3-88c7-452f3e9d5129')
+ @classmethod
+ def resource_setup(cls):
+ super(GroupsV3TestJSON, cls).resource_setup()
+ cls.domain = cls.create_domain()
+
+ @decorators.idempotent_id('2e80343b-6c81-4ac3-88c7-452f3e9d5129')
def test_group_create_update_get(self):
name = data_utils.rand_name('Group')
description = data_utils.rand_name('Description')
group = self.groups_client.create_group(
- name=name, description=description)['group']
+ name=name, domain_id=self.domain['id'],
+ description=description)['group']
self.addCleanup(self.groups_client.delete_group, group['id'])
self.assertEqual(group['name'], name)
self.assertEqual(group['description'], description)
@@ -42,12 +51,13 @@
self.assertEqual(new_name, new_group['name'])
self.assertEqual(new_desc, new_group['description'])
- @test.idempotent_id('b66eb441-b08a-4a6d-81ab-fef71baeb26c')
+ @decorators.idempotent_id('b66eb441-b08a-4a6d-81ab-fef71baeb26c')
def test_group_update_with_few_fields(self):
name = data_utils.rand_name('Group')
old_description = data_utils.rand_name('Description')
group = self.groups_client.create_group(
- name=name, description=old_description)['group']
+ name=name, domain_id=self.domain['id'],
+ description=old_description)['group']
self.addCleanup(self.groups_client.delete_group, group['id'])
new_name = data_utils.rand_name('UpdateGroup')
@@ -57,51 +67,52 @@
# Verify that 'description' is not being updated or deleted.
self.assertEqual(old_description, updated_group['description'])
- @test.attr(type='smoke')
- @test.idempotent_id('1598521a-2f36-4606-8df9-30772bd51339')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('1598521a-2f36-4606-8df9-30772bd51339')
def test_group_users_add_list_delete(self):
name = data_utils.rand_name('Group')
- group = self.groups_client.create_group(name=name)['group']
+ group = self.groups_client.create_group(
+ name=name, domain_id=self.domain['id'])['group']
self.addCleanup(self.groups_client.delete_group, group['id'])
# add user into group
users = []
- for i in range(3):
- name = data_utils.rand_name('User')
- password = data_utils.rand_password()
- user = self.client.create_user(name, password)['user']
+ for _ in range(3):
+ user = self.create_test_user()
users.append(user)
- self.addCleanup(self.client.delete_user, user['id'])
self.groups_client.add_group_user(group['id'], user['id'])
# list users in group
group_users = self.groups_client.list_group_users(group['id'])['users']
- self.assertEqual(sorted(users), sorted(group_users))
- # delete user in group
+ self.assertEqual(sorted(users, key=lambda k: k['name']),
+ sorted(group_users, key=lambda k: k['name']))
+ # check and delete user in group
for user in users:
+ self.groups_client.check_group_user_existence(
+ group['id'], user['id'])
self.groups_client.delete_group_user(group['id'], user['id'])
group_users = self.groups_client.list_group_users(group['id'])['users']
self.assertEqual(len(group_users), 0)
- @test.idempotent_id('64573281-d26a-4a52-b899-503cb0f4e4ec')
+ @decorators.idempotent_id('64573281-d26a-4a52-b899-503cb0f4e4ec')
def test_list_user_groups(self):
# create a user
- user = self.client.create_user(
- data_utils.rand_name('User'), data_utils.rand_password())['user']
- self.addCleanup(self.client.delete_user, user['id'])
+ user = self.create_test_user()
# create two groups, and add user into them
groups = []
- for i in range(2):
+ for _ in range(2):
name = data_utils.rand_name('Group')
- group = self.groups_client.create_group(name=name)['group']
+ group = self.groups_client.create_group(
+ name=name, domain_id=self.domain['id'])['group']
groups.append(group)
self.addCleanup(self.groups_client.delete_group, group['id'])
self.groups_client.add_group_user(group['id'], user['id'])
# list groups which user belongs to
- user_groups = self.client.list_user_groups(user['id'])['groups']
- self.assertEqual(sorted(groups), sorted(user_groups))
+ user_groups = self.users_client.list_user_groups(user['id'])['groups']
+ self.assertEqual(sorted(groups, key=lambda k: k['name']),
+ sorted(user_groups, key=lambda k: k['name']))
self.assertEqual(2, len(user_groups))
- @test.idempotent_id('cc9a57a5-a9ed-4f2d-a29f-4f979a06ec71')
+ @decorators.idempotent_id('cc9a57a5-a9ed-4f2d-a29f-4f979a06ec71')
def test_list_groups(self):
# Test to list groups
group_ids = list()
@@ -110,12 +121,20 @@
name = data_utils.rand_name('Group')
description = data_utils.rand_name('Description')
group = self.groups_client.create_group(
- name=name, description=description)['group']
+ name=name, domain_id=self.domain['id'],
+ description=description)['group']
self.addCleanup(self.groups_client.delete_group, group['id'])
group_ids.append(group['id'])
# List and Verify Groups
- body = self.groups_client.list_groups()['groups']
+ # When domain specific drivers are enabled the operations
+ # of listing all users and listing all groups are not supported,
+ # they need a domain filter to be specified
+ if CONF.identity_feature_enabled.domain_specific_drivers:
+ body = self.groups_client.list_groups(
+ domain_id=self.domain['id'])['groups']
+ else:
+ body = self.groups_client.list_groups()['groups']
for g in body:
fetched_ids.append(g['id'])
missing_groups = [g for g in group_ids if g not in fetched_ids]
- self.assertEqual([], missing_groups)
+ self.assertEmpty(missing_groups)
diff --git a/tempest/api/identity/admin/v3/test_inherits.py b/tempest/api/identity/admin/v3/test_inherits.py
new file mode 100644
index 0000000..68c0225
--- /dev/null
+++ b/tempest/api/identity/admin/v3/test_inherits.py
@@ -0,0 +1,208 @@
+# 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.common import utils
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+
+
+class InheritsV3TestJSON(base.BaseIdentityV3AdminTest):
+
+ @classmethod
+ def skip_checks(cls):
+ super(InheritsV3TestJSON, cls).skip_checks()
+ if not utils.is_extension_enabled('OS-INHERIT', 'identity'):
+ raise cls.skipException("Inherits aren't enabled")
+
+ @classmethod
+ def resource_setup(cls):
+ super(InheritsV3TestJSON, cls).resource_setup()
+ u_name = data_utils.rand_name('user-')
+ u_desc = '%s description' % u_name
+ u_email = '%s@testmail.tm' % u_name
+ u_password = data_utils.rand_name('pass-')
+ cls.domain = cls.create_domain()
+ cls.project = cls.projects_client.create_project(
+ data_utils.rand_name('project-'),
+ description=data_utils.rand_name('project-desc-'),
+ domain_id=cls.domain['id'])['project']
+ cls.addClassResourceCleanup(cls.projects_client.delete_project,
+ cls.project['id'])
+ cls.group = cls.groups_client.create_group(
+ name=data_utils.rand_name('group-'), project_id=cls.project['id'],
+ domain_id=cls.domain['id'])['group']
+ cls.addClassResourceCleanup(cls.groups_client.delete_group,
+ cls.group['id'])
+ cls.user = cls.users_client.create_user(
+ name=u_name, description=u_desc, password=u_password,
+ email=u_email, project_id=cls.project['id'],
+ domain_id=cls.domain['id'])['user']
+ cls.addClassResourceCleanup(cls.users_client.delete_user,
+ cls.user['id'])
+
+ def _list_assertions(self, body, fetched_role_ids, role_id):
+ self.assertEqual(len(body), 1)
+ self.assertIn(role_id, fetched_role_ids)
+
+ @decorators.idempotent_id('4e6f0366-97c8-423c-b2be-41eae6ac91c8')
+ def test_inherit_assign_list_check_revoke_roles_on_domains_user(self):
+ # Create role
+ src_role = self.setup_test_role()
+ # Assign role on domains user
+ self.inherited_roles_client.create_inherited_role_on_domains_user(
+ self.domain['id'], self.user['id'], src_role['id'])
+ # list role on domains user
+ roles = self.inherited_roles_client.\
+ list_inherited_project_role_for_user_on_domain(
+ self.domain['id'], self.user['id'])['roles']
+
+ fetched_role_ids = [i['id'] for i in roles]
+ self._list_assertions(roles, fetched_role_ids,
+ src_role['id'])
+
+ # Check role on domains user
+ (self.inherited_roles_client.
+ check_user_inherited_project_role_on_domain(
+ self.domain['id'], self.user['id'], src_role['id']))
+ # Revoke role from domains user.
+ self.inherited_roles_client.delete_inherited_role_from_user_on_domain(
+ self.domain['id'], self.user['id'], src_role['id'])
+
+ @decorators.idempotent_id('c7a8dda2-be50-4fb4-9a9c-e830771078b1')
+ def test_inherit_assign_list_check_revoke_roles_on_domains_group(self):
+ # Create role
+ src_role = self.setup_test_role()
+ # Assign role on domains group
+ self.inherited_roles_client.create_inherited_role_on_domains_group(
+ self.domain['id'], self.group['id'], src_role['id'])
+ # List role on domains group
+ roles = self.inherited_roles_client.\
+ list_inherited_project_role_for_group_on_domain(
+ self.domain['id'], self.group['id'])['roles']
+
+ fetched_role_ids = [i['id'] for i in roles]
+ self._list_assertions(roles, fetched_role_ids,
+ src_role['id'])
+
+ # Check role on domains group
+ (self.inherited_roles_client.
+ check_group_inherited_project_role_on_domain(
+ self.domain['id'], self.group['id'], src_role['id']))
+ # Revoke role from domains group
+ self.inherited_roles_client.delete_inherited_role_from_group_on_domain(
+ self.domain['id'], self.group['id'], src_role['id'])
+
+ @decorators.idempotent_id('18b70e45-7687-4b72-8277-b8f1a47d7591')
+ def test_inherit_assign_check_revoke_roles_on_projects_user(self):
+ # Create role
+ src_role = self.setup_test_role()
+ # Assign role on projects user
+ self.inherited_roles_client.create_inherited_role_on_projects_user(
+ self.project['id'], self.user['id'], src_role['id'])
+ # Check role on projects user
+ (self.inherited_roles_client.
+ check_user_has_flag_on_inherited_to_project(
+ self.project['id'], self.user['id'], src_role['id']))
+ # Revoke role from projects user
+ self.inherited_roles_client.delete_inherited_role_from_user_on_project(
+ self.project['id'], self.user['id'], src_role['id'])
+
+ @decorators.idempotent_id('26021436-d5a4-4256-943c-ded01e0d4b45')
+ def test_inherit_assign_check_revoke_roles_on_projects_group(self):
+ # Create role
+ src_role = self.setup_test_role()
+ # Assign role on projects group
+ self.inherited_roles_client.create_inherited_role_on_projects_group(
+ self.project['id'], self.group['id'], src_role['id'])
+ # Check role on projects group
+ (self.inherited_roles_client.
+ check_group_has_flag_on_inherited_to_project(
+ self.project['id'], self.group['id'], src_role['id']))
+ # Revoke role from projects group
+ (self.inherited_roles_client.
+ delete_inherited_role_from_group_on_project(
+ self.project['id'], self.group['id'], src_role['id']))
+
+ @decorators.idempotent_id('3acf666e-5354-42ac-8e17-8b68893bcd36')
+ def test_inherit_assign_list_revoke_user_roles_on_domain(self):
+ # Create role
+ src_role = self.setup_test_role()
+
+ # Create a project hierarchy
+ leaf_project = self.setup_test_project(domain_id=self.domain['id'],
+ parent_id=self.project['id'])
+
+ # Assign role on domain
+ self.inherited_roles_client.create_inherited_role_on_domains_user(
+ self.domain['id'], self.user['id'], src_role['id'])
+
+ # List "effective" role assignments from user on the parent project
+ params = {'scope.project.id': self.project['id'],
+ 'user.id': self.user['id']}
+ assignments = self.role_assignments.list_role_assignments(
+ effective=True, **params)['role_assignments']
+ self.assertNotEmpty(assignments)
+
+ # List "effective" role assignments from user on the leaf project
+ params['scope.project.id'] = leaf_project['id']
+ assignments = self.role_assignments.list_role_assignments(
+ effective=True, **params)['role_assignments']
+ self.assertNotEmpty(assignments)
+
+ # Revoke role from domain
+ self.inherited_roles_client.delete_inherited_role_from_user_on_domain(
+ self.domain['id'], self.user['id'], src_role['id'])
+
+ # List "effective" role assignments from user on the parent project
+ # should return an empty list
+ params['scope.project.id'] = self.project['id']
+ assignments = self.role_assignments.list_role_assignments(
+ effective=True, **params)['role_assignments']
+ self.assertEmpty(assignments)
+
+ # List "effective" role assignments from user on the leaf project
+ # should return an empty list
+ params['scope.project.id'] = leaf_project['id']
+ assignments = self.role_assignments.list_role_assignments(
+ effective=True, **params)['role_assignments']
+ self.assertEmpty(assignments)
+
+ @decorators.idempotent_id('9f02ccd9-9b57-46b4-8f77-dd5a736f3a06')
+ def test_inherit_assign_list_revoke_user_roles_on_project_tree(self):
+ # Create role
+ src_role = self.setup_test_role()
+
+ # Create a project hierarchy
+ leaf_project = self.setup_test_project(domain_id=self.domain['id'],
+ parent_id=self.project['id'])
+
+ # Assign role on parent project
+ self.inherited_roles_client.create_inherited_role_on_projects_user(
+ self.project['id'], self.user['id'], src_role['id'])
+
+ # List "effective" role assignments from user on the leaf project
+ params = {'scope.project.id': leaf_project['id'],
+ 'user.id': self.user['id']}
+ assignments = self.role_assignments.list_role_assignments(
+ effective=True, **params)['role_assignments']
+ self.assertNotEmpty(assignments)
+
+ # Revoke role from parent project
+ self.inherited_roles_client.delete_inherited_role_from_user_on_project(
+ self.project['id'], self.user['id'], src_role['id'])
+
+ # List "effective" role assignments from user on the leaf project
+ # should return an empty list
+ assignments = self.role_assignments.list_role_assignments(
+ effective=True, **params)['role_assignments']
+ self.assertEmpty(assignments)
diff --git a/tempest/api/identity/admin/v3/test_list_projects.py b/tempest/api/identity/admin/v3/test_list_projects.py
index 5185fea..82664e8 100644
--- a/tempest/api/identity/admin/v3/test_list_projects.py
+++ b/tempest/api/identity/admin/v3/test_list_projects.py
@@ -14,8 +14,8 @@
# under the License.
from tempest.api.identity import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
class ListProjectsTestJSON(base.BaseIdentityV3AdminTest):
@@ -24,46 +24,66 @@
def resource_setup(cls):
super(ListProjectsTestJSON, cls).resource_setup()
cls.project_ids = list()
- cls.data.setup_test_domain()
+ # Create a domain
+ cls.domain = cls.create_domain()
# Create project with domain
cls.p1_name = data_utils.rand_name('project')
- cls.p1 = cls.client.create_project(
+ cls.p1 = cls.projects_client.create_project(
cls.p1_name, enabled=False,
- domain_id=cls.data.domain['id'])['project']
- cls.data.projects.append(cls.p1)
+ domain_id=cls.domain['id'])['project']
+ cls.addClassResourceCleanup(cls.projects_client.delete_project,
+ cls.p1['id'])
cls.project_ids.append(cls.p1['id'])
# Create default project
p2_name = data_utils.rand_name('project')
- cls.p2 = cls.client.create_project(p2_name)['project']
- cls.data.projects.append(cls.p2)
+ cls.p2 = cls.projects_client.create_project(p2_name)['project']
+ cls.addClassResourceCleanup(cls.projects_client.delete_project,
+ cls.p2['id'])
cls.project_ids.append(cls.p2['id'])
+ # Create a new project (p3) using p2 as parent project
+ p3_name = data_utils.rand_name('project')
+ cls.p3 = cls.projects_client.create_project(
+ p3_name, parent_id=cls.p2['id'])['project']
+ cls.addClassResourceCleanup(cls.projects_client.delete_project,
+ cls.p3['id'])
+ cls.project_ids.append(cls.p3['id'])
- @test.idempotent_id('1d830662-22ad-427c-8c3e-4ec854b0af44')
- def test_projects_list(self):
+ @decorators.idempotent_id('1d830662-22ad-427c-8c3e-4ec854b0af44')
+ def test_list_projects(self):
# List projects
- list_projects = self.client.list_projects()['projects']
+ list_projects = self.projects_client.list_projects()['projects']
for p in self.project_ids:
- get_project = self.client.get_project(p)['project']
- self.assertIn(get_project, list_projects)
+ show_project = self.projects_client.show_project(p)['project']
+ self.assertIn(show_project, list_projects)
- @test.idempotent_id('fab13f3c-f6a6-4b9f-829b-d32fd44fdf10')
+ @decorators.idempotent_id('fab13f3c-f6a6-4b9f-829b-d32fd44fdf10')
def test_list_projects_with_domains(self):
# List projects with domain
self._list_projects_with_params(
- {'domain_id': self.data.domain['id']}, 'domain_id')
+ {'domain_id': self.domain['id']}, 'domain_id')
- @test.idempotent_id('0fe7a334-675a-4509-b00e-1c4b95d5dae8')
+ @decorators.idempotent_id('0fe7a334-675a-4509-b00e-1c4b95d5dae8')
def test_list_projects_with_enabled(self):
# List the projects with enabled
self._list_projects_with_params({'enabled': False}, 'enabled')
- @test.idempotent_id('fa178524-4e6d-4925-907c-7ab9f42c7e26')
+ @decorators.idempotent_id('fa178524-4e6d-4925-907c-7ab9f42c7e26')
def test_list_projects_with_name(self):
# List projects with name
self._list_projects_with_params({'name': self.p1_name}, 'name')
+ @decorators.idempotent_id('6edc66f5-2941-4a17-9526-4073311c1fac')
+ def test_list_projects_with_parent(self):
+ # List projects with parent
+ params = {'parent_id': self.p3['parent_id']}
+ fetched_projects = self.projects_client.list_projects(
+ params)['projects']
+ self.assertNotEmpty(fetched_projects)
+ for project in fetched_projects:
+ self.assertEqual(self.p3['parent_id'], project['parent_id'])
+
def _list_projects_with_params(self, params, key):
- body = self.client.list_projects(params)['projects']
+ body = self.projects_client.list_projects(params)['projects']
self.assertIn(self.p1[key], map(lambda x: x[key], body))
self.assertNotIn(self.p2[key], map(lambda x: x[key], body))
diff --git a/tempest/api/identity/admin/v3/test_list_users.py b/tempest/api/identity/admin/v3/test_list_users.py
index ca91ce5..c69e4c8 100644
--- a/tempest/api/identity/admin/v3/test_list_users.py
+++ b/tempest/api/identity/admin/v3/test_list_users.py
@@ -14,8 +14,11 @@
# under the License.
from tempest.api.identity import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+
+CONF = config.CONF
class UsersV3TestJSON(base.BaseIdentityV3AdminTest):
@@ -25,7 +28,7 @@
# assert the response based on expected and not_expected
# expected: user expected in the list response
# not_expected: user, which should not be present in list response
- body = self.client.list_users(params)['users']
+ body = self.users_client.list_users(**params)['users']
self.assertIn(expected[key], map(lambda x: x[key], body))
self.assertNotIn(not_expected[key],
map(lambda x: x[key], body))
@@ -36,29 +39,35 @@
alt_user = data_utils.rand_name('test_user')
alt_password = data_utils.rand_password()
cls.alt_email = alt_user + '@testmail.tm'
- cls.data.setup_test_domain()
+ # Create a domain
+ cls.domain = cls.create_domain()
# Create user with Domain
+ cls.users = list()
u1_name = data_utils.rand_name('test_user')
- cls.domain_enabled_user = cls.client.create_user(
- u1_name, password=alt_password,
- email=cls.alt_email, domain_id=cls.data.domain['id'])['user']
- cls.data.v3_users.append(cls.domain_enabled_user)
+ cls.domain_enabled_user = cls.users_client.create_user(
+ name=u1_name, password=alt_password,
+ email=cls.alt_email, domain_id=cls.domain['id'])['user']
+ cls.addClassResourceCleanup(cls.users_client.delete_user,
+ cls.domain_enabled_user['id'])
+ cls.users.append(cls.domain_enabled_user)
# Create default not enabled user
u2_name = data_utils.rand_name('test_user')
- cls.non_domain_enabled_user = cls.client.create_user(
- u2_name, password=alt_password,
+ cls.non_domain_enabled_user = cls.users_client.create_user(
+ name=u2_name, password=alt_password,
email=cls.alt_email, enabled=False)['user']
- cls.data.v3_users.append(cls.non_domain_enabled_user)
+ cls.addClassResourceCleanup(cls.users_client.delete_user,
+ cls.non_domain_enabled_user['id'])
+ cls.users.append(cls.non_domain_enabled_user)
- @test.idempotent_id('08f9aabb-dcfe-41d0-8172-82b5fa0bd73d')
+ @decorators.idempotent_id('08f9aabb-dcfe-41d0-8172-82b5fa0bd73d')
def test_list_user_domains(self):
# List users with domain
- params = {'domain_id': self.data.domain['id']}
+ params = {'domain_id': self.domain['id']}
self._list_users_with_params(params, 'domain_id',
self.domain_enabled_user,
self.non_domain_enabled_user)
- @test.idempotent_id('bff8bf2f-9408-4ef5-b63a-753c8c2124eb')
+ @decorators.idempotent_id('bff8bf2f-9408-4ef5-b63a-753c8c2124eb')
def test_list_users_with_not_enabled(self):
# List the users with not enabled
params = {'enabled': False}
@@ -66,30 +75,46 @@
self.non_domain_enabled_user,
self.domain_enabled_user)
- @test.idempotent_id('c285bb37-7325-4c02-bff3-3da5d946d683')
+ @decorators.idempotent_id('c285bb37-7325-4c02-bff3-3da5d946d683')
def test_list_users_with_name(self):
# List users with name
params = {'name': self.domain_enabled_user['name']}
+ # When domain specific drivers are enabled the operations
+ # of listing all users and listing all groups are not supported,
+ # they need a domain filter to be specified
+ if CONF.identity_feature_enabled.domain_specific_drivers:
+ params['domain_id'] = self.domain_enabled_user['domain_id']
self._list_users_with_params(params, 'name',
self.domain_enabled_user,
self.non_domain_enabled_user)
- @test.idempotent_id('b30d4651-a2ea-4666-8551-0c0e49692635')
+ @decorators.idempotent_id('b30d4651-a2ea-4666-8551-0c0e49692635')
def test_list_users(self):
# List users
- body = self.client.list_users()['users']
+ # When domain specific drivers are enabled the operations
+ # of listing all users and listing all groups are not supported,
+ # they need a domain filter to be specified
+ if CONF.identity_feature_enabled.domain_specific_drivers:
+ body_enabled_user = self.users_client.list_users(
+ domain_id=self.domain_enabled_user['domain_id'])['users']
+ body_non_enabled_user = self.users_client.list_users(
+ domain_id=self.non_domain_enabled_user['domain_id'])['users']
+ body = (body_enabled_user + body_non_enabled_user)
+ else:
+ body = self.users_client.list_users()['users']
+
fetched_ids = [u['id'] for u in body]
- missing_users = [u['id'] for u in self.data.v3_users
+ missing_users = [u['id'] for u in self.users
if u['id'] not in fetched_ids]
- self.assertEqual(0, len(missing_users),
+ self.assertEmpty(missing_users,
"Failed to find user %s in fetched list" %
', '.join(m_user for m_user in missing_users))
- @test.idempotent_id('b4baa3ae-ac00-4b4e-9e27-80deaad7771f')
+ @decorators.idempotent_id('b4baa3ae-ac00-4b4e-9e27-80deaad7771f')
def test_get_user(self):
# Get a user detail
- user = self.client.show_user(self.data.v3_users[0]['id'])['user']
- self.assertEqual(self.data.v3_users[0]['id'], user['id'])
- self.assertEqual(self.data.v3_users[0]['name'], user['name'])
+ user = self.users_client.show_user(self.users[0]['id'])['user']
+ self.assertEqual(self.users[0]['id'], user['id'])
+ self.assertEqual(self.users[0]['name'], user['name'])
self.assertEqual(self.alt_email, user['email'])
- self.assertEqual(self.data.domain['id'], user['domain_id'])
+ self.assertEqual(self.domain['id'], user['domain_id'])
diff --git a/tempest/api/identity/admin/v3/test_oauth_consumers.py b/tempest/api/identity/admin/v3/test_oauth_consumers.py
new file mode 100644
index 0000000..062cce5
--- /dev/null
+++ b/tempest/api/identity/admin/v3/test_oauth_consumers.py
@@ -0,0 +1,91 @@
+# Copyright 2017 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 tempest.api.identity import base
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions
+
+
+class OAUTHConsumersV3Test(base.BaseIdentityV3AdminTest):
+
+ def _create_consumer(self):
+ """Creates a consumer with a random description."""
+ description = data_utils.rand_name('test_create_consumer')
+ consumer = self.oauth_consumers_client.create_consumer(
+ description)['consumer']
+ # cleans up created consumers after tests
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.oauth_consumers_client.delete_consumer,
+ consumer['id'])
+ return consumer
+
+ @decorators.idempotent_id('c8307ea6-a86c-47fd-ae7b-5b3b2caca76d')
+ def test_create_and_show_consumer(self):
+ """Tests to make sure that a consumer with parameters is made"""
+ consumer = self._create_consumer()
+ # fetch created consumer from client
+ fetched_consumer = self.oauth_consumers_client.show_consumer(
+ consumer['id'])['consumer']
+ # assert that the fetched consumer matches the created one and
+ # has all parameters
+ for key in ['description', 'id', 'links']:
+ self.assertEqual(consumer[key], fetched_consumer[key])
+
+ @decorators.idempotent_id('fdfa1b7f-2a31-4354-b2c7-f6ae20554f93')
+ def test_delete_consumer(self):
+ """Tests the delete function."""
+ consumer = self._create_consumer()
+ # fetch consumer from client to confirm it exists
+ fetched_consumer = self.oauth_consumers_client.show_consumer(
+ consumer['id'])['consumer']
+ self.assertEqual(consumer['id'], fetched_consumer['id'])
+ # delete existing consumer
+ self.oauth_consumers_client.delete_consumer(consumer['id'])
+ # check that consumer no longer exists
+ self.assertRaises(exceptions.NotFound,
+ self.oauth_consumers_client.show_consumer,
+ consumer['id'])
+
+ @decorators.idempotent_id('080a9b1a-c009-47c0-9979-5305bf72e3dc')
+ def test_update_consumer(self):
+ """Tests the update functionality"""
+ # create a new consumer to update
+ consumer = self._create_consumer()
+ # create new description
+ new_description = data_utils.rand_name('test_update_consumer')
+ # update consumer
+ self.oauth_consumers_client.update_consumer(consumer['id'],
+ new_description)
+ # check that the same consumer now has the new description
+ updated_consumer = self.oauth_consumers_client.show_consumer(
+ consumer['id'])['consumer']
+ self.assertEqual(new_description, updated_consumer['description'])
+
+ @decorators.idempotent_id('09ca50de-78f2-4ffb-ac71-f2254036b2b8')
+ def test_list_consumers(self):
+ """Test for listing consumers"""
+ # create two consumers to populate list
+ new_consumer_one = self._create_consumer()
+ new_consumer_two = self._create_consumer()
+ # fetch the list of consumers
+ consumer_list = self.oauth_consumers_client \
+ .list_consumers()['consumers']
+ # add fetched consumer ids to a list
+ id_list = [consumer['id'] for consumer in consumer_list]
+ # check if created consumers are in the list
+ self.assertIn(new_consumer_one['id'], id_list)
+ self.assertIn(new_consumer_two['id'], id_list)
diff --git a/tempest/api/identity/admin/v3/test_policies.py b/tempest/api/identity/admin/v3/test_policies.py
index 3b5e5d4..2908fc4 100644
--- a/tempest/api/identity/admin/v3/test_policies.py
+++ b/tempest/api/identity/admin/v3/test_policies.py
@@ -14,8 +14,8 @@
# under the License.
from tempest.api.identity import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
class PoliciesTestJSON(base.BaseIdentityV3AdminTest):
@@ -23,7 +23,7 @@
def _delete_policy(self, policy_id):
self.policies_client.delete_policy(policy_id)
- @test.idempotent_id('1a0ad286-2d06-4123-ab0d-728893a76201')
+ @decorators.idempotent_id('1a0ad286-2d06-4123-ab0d-728893a76201')
def test_list_policies(self):
# Test to list policies
policy_ids = list()
@@ -41,10 +41,10 @@
for p in body:
fetched_ids.append(p['id'])
missing_pols = [p for p in policy_ids if p not in fetched_ids]
- self.assertEqual(0, len(missing_pols))
+ self.assertEmpty(missing_pols)
- @test.attr(type='smoke')
- @test.idempotent_id('e544703a-2f03-4cf2-9b0f-350782fdb0d3')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('e544703a-2f03-4cf2-9b0f-350782fdb0d3')
def test_create_update_delete_policy(self):
# Test to update policy
blob = data_utils.rand_name('BlobName')
@@ -52,7 +52,6 @@
policy = self.policies_client.create_policy(blob=blob,
type=policy_type)['policy']
self.addCleanup(self._delete_policy, policy['id'])
- self.assertIn('id', policy)
self.assertIn('type', policy)
self.assertIn('blob', policy)
self.assertIsNotNone(policy['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 af9497c..bc94a8e 100644
--- a/tempest/api/identity/admin/v3/test_projects.py
+++ b/tempest/api/identity/admin/v3/test_projects.py
@@ -1,4 +1,4 @@
-# Copyright 2013 OpenStack, LLC
+# Copyright 2013 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -14,165 +14,220 @@
# under the License.
from tempest.api.identity import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+
+CONF = config.CONF
class ProjectsTestJSON(base.BaseIdentityV3AdminTest):
- @test.idempotent_id('0ecf465c-0dc4-4532-ab53-91ffeb74d12d')
+ @decorators.idempotent_id('0ecf465c-0dc4-4532-ab53-91ffeb74d12d')
def test_project_create_with_description(self):
# Create project with a description
- project_name = data_utils.rand_name('project')
project_desc = data_utils.rand_name('desc')
- project = self.client.create_project(
- project_name, description=project_desc)['project']
- self.data.projects.append(project)
+ project = self.setup_test_project(description=project_desc)
project_id = project['id']
desc1 = project['description']
self.assertEqual(desc1, project_desc, 'Description should have '
'been sent in response for create')
- body = self.client.get_project(project_id)['project']
+ body = self.projects_client.show_project(project_id)['project']
desc2 = body['description']
self.assertEqual(desc2, project_desc, 'Description does not appear'
'to be set')
- @test.idempotent_id('5f50fe07-8166-430b-a882-3b2ee0abe26f')
+ @decorators.idempotent_id('5f50fe07-8166-430b-a882-3b2ee0abe26f')
def test_project_create_with_domain(self):
# Create project with a domain
- self.data.setup_test_domain()
+ domain = self.setup_test_domain()
project_name = data_utils.rand_name('project')
- project = self.client.create_project(
- project_name, domain_id=self.data.domain['id'])['project']
- self.data.projects.append(project)
+ project = self.setup_test_project(
+ name=project_name, domain_id=domain['id'])
project_id = project['id']
self.assertEqual(project_name, project['name'])
- self.assertEqual(self.data.domain['id'], project['domain_id'])
- body = self.client.get_project(project_id)['project']
+ self.assertEqual(domain['id'], project['domain_id'])
+ body = self.projects_client.show_project(project_id)['project']
self.assertEqual(project_name, body['name'])
- self.assertEqual(self.data.domain['id'], body['domain_id'])
+ self.assertEqual(domain['id'], body['domain_id'])
- @test.idempotent_id('1f66dc76-50cc-4741-a200-af984509e480')
+ @decorators.idempotent_id('1854f9c0-70bc-4d11-a08a-1c789d339e3d')
+ def test_project_create_with_parent(self):
+ # Create root project without providing a parent_id
+ domain = self.setup_test_domain()
+ domain_id = domain['id']
+
+ root_project_name = data_utils.rand_name('root_project')
+ root_project = self.setup_test_project(
+ name=root_project_name, domain_id=domain_id)
+
+ root_project_id = root_project['id']
+ parent_id = root_project['parent_id']
+ self.assertEqual(root_project_name, root_project['name'])
+ # If not provided, the parent_id must point to the top level
+ # project in the hierarchy, i.e. its domain
+ self.assertEqual(domain_id, parent_id)
+
+ # Create a project using root_project_id as parent_id
+ project_name = data_utils.rand_name('project')
+ project = self.setup_test_project(
+ name=project_name, domain_id=domain_id, parent_id=root_project_id)
+ parent_id = project['parent_id']
+ self.assertEqual(project_name, project['name'])
+ self.assertEqual(root_project_id, parent_id)
+
+ @decorators.idempotent_id('a7eb9416-6f9b-4dbb-b71b-7f73aaef59d5')
+ def test_create_is_domain_project(self):
+ project = self.setup_test_project(domain_id=None, is_domain=True)
+ # To delete a domain, we need to disable it first
+ self.addCleanup(self.projects_client.update_project, project['id'],
+ enabled=False)
+
+ # Check if the is_domain project is correctly returned by both
+ # project and domain APIs
+ projects_list = self.projects_client.list_projects(
+ params={'is_domain': True})['projects']
+ project_ids = [p['id'] for p in projects_list]
+ self.assertIn(project['id'], project_ids)
+
+ # The domains API return different attributes for the entity, so we
+ # compare the entities IDs
+ domains_ids = [d['id'] for d in self.domains_client.list_domains()[
+ 'domains']]
+ self.assertIn(project['id'], domains_ids)
+
+ @decorators.idempotent_id('1f66dc76-50cc-4741-a200-af984509e480')
def test_project_create_enabled(self):
# Create a project that is enabled
- project_name = data_utils.rand_name('project')
- project = self.client.create_project(
- project_name, enabled=True)['project']
- self.data.projects.append(project)
+ project = self.setup_test_project(enabled=True)
project_id = project['id']
- en1 = project['enabled']
- self.assertTrue(en1, 'Enable should be True in response')
- body = self.client.get_project(project_id)['project']
- en2 = body['enabled']
- self.assertTrue(en2, 'Enable should be True in lookup')
+ self.assertTrue(project['enabled'],
+ 'Enable should be True in response')
+ body = self.projects_client.show_project(project_id)['project']
+ self.assertTrue(body['enabled'], 'Enable should be True in lookup')
- @test.idempotent_id('78f96a9c-e0e0-4ee6-a3ba-fbf6dfd03207')
+ @decorators.idempotent_id('78f96a9c-e0e0-4ee6-a3ba-fbf6dfd03207')
def test_project_create_not_enabled(self):
# Create a project that is not enabled
- project_name = data_utils.rand_name('project')
- project = self.client.create_project(
- project_name, enabled=False)['project']
- self.data.projects.append(project)
- en1 = project['enabled']
- self.assertEqual('false', str(en1).lower(),
+ project = self.setup_test_project(enabled=False)
+ self.assertFalse(project['enabled'],
'Enable should be False in response')
- body = self.client.get_project(project['id'])['project']
- en2 = body['enabled']
- self.assertEqual('false', str(en2).lower(),
+ body = self.projects_client.show_project(project['id'])['project']
+ self.assertFalse(body['enabled'],
'Enable should be False in lookup')
- @test.idempotent_id('f608f368-048c-496b-ad63-d286c26dab6b')
+ @decorators.idempotent_id('f608f368-048c-496b-ad63-d286c26dab6b')
def test_project_update_name(self):
# Update name attribute of a project
p_name1 = data_utils.rand_name('project')
- project = self.client.create_project(p_name1)['project']
- self.data.projects.append(project)
+ project = self.setup_test_project(name=p_name1)
resp1_name = project['name']
p_name2 = data_utils.rand_name('project2')
- body = self.client.update_project(project['id'],
- name=p_name2)['project']
+ body = self.projects_client.update_project(project['id'],
+ name=p_name2)['project']
resp2_name = body['name']
self.assertNotEqual(resp1_name, resp2_name)
- body = self.client.get_project(project['id'])['project']
+ body = self.projects_client.show_project(project['id'])['project']
resp3_name = body['name']
self.assertNotEqual(resp1_name, resp3_name)
self.assertEqual(p_name1, resp1_name)
self.assertEqual(resp2_name, resp3_name)
- @test.idempotent_id('f138b715-255e-4a7d-871d-351e1ef2e153')
+ @decorators.idempotent_id('f138b715-255e-4a7d-871d-351e1ef2e153')
def test_project_update_desc(self):
# Update description attribute of a project
- p_name = data_utils.rand_name('project')
p_desc = data_utils.rand_name('desc')
- project = self.client.create_project(
- p_name, description=p_desc)['project']
- self.data.projects.append(project)
+ project = self.setup_test_project(description=p_desc)
resp1_desc = project['description']
p_desc2 = data_utils.rand_name('desc2')
- body = self.client.update_project(
+ body = self.projects_client.update_project(
project['id'], description=p_desc2)['project']
resp2_desc = body['description']
self.assertNotEqual(resp1_desc, resp2_desc)
- body = self.client.get_project(project['id'])['project']
+ body = self.projects_client.show_project(project['id'])['project']
resp3_desc = body['description']
self.assertNotEqual(resp1_desc, resp3_desc)
self.assertEqual(p_desc, resp1_desc)
self.assertEqual(resp2_desc, resp3_desc)
- @test.idempotent_id('b6b25683-c97f-474d-a595-55d410b68100')
+ @decorators.idempotent_id('b6b25683-c97f-474d-a595-55d410b68100')
def test_project_update_enable(self):
# Update the enabled attribute of a project
- p_name = data_utils.rand_name('project')
p_en = False
- project = self.client.create_project(p_name, enabled=p_en)['project']
- self.data.projects.append(project)
+ project = self.setup_test_project(enabled=p_en)
resp1_en = project['enabled']
p_en2 = True
- body = self.client.update_project(
- project['id'], enabled=p_en2)['project']
+ body = self.projects_client.update_project(project['id'],
+ enabled=p_en2)['project']
resp2_en = body['enabled']
self.assertNotEqual(resp1_en, resp2_en)
- body = self.client.get_project(project['id'])['project']
+ body = self.projects_client.show_project(project['id'])['project']
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)
- @test.idempotent_id('59398d4a-5dc5-4f86-9a4c-c26cc804d6c6')
+ @decorators.idempotent_id('59398d4a-5dc5-4f86-9a4c-c26cc804d6c6')
def test_associate_user_to_project(self):
# Associate a user to a project
# Create a Project
- p_name = data_utils.rand_name('project')
- project = self.client.create_project(p_name)['project']
- self.data.projects.append(project)
+ project = self.setup_test_project()
# Create a User
u_name = data_utils.rand_name('user')
u_desc = u_name + 'description'
u_email = u_name + '@testmail.tm'
u_password = data_utils.rand_password()
- user = self.client.create_user(
- u_name, description=u_desc, password=u_password,
+ user = self.users_client.create_user(
+ name=u_name, description=u_desc, password=u_password,
email=u_email, project_id=project['id'])['user']
# Delete the User at the end of this method
- self.addCleanup(self.client.delete_user, user['id'])
+ self.addCleanup(self.users_client.delete_user, user['id'])
# Get User To validate the user details
- new_user_get = self.client.show_user(user['id'])['user']
+ new_user_get = self.users_client.show_user(user['id'])['user']
# Assert response body of GET
self.assertEqual(u_name, new_user_get['name'])
self.assertEqual(u_desc, new_user_get['description'])
self.assertEqual(project['id'],
new_user_get['project_id'])
self.assertEqual(u_email, new_user_get['email'])
+
+ @decorators.idempotent_id('d1db68b6-aebe-4fa0-b79d-d724d2e21162')
+ def test_project_get_equals_list(self):
+ fields = ['parent_id', 'is_domain', 'description', 'links',
+ 'name', 'enabled', 'domain_id', 'id', 'tags']
+
+ # Tags must be unique, keystone API will reject duplicates
+ tags = ['a', 'c', 'b', 'd']
+
+ # Create a Project, cleanup is handled in the helper
+ project = self.setup_test_project(tags=tags)
+
+ # Show and list for the project
+ project_get = self.projects_client.show_project(
+ project['id'])['project']
+ _projects = self.projects_client.list_projects()['projects']
+ project_list = next(x for x in _projects if x['id'] == project['id'])
+
+ # Assert the list of fields is correct (one is enough to check here)
+ self.assertSetEqual(set(fields), set(project_get.keys()))
+
+ # Ensure the set of tags is identical and match the expected one
+ get_tags = set(project_get.pop("tags"))
+ self.assertSetEqual(get_tags, set(project_list.pop("tags")))
+ self.assertSetEqual(get_tags, set(tags))
+
+ # Ensure all other fields are identical
+ self.assertDictEqual(project_get, project_list)
diff --git a/tempest/api/identity/admin/v3/test_projects_negative.py b/tempest/api/identity/admin/v3/test_projects_negative.py
index 9b60d54..33a9c8c 100644
--- a/tempest/api/identity/admin/v3/test_projects_negative.py
+++ b/tempest/api/identity/admin/v3/test_projects_negative.py
@@ -1,4 +1,4 @@
-# Copyright 2013 OpenStack, LLC
+# Copyright 2013 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -13,71 +13,68 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.identity import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class ProjectsNegativeTestJSON(base.BaseIdentityV3AdminTest):
- @test.attr(type=['negative'])
- @test.idempotent_id('24c49279-45dd-4155-887a-cb738c2385aa')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('24c49279-45dd-4155-887a-cb738c2385aa')
def test_list_projects_by_unauthorized_user(self):
# Non-admin user should not be able to list projects
self.assertRaises(lib_exc.Forbidden,
- self.non_admin_client.list_projects)
+ self.non_admin_projects_client.list_projects)
- @test.attr(type=['negative'])
- @test.idempotent_id('874c3e84-d174-4348-a16b-8c01f599561b')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('874c3e84-d174-4348-a16b-8c01f599561b')
def test_project_create_duplicate(self):
# Project names should be unique
project_name = data_utils.rand_name('project-dup')
- project = self.client.create_project(project_name)['project']
- self.data.projects.append(project)
+ self.setup_test_project(name=project_name)
- self.assertRaises(
- lib_exc.Conflict, self.client.create_project, project_name)
+ self.assertRaises(lib_exc.Conflict,
+ self.projects_client.create_project, project_name)
- @test.attr(type=['negative'])
- @test.idempotent_id('8fba9de2-3e1f-4e77-812a-60cb68f8df13')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('8fba9de2-3e1f-4e77-812a-60cb68f8df13')
def test_create_project_by_unauthorized_user(self):
# Non-admin user should not be authorized to create a project
project_name = data_utils.rand_name('project')
self.assertRaises(
- lib_exc.Forbidden, self.non_admin_client.create_project,
+ lib_exc.Forbidden, self.non_admin_projects_client.create_project,
project_name)
- @test.attr(type=['negative'])
- @test.idempotent_id('7828db17-95e5-475b-9432-9a51b4aa79a9')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('7828db17-95e5-475b-9432-9a51b4aa79a9')
def test_create_project_with_empty_name(self):
# Project name should not be empty
- self.assertRaises(lib_exc.BadRequest, self.client.create_project,
- name='')
+ self.assertRaises(lib_exc.BadRequest,
+ self.projects_client.create_project, name='')
- @test.attr(type=['negative'])
- @test.idempotent_id('502b6ceb-b0c8-4422-bf53-f08fdb21e2f0')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('502b6ceb-b0c8-4422-bf53-f08fdb21e2f0')
def test_create_projects_name_length_over_64(self):
# Project name length should not be greater than 64 characters
project_name = 'a' * 65
- self.assertRaises(lib_exc.BadRequest, self.client.create_project,
- project_name)
+ self.assertRaises(lib_exc.BadRequest,
+ self.projects_client.create_project, project_name)
- @test.attr(type=['negative'])
- @test.idempotent_id('8d68c012-89e0-4394-8d6b-ccd7196def97')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('8d68c012-89e0-4394-8d6b-ccd7196def97')
def test_project_delete_by_unauthorized_user(self):
# Non-admin user should not be able to delete a project
- project_name = data_utils.rand_name('project')
- project = self.client.create_project(project_name)['project']
- self.data.projects.append(project)
+ project = self.setup_test_project()
self.assertRaises(
- lib_exc.Forbidden, self.non_admin_client.delete_project,
+ lib_exc.Forbidden, self.non_admin_projects_client.delete_project,
project['id'])
- @test.attr(type=['negative'])
- @test.idempotent_id('7965b581-60c1-43b7-8169-95d4ab7fc6fb')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('7965b581-60c1-43b7-8169-95d4ab7fc6fb')
def test_delete_non_existent_project(self):
# Attempt to delete a non existent project should fail
- self.assertRaises(lib_exc.NotFound, self.client.delete_project,
+ self.assertRaises(lib_exc.NotFound,
+ self.projects_client.delete_project,
data_utils.rand_uuid_hex())
diff --git a/tempest/api/identity/admin/v3/test_regions.py b/tempest/api/identity/admin/v3/test_regions.py
index 8bba3cb..f22a528 100644
--- a/tempest/api/identity/admin/v3/test_regions.py
+++ b/tempest/api/identity/admin/v3/test_regions.py
@@ -13,11 +13,10 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.identity import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
class RegionsTestJSON(base.BaseIdentityV3AdminTest):
@@ -31,30 +30,27 @@
def resource_setup(cls):
super(RegionsTestJSON, cls).resource_setup()
cls.setup_regions = list()
- for i in range(2):
+ for _ in range(2):
r_description = data_utils.rand_name('description')
region = cls.client.create_region(
description=r_description)['region']
+ cls.addClassResourceCleanup(
+ cls.client.delete_region, region['id'])
cls.setup_regions.append(region)
- @classmethod
- def resource_cleanup(cls):
- for r in cls.setup_regions:
- cls.client.delete_region(r['id'])
- super(RegionsTestJSON, cls).resource_cleanup()
-
- def _delete_region(self, region_id):
- self.client.delete_region(region_id)
- self.assertRaises(lib_exc.NotFound,
- self.client.show_region, region_id)
-
- @test.idempotent_id('56186092-82e4-43f2-b954-91013218ba42')
+ @decorators.idempotent_id('56186092-82e4-43f2-b954-91013218ba42')
def test_create_update_get_delete_region(self):
+ # Create region
r_description = data_utils.rand_name('description')
region = self.client.create_region(
description=r_description,
parent_region_id=self.setup_regions[0]['id'])['region']
- self.addCleanup(self._delete_region, region['id'])
+ # This test will delete the region as part of the validation
+ # procedure, so it needs a different cleanup method that
+ # would be useful in case the tests fails at any point before
+ # reaching the deletion part.
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.client.delete_region, region['id'])
self.assertEqual(r_description, region['description'])
self.assertEqual(self.setup_regions[0]['id'],
region['parent_region_id'])
@@ -72,27 +68,49 @@
self.assertEqual(r_alt_description, region['description'])
self.assertEqual(self.setup_regions[1]['id'],
region['parent_region_id'])
+ # Delete the region
+ self.client.delete_region(region['id'])
+ body = self.client.list_regions()['regions']
+ regions_list = [r['id'] for r in body]
+ self.assertNotIn(region['id'], regions_list)
- @test.attr(type='smoke')
- @test.idempotent_id('2c12c5b5-efcf-4aa5-90c5-bff1ab0cdbe2')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('2c12c5b5-efcf-4aa5-90c5-bff1ab0cdbe2')
def test_create_region_with_specific_id(self):
# Create a region with a specific id
r_region_id = data_utils.rand_uuid()
r_description = data_utils.rand_name('description')
region = self.client.create_region(
region_id=r_region_id, description=r_description)['region']
- self.addCleanup(self._delete_region, region['id'])
+ self.addCleanup(self.client.delete_region, region['id'])
# Asserting Create Region with specific id response body
self.assertEqual(r_region_id, region['id'])
self.assertEqual(r_description, region['description'])
- @test.idempotent_id('d180bf99-544a-445c-ad0d-0c0d27663796')
+ @decorators.idempotent_id('d180bf99-544a-445c-ad0d-0c0d27663796')
def test_list_regions(self):
# Get a list of regions
fetched_regions = self.client.list_regions()['regions']
missing_regions =\
[e for e in self.setup_regions if e not in fetched_regions]
# Asserting List Regions response
- self.assertEqual(0, len(missing_regions),
+ self.assertEmpty(missing_regions,
"Failed to find region %s in fetched list" %
', '.join(str(e) for e in missing_regions))
+
+ @decorators.idempotent_id('2d1057cb-bbde-413a-acdf-e2d265284542')
+ def test_list_regions_filter_by_parent_region_id(self):
+ # Add a sub-region to one of the existing test regions
+ r_description = data_utils.rand_name('description')
+ region = self.client.create_region(
+ description=r_description,
+ parent_region_id=self.setup_regions[0]['id'])['region']
+ self.addCleanup(self.client.delete_region, region['id'])
+ # Get the list of regions filtering with the parent_region_id
+ params = {'parent_region_id': self.setup_regions[0]['id']}
+ fetched_regions = self.client.list_regions(params=params)['regions']
+ # Asserting list regions response
+ self.assertIn(region, fetched_regions)
+ for r in fetched_regions:
+ self.assertEqual(self.setup_regions[0]['id'],
+ r['parent_region_id'])
diff --git a/tempest/api/identity/admin/v3/test_roles.py b/tempest/api/identity/admin/v3/test_roles.py
index ae714aa..62ced19 100644
--- a/tempest/api/identity/admin/v3/test_roles.py
+++ b/tempest/api/identity/admin/v3/test_roles.py
@@ -14,8 +14,13 @@
# under the License.
from tempest.api.identity import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+CONF = config.CONF
class RolesV3TestJSON(base.BaseIdentityV3AdminTest):
@@ -23,120 +28,111 @@
@classmethod
def resource_setup(cls):
super(RolesV3TestJSON, cls).resource_setup()
+ cls.roles = list()
for _ in range(3):
role_name = data_utils.rand_name(name='role')
- role = cls.client.create_role(name=role_name)['role']
- cls.data.v3_roles.append(role)
- cls.fetched_role_ids = list()
+ role = cls.roles_client.create_role(name=role_name)['role']
+ cls.addClassResourceCleanup(cls.roles_client.delete_role,
+ role['id'])
+ cls.roles.append(role)
u_name = data_utils.rand_name('user')
u_desc = '%s description' % u_name
u_email = '%s@testmail.tm' % u_name
cls.u_password = data_utils.rand_password()
- cls.domain = cls.client.create_domain(
- data_utils.rand_name('domain'),
- description=data_utils.rand_name('domain-desc'))['domain']
- cls.project = cls.client.create_project(
+ cls.domain = cls.create_domain()
+ cls.project = cls.projects_client.create_project(
data_utils.rand_name('project'),
description=data_utils.rand_name('project-desc'),
domain_id=cls.domain['id'])['project']
+ cls.addClassResourceCleanup(cls.projects_client.delete_project,
+ cls.project['id'])
cls.group_body = cls.groups_client.create_group(
name=data_utils.rand_name('Group'), project_id=cls.project['id'],
domain_id=cls.domain['id'])['group']
- cls.user_body = cls.client.create_user(
- u_name, description=u_desc, password=cls.u_password,
+ cls.addClassResourceCleanup(cls.groups_client.delete_group,
+ cls.group_body['id'])
+ cls.user_body = cls.users_client.create_user(
+ name=u_name, description=u_desc, password=cls.u_password,
email=u_email, project_id=cls.project['id'],
domain_id=cls.domain['id'])['user']
- cls.role = cls.client.create_role(
+ cls.addClassResourceCleanup(cls.users_client.delete_user,
+ cls.user_body['id'])
+ cls.role = cls.roles_client.create_role(
name=data_utils.rand_name('Role'))['role']
+ cls.addClassResourceCleanup(cls.roles_client.delete_role,
+ cls.role['id'])
- @classmethod
- def resource_cleanup(cls):
- cls.client.delete_role(cls.role['id'])
- cls.groups_client.delete_group(cls.group_body['id'])
- cls.client.delete_user(cls.user_body['id'])
- cls.client.delete_project(cls.project['id'])
- # NOTE(harika-vakadi): It is necessary to disable the domain
- # before deleting,or else it would result in unauthorized error
- cls.client.update_domain(cls.domain['id'], enabled=False)
- cls.client.delete_domain(cls.domain['id'])
- super(RolesV3TestJSON, cls).resource_cleanup()
-
- def _list_assertions(self, body, fetched_role_ids, role_id):
- self.assertEqual(len(body), 1)
- self.assertIn(role_id, fetched_role_ids)
-
- @test.attr(type='smoke')
- @test.idempotent_id('18afc6c0-46cf-4911-824e-9989cc056c3a')
- def test_role_create_update_get_list(self):
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('18afc6c0-46cf-4911-824e-9989cc056c3a')
+ def test_role_create_update_show_list(self):
r_name = data_utils.rand_name('Role')
- role = self.client.create_role(name=r_name)['role']
- self.addCleanup(self.client.delete_role, role['id'])
+ role = self.roles_client.create_role(name=r_name)['role']
+ self.addCleanup(self.roles_client.delete_role, role['id'])
self.assertIn('name', role)
self.assertEqual(role['name'], r_name)
new_name = data_utils.rand_name('NewRole')
- updated_role = self.client.update_role(role['id'],
- name=new_name)['role']
+ updated_role = self.roles_client.update_role(role['id'],
+ name=new_name)['role']
self.assertIn('name', updated_role)
self.assertIn('id', updated_role)
self.assertIn('links', updated_role)
self.assertNotEqual(r_name, updated_role['name'])
- new_role = self.client.show_role(role['id'])['role']
+ new_role = self.roles_client.show_role(role['id'])['role']
self.assertEqual(new_name, new_role['name'])
self.assertEqual(updated_role['id'], new_role['id'])
- roles = self.client.list_roles()['roles']
+ roles = self.roles_client.list_roles()['roles']
self.assertIn(role['id'], [r['id'] for r in roles])
- @test.idempotent_id('c6b80012-fe4a-498b-9ce8-eb391c05169f')
+ @decorators.idempotent_id('c6b80012-fe4a-498b-9ce8-eb391c05169f')
def test_grant_list_revoke_role_to_user_on_project(self):
- self.client.assign_user_role_on_project(
- self.project['id'], self.user_body['id'], self.role['id'])
+ self.roles_client.create_user_role_on_project(self.project['id'],
+ self.user_body['id'],
+ self.role['id'])
- roles = self.client.list_user_roles_on_project(
+ roles = self.roles_client.list_user_roles_on_project(
self.project['id'], self.user_body['id'])['roles']
- for i in roles:
- self.fetched_role_ids.append(i['id'])
+ self.assertEqual(1, len(roles))
+ self.assertEqual(self.role['id'], roles[0]['id'])
- self._list_assertions(roles, self.fetched_role_ids,
- self.role['id'])
-
- self.client.delete_role_from_user_on_project(
+ self.roles_client.check_user_role_existence_on_project(
self.project['id'], self.user_body['id'], self.role['id'])
- @test.idempotent_id('6c9a2940-3625-43a3-ac02-5dcec62ef3bd')
+ self.roles_client.delete_role_from_user_on_project(
+ self.project['id'], self.user_body['id'], self.role['id'])
+
+ @decorators.idempotent_id('6c9a2940-3625-43a3-ac02-5dcec62ef3bd')
def test_grant_list_revoke_role_to_user_on_domain(self):
- self.client.assign_user_role_on_domain(
+ self.roles_client.create_user_role_on_domain(
self.domain['id'], self.user_body['id'], self.role['id'])
- roles = self.client.list_user_roles_on_domain(
+ roles = self.roles_client.list_user_roles_on_domain(
self.domain['id'], self.user_body['id'])['roles']
- for i in roles:
- self.fetched_role_ids.append(i['id'])
+ self.assertEqual(1, len(roles))
+ self.assertEqual(self.role['id'], roles[0]['id'])
- self._list_assertions(roles, self.fetched_role_ids,
- self.role['id'])
-
- self.client.delete_role_from_user_on_domain(
+ self.roles_client.check_user_role_existence_on_domain(
self.domain['id'], self.user_body['id'], self.role['id'])
- @test.idempotent_id('cbf11737-1904-4690-9613-97bcbb3df1c4')
+ self.roles_client.delete_role_from_user_on_domain(
+ self.domain['id'], self.user_body['id'], self.role['id'])
+
+ @decorators.idempotent_id('cbf11737-1904-4690-9613-97bcbb3df1c4')
def test_grant_list_revoke_role_to_group_on_project(self):
# Grant role to group on project
- self.client.assign_group_role_on_project(
+ self.roles_client.create_group_role_on_project(
self.project['id'], self.group_body['id'], self.role['id'])
# List group roles on project
- roles = self.client.list_group_roles_on_project(
+ roles = self.roles_client.list_group_roles_on_project(
self.project['id'], self.group_body['id'])['roles']
- for i in roles:
- self.fetched_role_ids.append(i['id'])
+ self.assertEqual(1, len(roles))
+ self.assertEqual(self.role['id'], roles[0]['id'])
- self._list_assertions(roles, self.fetched_role_ids,
- self.role['id'])
# Add user to group, and insure user has role on project
self.groups_client.add_group_user(self.group_body['id'],
self.user_body['id'])
@@ -150,30 +146,280 @@
roles = body['token']['roles']
self.assertEqual(len(roles), 1)
self.assertEqual(roles[0]['id'], self.role['id'])
- # Revoke role to group on project
- self.client.delete_role_from_group_on_project(
+
+ self.roles_client.check_role_from_group_on_project_existence(
self.project['id'], self.group_body['id'], self.role['id'])
- @test.idempotent_id('4bf8a70b-e785-413a-ad53-9f91ce02faa7')
+ # Revoke role to group on project
+ self.roles_client.delete_role_from_group_on_project(
+ self.project['id'], self.group_body['id'], self.role['id'])
+
+ @decorators.idempotent_id('4bf8a70b-e785-413a-ad53-9f91ce02faa7')
def test_grant_list_revoke_role_to_group_on_domain(self):
- self.client.assign_group_role_on_domain(
+ self.roles_client.create_group_role_on_domain(
self.domain['id'], self.group_body['id'], self.role['id'])
- roles = self.client.list_group_roles_on_domain(
+ roles = self.roles_client.list_group_roles_on_domain(
self.domain['id'], self.group_body['id'])['roles']
- for i in roles:
- self.fetched_role_ids.append(i['id'])
+ self.assertEqual(1, len(roles))
+ self.assertEqual(self.role['id'], roles[0]['id'])
- self._list_assertions(roles, self.fetched_role_ids,
- self.role['id'])
-
- self.client.delete_role_from_group_on_domain(
+ self.roles_client.check_role_from_group_on_domain_existence(
self.domain['id'], self.group_body['id'], self.role['id'])
- @test.idempotent_id('f5654bcc-08c4-4f71-88fe-05d64e06de94')
+ self.roles_client.delete_role_from_group_on_domain(
+ self.domain['id'], self.group_body['id'], self.role['id'])
+
+ @decorators.idempotent_id('f5654bcc-08c4-4f71-88fe-05d64e06de94')
def test_list_roles(self):
# Return a list of all roles
- body = self.client.list_roles()['roles']
- found = [role for role in body if role in self.data.v3_roles]
- self.assertEqual(len(found), len(self.data.v3_roles))
+ body = self.roles_client.list_roles()['roles']
+ found = [role for role in body if role in self.roles]
+ self.assertEqual(len(found), len(self.roles))
+
+ def _create_implied_role(self, prior_role_id, implies_role_id,
+ ignore_not_found=False):
+ self.roles_client.create_role_inference_rule(
+ prior_role_id, implies_role_id)
+ if ignore_not_found:
+ self.addCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ self.roles_client.delete_role_inference_rule,
+ prior_role_id,
+ implies_role_id)
+ else:
+ self.addCleanup(
+ self.roles_client.delete_role_inference_rule,
+ prior_role_id,
+ implies_role_id)
+
+ @decorators.idempotent_id('c90c316c-d706-4728-bcba-eb1912081b69')
+ def test_implied_roles_create_check_show_delete(self):
+ prior_role_id = self.roles[0]['id']
+ implies_role_id = self.roles[1]['id']
+
+ # Create an inference rule from prior_role to implies_role
+ self._create_implied_role(prior_role_id, implies_role_id,
+ ignore_not_found=True)
+
+ # Check if the inference rule exists
+ self.roles_client.check_role_inference_rule(
+ prior_role_id, implies_role_id)
+
+ # Show the inference rule and check its elements
+ resp_body = self.roles_client.show_role_inference_rule(
+ prior_role_id, implies_role_id)
+ self.assertIn('role_inference', resp_body)
+ role_inference = resp_body['role_inference']
+ for key1 in ['prior_role', 'implies']:
+ self.assertIn(key1, role_inference)
+ for key2 in ['id', 'links', 'name']:
+ self.assertIn(key2, role_inference[key1])
+
+ # Delete the inference rule
+ self.roles_client.delete_role_inference_rule(
+ prior_role_id, implies_role_id)
+ # Check if the inference rule no longer exists
+ self.assertRaises(
+ lib_exc.NotFound,
+ self.roles_client.show_role_inference_rule,
+ prior_role_id,
+ implies_role_id)
+
+ @decorators.idempotent_id('dc6f5959-b74d-4e30-a9e5-a8255494ff00')
+ def test_roles_hierarchy(self):
+ # Create inference rule from "roles[0]" to "role[1]"
+ self._create_implied_role(
+ self.roles[0]['id'], self.roles[1]['id'])
+
+ # Create inference rule from "roles[0]" to "role[2]"
+ self._create_implied_role(
+ self.roles[0]['id'], self.roles[2]['id'])
+
+ # Create inference rule from "roles[2]" to "role"
+ self._create_implied_role(
+ self.roles[2]['id'], self.role['id'])
+
+ # Listing inferences rules from "roles[2]" should only return "role"
+ rules = self.roles_client.list_role_inferences_rules(
+ self.roles[2]['id'])['role_inference']
+ self.assertEqual(1, len(rules['implies']))
+ self.assertEqual(self.role['id'], rules['implies'][0]['id'])
+
+ # Listing inferences rules from "roles[0]" should return "roles[1]" and
+ # "roles[2]" (only direct rules are listed)
+ rules = self.roles_client.list_role_inferences_rules(
+ self.roles[0]['id'])['role_inference']
+ implies_ids = [role['id'] for role in rules['implies']]
+ self.assertEqual(2, len(implies_ids))
+ self.assertIn(self.roles[1]['id'], implies_ids)
+ self.assertIn(self.roles[2]['id'], implies_ids)
+
+ @decorators.idempotent_id('c8828027-df48-4021-95df-b65b92c7429e')
+ def test_assignments_for_implied_roles_create_delete(self):
+ # Create a grant using "roles[0]"
+ self.roles_client.create_user_role_on_project(
+ self.project['id'], self.user_body['id'], self.roles[0]['id'])
+ self.addCleanup(
+ self.roles_client.delete_role_from_user_on_project,
+ self.project['id'], self.user_body['id'], self.roles[0]['id'])
+
+ # Create an inference rule from "roles[0]" to "roles[1]"
+ self._create_implied_role(self.roles[0]['id'], self.roles[1]['id'],
+ ignore_not_found=True)
+
+ # In the effective list of role assignments, both prior role and
+ # implied role should be present. This means that a user can
+ # authenticate using both roles (both roles will be present
+ # in the token).
+ params = {'scope.project.id': self.project['id'],
+ 'user.id': self.user_body['id']}
+ role_assignments = self.role_assignments.list_role_assignments(
+ effective=True, **params)['role_assignments']
+ self.assertEqual(2, len(role_assignments))
+
+ roles_ids = [assignment['role']['id']
+ for assignment in role_assignments]
+ self.assertIn(self.roles[0]['id'], roles_ids)
+ self.assertIn(self.roles[1]['id'], roles_ids)
+
+ # After deleting the implied role, only the assignment with "roles[0]"
+ # should be present.
+ self.roles_client.delete_role_inference_rule(
+ self.roles[0]['id'], self.roles[1]['id'])
+
+ role_assignments = self.role_assignments.list_role_assignments(
+ effective=True, **params)['role_assignments']
+ self.assertEqual(1, len(role_assignments))
+
+ roles_ids = [assignment['role']['id']
+ for assignment in role_assignments]
+ self.assertIn(self.roles[0]['id'], roles_ids)
+
+ @decorators.idempotent_id('d92a41d2-5501-497a-84bb-6e294330e8f8')
+ def test_domain_roles_create_delete(self):
+ domain_role = self.roles_client.create_role(
+ name=data_utils.rand_name('domain_role'),
+ domain_id=self.domain['id'])['role']
+ self.addCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ self.roles_client.delete_role,
+ domain_role['id'])
+
+ domain_roles = self.roles_client.list_roles(
+ domain_id=self.domain['id'])['roles']
+ self.assertEqual(1, len(domain_roles))
+ self.assertIn(domain_role, domain_roles)
+
+ self.roles_client.delete_role(domain_role['id'])
+ domain_roles = self.roles_client.list_roles(
+ domain_id=self.domain['id'])['roles']
+ self.assertEmpty(domain_roles)
+
+ @decorators.idempotent_id('eb1e1c24-1bc4-4d47-9748-e127a1852c82')
+ def test_implied_domain_roles(self):
+ # Create two roles in the same domain
+ domain_role1 = self.setup_test_role(domain_id=self.domain['id'])
+ domain_role2 = self.setup_test_role(domain_id=self.domain['id'])
+
+ # Check if we can create an inference rule from roles in the same
+ # domain
+ self._create_implied_role(domain_role1['id'], domain_role2['id'])
+
+ # Create another role in a different domain
+ domain2 = self.setup_test_domain()
+ domain_role3 = self.setup_test_role(domain_id=domain2['id'])
+
+ # Check if we can create cross domain implied roles
+ self._create_implied_role(domain_role1['id'], domain_role3['id'])
+
+ # Finally, we also should be able to create an implied from a
+ # domain role to a global one
+ self._create_implied_role(domain_role1['id'], self.role['id'])
+
+ # The contrary is not true: we can't create an inference rule
+ # from a global role to a domain role
+ self.assertRaises(
+ lib_exc.Forbidden,
+ self.roles_client.create_role_inference_rule,
+ self.role['id'],
+ domain_role1['id'])
+
+ @decorators.idempotent_id('3859df7e-5b78-4e4d-b10e-214c8953842a')
+ def test_assignments_for_domain_roles(self):
+ domain_role = self.setup_test_role(domain_id=self.domain['id'])
+
+ # Create a grant using "domain_role"
+ self.roles_client.create_user_role_on_project(
+ self.project['id'], self.user_body['id'], domain_role['id'])
+ self.addCleanup(
+ self.roles_client.delete_role_from_user_on_project,
+ self.project['id'], self.user_body['id'], domain_role['id'])
+
+ # NOTE(rodrigods): Regular roles would appear in the effective
+ # list of role assignments (meaning the role would be returned in
+ # a token) as a result from the grant above. This is not the case
+ # for domain roles, they should not appear in the effective role
+ # assignments list.
+ params = {'scope.project.id': self.project['id'],
+ 'user.id': self.user_body['id']}
+ role_assignments = self.role_assignments.list_role_assignments(
+ effective=True, **params)['role_assignments']
+ self.assertEmpty(role_assignments)
+
+ @decorators.idempotent_id('3748c316-c18f-4b08-997b-c60567bc6235')
+ def test_list_all_implied_roles(self):
+ # Create inference rule from "roles[0]" to "roles[1]"
+ self._create_implied_role(
+ self.roles[0]['id'], self.roles[1]['id'])
+
+ # Create inference rule from "roles[0]" to "roles[2]"
+ self._create_implied_role(
+ self.roles[0]['id'], self.roles[2]['id'])
+
+ # Create inference rule from "roles[2]" to "role"
+ self._create_implied_role(
+ self.roles[2]['id'], self.role['id'])
+
+ rules = self.roles_client.list_all_role_inference_rules()[
+ 'role_inferences']
+
+ # NOTE(jaosorior): With the work related to the define-default-roles
+ # blueprint, we now have 'admin', 'member' and 'reader' by default. So
+ # we filter every other implied role to only take into account the ones
+ # relates to this test.
+ relevant_roles = (self.roles[0]['id'], self.roles[1]['id'],
+ self.roles[2]['id'], self.role['id'])
+
+ def is_implied_role_relevant(rule):
+ return any(r for r in rule['implies'] if r['id'] in relevant_roles)
+
+ relevant_rules = filter(is_implied_role_relevant, rules)
+ # Sort the rules by the number of inferences, since there should be 1
+ # inference between "roles[2]" and "role" and 2 inferences for
+ # "roles[0]": between "roles[1]" and "roles[2]".
+ sorted_rules = sorted(relevant_rules, key=lambda r: len(r['implies']))
+
+ self.assertEqual(2, len(sorted_rules))
+ # Check that only 1 inference rule exists between "roles[2]" and "role"
+ self.assertEqual(1, len(sorted_rules[0]['implies']))
+ # Check that 2 inference rules exist for "roles[0]": one between
+ # "roles[1]" and one between "roles[2]".
+ self.assertEqual(2, len(sorted_rules[1]['implies']))
+
+ # Check that "roles[2]" is the "prior_role" and that "role" is the
+ # "implies" role.
+ self.assertEqual(self.roles[2]['id'],
+ sorted_rules[0]['prior_role']['id'])
+ self.assertEqual(self.role['id'],
+ sorted_rules[0]['implies'][0]['id'])
+
+ # Check that "roles[0]" is the "prior_role" and that "roles[1]" and
+ # "roles[2]" are the "implies" roles.
+ self.assertEqual(self.roles[0]['id'],
+ sorted_rules[1]['prior_role']['id'])
+
+ implies_ids = [r['id'] for r in sorted_rules[1]['implies']]
+ self.assertIn(self.roles[1]['id'], implies_ids)
+ self.assertIn(self.roles[2]['id'], implies_ids)
diff --git a/tempest/api/identity/admin/v3/test_services.py b/tempest/api/identity/admin/v3/test_services.py
index c6e3df4..5afeb98 100644
--- a/tempest/api/identity/admin/v3/test_services.py
+++ b/tempest/api/identity/admin/v3/test_services.py
@@ -13,11 +13,10 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.identity import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class ServicesTestJSON(base.BaseIdentityV3AdminTest):
@@ -29,8 +28,8 @@
self.assertRaises(lib_exc.NotFound, self.services_client.show_service,
service_id)
- @test.attr(type='smoke')
- @test.idempotent_id('5193aad5-bcb7-411d-85b0-b3b61b96ef06')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('5193aad5-bcb7-411d-85b0-b3b61b96ef06')
def test_create_update_get_service(self):
# Creating a Service
name = data_utils.rand_name('service')
@@ -62,7 +61,7 @@
self.assertEqual(resp2_desc, resp3_desc)
self.assertDictContainsSubset(update_service, fetched_service)
- @test.idempotent_id('d1dcb1a1-2b6b-4da8-bbb8-5532ef6e8269')
+ @decorators.idempotent_id('d1dcb1a1-2b6b-4da8-bbb8-5532ef6e8269')
def test_create_service_without_description(self):
# Create a service only with name and type
name = data_utils.rand_name('service')
@@ -70,25 +69,33 @@
service = self.services_client.create_service(
type=serv_type, name=name)['service']
self.addCleanup(self.services_client.delete_service, service['id'])
- self.assertIn('id', service)
expected_data = {'name': name, 'type': serv_type}
self.assertDictContainsSubset(expected_data, service)
- @test.idempotent_id('e55908e8-360e-439e-8719-c3230a3e179e')
+ @decorators.idempotent_id('e55908e8-360e-439e-8719-c3230a3e179e')
def test_list_services(self):
# Create, List, Verify and Delete Services
service_ids = list()
+ service_types = list()
for _ in range(3):
- name = data_utils.rand_name('service')
- serv_type = data_utils.rand_name('type')
+ name = data_utils.rand_name(self.__class__.__name__ + '-Service')
+ serv_type = data_utils.rand_name(self.__class__.__name__ + '-Type')
create_service = self.services_client.create_service(
type=serv_type, name=name)['service']
self.addCleanup(self.services_client.delete_service,
create_service['id'])
service_ids.append(create_service['id'])
+ service_types.append(serv_type)
# List and Verify Services
services = self.services_client.list_services()['services']
fetched_ids = [service['id'] for service in services]
found = [s for s in fetched_ids if s in service_ids]
self.assertEqual(len(found), len(service_ids))
+
+ # Check that filtering by service type works.
+ for serv_type in service_types:
+ fetched_services = self.services_client.list_services(
+ type=serv_type)['services']
+ self.assertEqual(1, len(fetched_services))
+ self.assertEqual(serv_type, fetched_services[0]['type'])
diff --git a/tempest/api/identity/admin/v3/test_tokens.py b/tempest/api/identity/admin/v3/test_tokens.py
index b1446cf..0845407 100644
--- a/tempest/api/identity/admin/v3/test_tokens.py
+++ b/tempest/api/identity/admin/v3/test_tokens.py
@@ -13,31 +13,35 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
+import six
from tempest.api.identity import base
-from tempest.common.utils import data_utils
-from tempest import test
+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 TokensV3TestJSON(base.BaseIdentityV3AdminTest):
- @test.idempotent_id('0f9f5a5f-d5cd-4a86-8a5b-c5ded151f212')
+ credentials = ['primary', 'admin', 'alt']
+
+ @decorators.idempotent_id('0f9f5a5f-d5cd-4a86-8a5b-c5ded151f212')
def test_tokens(self):
# Valid user's token is authenticated
# Create a User
u_name = data_utils.rand_name('user')
u_desc = '%s-description' % u_name
- u_email = '%s@testmail.tm' % u_name
u_password = data_utils.rand_password()
- user = self.client.create_user(
- u_name, description=u_desc, password=u_password,
- email=u_email)['user']
- self.addCleanup(self.client.delete_user, user['id'])
+ user = self.create_test_user(
+ name=u_name, description=u_desc, password=u_password)
# Perform Authentication
resp = self.token.auth(user_id=user['id'],
password=u_password).response
subject_token = resp['x-subject-token']
+ self.client.check_token_existence(subject_token)
# Perform GET Token
token_details = self.client.show_token(subject_token)['token']
self.assertEqual(resp['x-subject-token'], subject_token)
@@ -45,10 +49,10 @@
self.assertEqual(token_details['user']['name'], u_name)
# Perform Delete Token
self.client.delete_token(subject_token)
- self.assertRaises(lib_exc.NotFound, self.client.show_token,
+ self.assertRaises(lib_exc.NotFound, self.client.check_token_existence,
subject_token)
- @test.idempotent_id('565fa210-1da1-4563-999b-f7b5b67cf112')
+ @decorators.idempotent_id('565fa210-1da1-4563-999b-f7b5b67cf112')
def test_rescope_token(self):
"""Rescope a token.
@@ -59,32 +63,28 @@
"""
# Create a user.
- user_name = data_utils.rand_name(name='user')
user_password = data_utils.rand_password()
- user = self.client.create_user(user_name,
- password=user_password)['user']
- self.addCleanup(self.client.delete_user, user['id'])
+ user = self.create_test_user(password=user_password)
# Create a couple projects
project1_name = data_utils.rand_name(name='project')
- project1 = self.client.create_project(project1_name)['project']
- self.addCleanup(self.client.delete_project, project1['id'])
+ project1 = self.setup_test_project(name=project1_name)
project2_name = data_utils.rand_name(name='project')
- project2 = self.client.create_project(project2_name)['project']
- self.addCleanup(self.client.delete_project, project2['id'])
+ project2 = self.setup_test_project(name=project2_name)
+ self.addCleanup(self.projects_client.delete_project, project2['id'])
# Create a role
- role_name = data_utils.rand_name(name='role')
- role = self.client.create_role(name=role_name)['role']
- self.addCleanup(self.client.delete_role, role['id'])
+ role = self.setup_test_role()
# Grant the user the role on both projects.
- self.client.assign_user_role(project1['id'], user['id'],
- role['id'])
+ self.roles_client.create_user_role_on_project(project1['id'],
+ user['id'],
+ role['id'])
- self.client.assign_user_role(project2['id'], user['id'],
- role['id'])
+ self.roles_client.create_user_role_on_project(project2['id'],
+ user['id'],
+ role['id'])
# Get an unscoped token.
token_auth = self.token.auth(user_id=user['id'],
@@ -94,28 +94,28 @@
orig_expires_at = token_auth['token']['expires_at']
orig_user = token_auth['token']['user']
- self.assertIsInstance(token_auth['token']['expires_at'], unicode)
- self.assertIsInstance(token_auth['token']['issued_at'], unicode)
+ self.assertIsInstance(token_auth['token']['expires_at'], six.text_type)
+ self.assertIsInstance(token_auth['token']['issued_at'], six.text_type)
self.assertEqual(['password'], token_auth['token']['methods'])
self.assertEqual(user['id'], token_auth['token']['user']['id'])
self.assertEqual(user['name'], token_auth['token']['user']['name'])
- self.assertEqual('default',
+ self.assertEqual(CONF.identity.default_domain_id,
token_auth['token']['user']['domain']['id'])
- self.assertEqual('Default',
- token_auth['token']['user']['domain']['name'])
+ self.assertIsNotNone(token_auth['token']['user']['domain']['name'])
self.assertNotIn('catalog', token_auth['token'])
self.assertNotIn('project', token_auth['token'])
self.assertNotIn('roles', token_auth['token'])
# Use the unscoped token to get a scoped token.
- token_auth = self.token.auth(token=token_id,
- project_name=project1_name,
- project_domain_name='Default')
+ token_auth = self.token.auth(
+ token=token_id,
+ project_name=project1_name,
+ project_domain_id=CONF.identity.default_domain_id)
token1_id = token_auth.response['x-subject-token']
self.assertEqual(orig_expires_at, token_auth['token']['expires_at'],
'Expiration time should match original token')
- self.assertIsInstance(token_auth['token']['issued_at'], unicode)
+ self.assertIsInstance(token_auth['token']['issued_at'], six.text_type)
self.assertEqual(set(['password', 'token']),
set(token_auth['token']['methods']))
self.assertEqual(orig_user, token_auth['token']['user'],
@@ -125,10 +125,9 @@
token_auth['token']['project']['id'])
self.assertEqual(project1['name'],
token_auth['token']['project']['name'])
- self.assertEqual('default',
+ self.assertEqual(CONF.identity.default_domain_id,
token_auth['token']['project']['domain']['id'])
- self.assertEqual('Default',
- token_auth['token']['project']['domain']['name'])
+ self.assertIsNotNone(token_auth['token']['project']['domain']['name'])
self.assertEqual(1, len(token_auth['token']['roles']))
self.assertEqual(role['id'], token_auth['token']['roles'][0]['id'])
self.assertEqual(role['name'], token_auth['token']['roles'][0]['name'])
@@ -137,11 +136,107 @@
self.client.delete_token(token1_id)
# Now get another scoped token using the unscoped token.
- token_auth = self.token.auth(token=token_id,
- project_name=project2_name,
- project_domain_name='Default')
+ token_auth = self.token.auth(
+ token=token_id,
+ project_name=project2_name,
+ project_domain_id=CONF.identity.default_domain_id)
self.assertEqual(project2['id'],
token_auth['token']['project']['id'])
self.assertEqual(project2['name'],
token_auth['token']['project']['name'])
+
+ @decorators.idempotent_id('08ed85ce-2ba8-4864-b442-bcc61f16ae89')
+ def test_get_available_project_scopes(self):
+ manager_project_id = self.os_primary.credentials.project_id
+ admin_user_id = self.os_admin.credentials.user_id
+ admin_role_id = self.get_role_by_name(CONF.identity.admin_role)['id']
+
+ # Grant the user the role on both projects.
+ self.roles_client.create_user_role_on_project(
+ manager_project_id, admin_user_id, admin_role_id)
+ self.addCleanup(
+ self.roles_client.delete_role_from_user_on_project,
+ manager_project_id, admin_user_id, admin_role_id)
+
+ assigned_project_ids = [self.os_admin.credentials.project_id,
+ manager_project_id]
+
+ # Get available project scopes
+ available_projects = self.client.list_auth_projects()['projects']
+
+ # Create list to save fetched project IDs
+ fetched_project_ids = [i['id'] for i in available_projects]
+
+ # verifying the project ids in list
+ missing_project_ids = \
+ [p for p in assigned_project_ids if p not in fetched_project_ids]
+ self.assertEmpty(missing_project_ids,
+ "Failed to find project_ids %s in fetched list" %
+ ', '.join(missing_project_ids))
+
+ @decorators.idempotent_id('ec5ecb05-af64-4c04-ac86-4d9f6f12f185')
+ def test_get_available_domain_scopes(self):
+ # Test for verifying that listing domain scopes for a user works if
+ # the user has a domain role or belongs to a group that has a domain
+ # role. For this test, admin client is used to add roles to alt user,
+ # which performs API calls, to avoid 401 Unauthorized errors.
+ alt_user_id = self.os_alt.credentials.user_id
+
+ def _create_user_domain_role_for_alt_user():
+ domain_id = self.setup_test_domain()['id']
+ role_id = self.setup_test_role()['id']
+
+ # Create a role association between the user and domain.
+ self.roles_client.create_user_role_on_domain(
+ domain_id, alt_user_id, role_id)
+ self.addCleanup(
+ self.roles_client.delete_role_from_user_on_domain,
+ domain_id, alt_user_id, role_id)
+
+ return domain_id
+
+ def _create_group_domain_role_for_alt_user():
+ domain_id = self.setup_test_domain()['id']
+ role_id = self.setup_test_role()['id']
+
+ # Create a group.
+ group_name = data_utils.rand_name('Group')
+ group_id = self.groups_client.create_group(
+ name=group_name, domain_id=domain_id)['group']['id']
+ self.addCleanup(self.groups_client.delete_group, group_id)
+
+ # Add the alt user to the group.
+ self.groups_client.add_group_user(group_id, alt_user_id)
+ self.addCleanup(self.groups_client.delete_group_user,
+ group_id, alt_user_id)
+
+ # Create a role association between the group and domain.
+ self.roles_client.create_group_role_on_domain(
+ domain_id, group_id, role_id)
+ self.addCleanup(
+ self.roles_client.delete_role_from_group_on_domain,
+ domain_id, group_id, role_id)
+
+ return domain_id
+
+ # Add the alt user to 2 random domains and 2 random groups
+ # with randomized domains and roles.
+ assigned_domain_ids = []
+ for _ in range(2):
+ domain_id = _create_user_domain_role_for_alt_user()
+ assigned_domain_ids.append(domain_id)
+ domain_id = _create_group_domain_role_for_alt_user()
+ assigned_domain_ids.append(domain_id)
+
+ # Get available domain scopes for the alt user.
+ available_domains = self.os_alt.identity_v3_client.list_auth_domains()[
+ 'domains']
+ fetched_domain_ids = [i['id'] for i in available_domains]
+
+ # Verify the expected domain IDs are in the list.
+ missing_domain_ids = \
+ [p for p in assigned_domain_ids if p not in fetched_domain_ids]
+ self.assertEmpty(missing_domain_ids,
+ "Failed to find domain_ids %s in fetched list"
+ % ", ".join(missing_domain_ids))
diff --git a/tempest/api/identity/admin/v3/test_trusts.py b/tempest/api/identity/admin/v3/test_trusts.py
index 85961b4..2530072 100644
--- a/tempest/api/identity/admin/v3/test_trusts.py
+++ b/tempest/api/identity/admin/v3/test_trusts.py
@@ -14,74 +14,83 @@
import re
from oslo_utils import timeutils
-from tempest_lib import exceptions as lib_exc
from tempest.api.identity import base
from tempest import clients
from tempest.common import credentials_factory as common_creds
-from tempest.common.utils import data_utils
from tempest import config
-from tempest import test
+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 BaseTrustsV3Test(base.BaseIdentityV3AdminTest):
+class TrustsV3TestJSON(base.BaseIdentityV3AdminTest):
+
+ @classmethod
+ def skip_checks(cls):
+ super(TrustsV3TestJSON, cls).skip_checks()
+ if not CONF.identity_feature_enabled.trust:
+ raise cls.skipException("Trusts aren't enabled")
def setUp(self):
- super(BaseTrustsV3Test, self).setUp()
+ super(TrustsV3TestJSON, self).setUp()
# Use alt_username as the trustee
- if not CONF.identity_feature_enabled.trust:
- raise self.skipException("Trusts aren't enabled")
-
self.trust_id = None
+ self.create_trustor_and_roles()
+ self.addCleanup(self.cleanup_user_and_roles)
def tearDown(self):
if self.trust_id:
# Do the delete in tearDown not addCleanup - we want the test to
# fail in the event there is a bug which causes undeletable trusts
self.delete_trust()
- super(BaseTrustsV3Test, self).tearDown()
+ super(TrustsV3TestJSON, self).tearDown()
def create_trustor_and_roles(self):
# create a project that trusts will be granted on
- self.trustor_project_name = data_utils.rand_name(name='project')
- project = self.client.create_project(self.trustor_project_name,
- domain_id='default')['project']
+ trustor_project_name = data_utils.rand_name(name='project')
+ project = self.projects_client.create_project(
+ trustor_project_name,
+ domain_id=CONF.identity.default_domain_id)['project']
self.trustor_project_id = project['id']
self.assertIsNotNone(self.trustor_project_id)
# Create a trustor User
- self.trustor_username = data_utils.rand_name('user')
- u_desc = self.trustor_username + 'description'
- u_email = self.trustor_username + '@testmail.xx'
- self.trustor_password = data_utils.rand_password()
- user = self.client.create_user(
- self.trustor_username,
+ trustor_username = data_utils.rand_name('user')
+ u_desc = trustor_username + 'description'
+ u_email = trustor_username + '@testmail.xx'
+ trustor_password = data_utils.rand_password()
+ user = self.users_client.create_user(
+ name=trustor_username,
description=u_desc,
- password=self.trustor_password,
+ password=trustor_password,
email=u_email,
project_id=self.trustor_project_id,
- domain_id='default')['user']
+ domain_id=CONF.identity.default_domain_id)['user']
self.trustor_user_id = user['id']
# And two roles, one we'll delegate and one we won't
self.delegated_role = data_utils.rand_name('DelegatedRole')
self.not_delegated_role = data_utils.rand_name('NotDelegatedRole')
- role = self.client.create_role(name=self.delegated_role)['role']
+ role = self.roles_client.create_role(name=self.delegated_role)['role']
self.delegated_role_id = role['id']
- role = self.client.create_role(name=self.not_delegated_role)['role']
+ role = self.roles_client.create_role(
+ name=self.not_delegated_role)['role']
self.not_delegated_role_id = role['id']
# Assign roles to trustor
- self.client.assign_user_role(self.trustor_project_id,
- self.trustor_user_id,
- self.delegated_role_id)
- self.client.assign_user_role(self.trustor_project_id,
- self.trustor_user_id,
- self.not_delegated_role_id)
+ self.roles_client.create_user_role_on_project(
+ self.trustor_project_id,
+ self.trustor_user_id,
+ self.delegated_role_id)
+ self.roles_client.create_user_role_on_project(
+ self.trustor_project_id,
+ self.trustor_user_id,
+ self.not_delegated_role_id)
# Get trustee user ID, use the demo user
trustee_username = self.non_admin_client.user
@@ -91,23 +100,24 @@
# Initialize a new client with the trustor credentials
creds = common_creds.get_credentials(
identity_version='v3',
- username=self.trustor_username,
- password=self.trustor_password,
- user_domain_id='default',
- tenant_name=self.trustor_project_name,
- project_domain_id='default')
+ username=trustor_username,
+ password=trustor_password,
+ user_domain_id=CONF.identity.default_domain_id,
+ tenant_name=trustor_project_name,
+ project_domain_id=CONF.identity.default_domain_id,
+ domain_id=CONF.identity.default_domain_id)
os = clients.Manager(credentials=creds)
- self.trustor_client = os.identity_v3_client
+ self.trustor_client = os.trusts_client
def cleanup_user_and_roles(self):
if self.trustor_user_id:
- self.client.delete_user(self.trustor_user_id)
+ self.users_client.delete_user(self.trustor_user_id)
if self.trustor_project_id:
- self.client.delete_project(self.trustor_project_id)
+ self.projects_client.delete_project(self.trustor_project_id)
if self.delegated_role_id:
- self.client.delete_role(self.delegated_role_id)
+ self.roles_client.delete_role(self.delegated_role_id)
if self.not_delegated_role_id:
- self.client.delete_role(self.not_delegated_role_id)
+ self.roles_client.delete_role(self.not_delegated_role_id)
def create_trust(self, impersonate=True, expires=None):
@@ -139,8 +149,8 @@
self.assertEqual(self.delegated_role, trust['roles'][0]['name'])
self.assertEqual(1, len(trust['roles']))
- def get_trust(self):
- trust_get = self.trustor_client.get_trust(self.trust_id)['trust']
+ def show_trust(self):
+ trust_get = self.trustor_client.show_trust(self.trust_id)['trust']
return trust_get
def validate_role(self, role):
@@ -155,12 +165,12 @@
def check_trust_roles(self):
# Check we find the delegated role
- roles_get = self.trustor_client.get_trust_roles(
+ roles_get = self.trustor_client.list_trust_roles(
self.trust_id)['roles']
self.assertEqual(1, len(roles_get))
self.validate_role(roles_get[0])
- role_get = self.trustor_client.get_trust_role(
+ role_get = self.trustor_client.show_trust_role(
self.trust_id, self.delegated_role_id)['role']
self.validate_role(role_get)
@@ -169,7 +179,7 @@
# And that we don't find not_delegated_role
self.assertRaises(lib_exc.NotFound,
- self.trustor_client.get_trust_role,
+ self.trustor_client.show_trust_role,
self.trust_id,
self.not_delegated_role_id)
@@ -181,43 +191,35 @@
def delete_trust(self):
self.trustor_client.delete_trust(self.trust_id)
self.assertRaises(lib_exc.NotFound,
- self.trustor_client.get_trust,
+ self.trustor_client.show_trust,
self.trust_id)
self.trust_id = None
-
-class TrustsV3TestJSON(BaseTrustsV3Test):
-
- def setUp(self):
- super(TrustsV3TestJSON, self).setUp()
- self.create_trustor_and_roles()
- self.addCleanup(self.cleanup_user_and_roles)
-
- @test.idempotent_id('5a0a91a4-baef-4a14-baba-59bf4d7fcace')
+ @decorators.idempotent_id('5a0a91a4-baef-4a14-baba-59bf4d7fcace')
def test_trust_impersonate(self):
# Test case to check we can create, get and delete a trust
# updates are not supported for trusts
trust = self.create_trust()
self.validate_trust(trust)
- trust_get = self.get_trust()
+ trust_get = self.show_trust()
self.validate_trust(trust_get)
self.check_trust_roles()
- @test.idempotent_id('ed2a8779-a7ac-49dc-afd7-30f32f936ed2')
+ @decorators.idempotent_id('ed2a8779-a7ac-49dc-afd7-30f32f936ed2')
def test_trust_noimpersonate(self):
# Test case to check we can create, get and delete a trust
# with impersonation=False
trust = self.create_trust(impersonate=False)
self.validate_trust(trust, impersonate=False)
- trust_get = self.get_trust()
+ trust_get = self.show_trust()
self.validate_trust(trust_get, impersonate=False)
self.check_trust_roles()
- @test.idempotent_id('0ed14b66-cefd-4b5c-a964-65759453e292')
+ @decorators.idempotent_id('0ed14b66-cefd-4b5c-a964-65759453e292')
def test_trust_expire(self):
# Test case to check we can create, get and delete a trust
# with an expiry specified
@@ -228,21 +230,23 @@
# For example, when creating a trust, we will set the expiry time of
# the trust to 2015-02-17T17:34:01.907051Z. However, if we make a GET
# request on the trust, the response will contain the time rounded up
- # to 2015-02-17T17:34:02.000000Z. That is why we shouldn't set flag
- # "subsecond" to True when we invoke timeutils.isotime(...) to avoid
- # problems with rounding.
- expires_str = timeutils.isotime(at=expires_at)
+ # to 2015-02-17T17:34:02.000000Z. That is why we set microsecond to
+ # 0 when we invoke isoformat to avoid problems with rounding.
+ expires_at = expires_at.replace(microsecond=0)
+ # NOTE(ekhugen) Python datetime does not support military timezones
+ # since we used UTC we'll add the Z so our compare works.
+ expires_str = expires_at.isoformat() + 'Z'
trust = self.create_trust(expires=expires_str)
self.validate_trust(trust, expires=expires_str)
- trust_get = self.get_trust()
+ trust_get = self.show_trust()
self.validate_trust(trust_get, expires=expires_str)
self.check_trust_roles()
- @test.idempotent_id('3e48f95d-e660-4fa9-85e0-5a3d85594384')
+ @decorators.idempotent_id('3e48f95d-e660-4fa9-85e0-5a3d85594384')
def test_trust_expire_invalid(self):
# Test case to check we can check an invalid expiry time
# is rejected with the correct error
@@ -252,19 +256,30 @@
self.create_trust,
expires=expires_str)
- @test.idempotent_id('6268b345-87ca-47c0-9ce3-37792b43403a')
+ @decorators.idempotent_id('6268b345-87ca-47c0-9ce3-37792b43403a')
def test_get_trusts_query(self):
self.create_trust()
- trusts_get = self.trustor_client.get_trusts(
+ trusts_get = self.trustor_client.list_trusts(
trustor_user_id=self.trustor_user_id)['trusts']
self.assertEqual(1, len(trusts_get))
self.validate_trust(trusts_get[0], summary=True)
- @test.attr(type='smoke')
- @test.idempotent_id('4773ebd5-ecbf-4255-b8d8-b63e6f72b65d')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('4773ebd5-ecbf-4255-b8d8-b63e6f72b65d')
def test_get_trusts_all(self):
+
+ # Simple function that can be used for cleanup
+ def set_scope(auth_provider, scope):
+ auth_provider.scope = scope
+
self.create_trust()
- trusts_get = self.client.get_trusts()['trusts']
+ # Listing trusts can be done by trustor, by trustee, or without
+ # any filter if scoped to a project, so we must ensure token scope is
+ # project for this test.
+ original_scope = self.os_admin.auth_provider.scope
+ set_scope(self.os_admin.auth_provider, 'project')
+ self.addCleanup(set_scope, self.os_admin.auth_provider, original_scope)
+ trusts_get = self.trusts_client.list_trusts()['trusts']
trusts = [t for t in trusts_get
if t['id'] == self.trust_id]
self.assertEqual(1, len(trusts))
diff --git a/tempest/api/identity/admin/v3/test_users.py b/tempest/api/identity/admin/v3/test_users.py
index a402b3f..3813568 100644
--- a/tempest/api/identity/admin/v3/test_users.py
+++ b/tempest/api/identity/admin/v3/test_users.py
@@ -15,14 +15,20 @@
import time
+import testtools
+
from tempest.api.identity import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+
+
+CONF = config.CONF
class UsersV3TestJSON(base.BaseIdentityV3AdminTest):
- @test.idempotent_id('b537d090-afb9-4519-b95d-270b0708e87e')
+ @decorators.idempotent_id('b537d090-afb9-4519-b95d-270b0708e87e')
def test_user_update(self):
# Test case to check if updating of user attributes is successful.
# Creating first user
@@ -30,127 +36,131 @@
u_desc = u_name + 'description'
u_email = u_name + '@testmail.tm'
u_password = data_utils.rand_password()
- user = self.client.create_user(
- u_name, description=u_desc, password=u_password,
+ user = self.users_client.create_user(
+ name=u_name, description=u_desc, password=u_password,
email=u_email, enabled=False)['user']
# Delete the User at the end of this method
- self.addCleanup(self.client.delete_user, user['id'])
- # Creating second project for updation
- project = self.client.create_project(
- data_utils.rand_name('project'),
- description=data_utils.rand_name('project-desc'))['project']
- # Delete the Project at the end of this method
- self.addCleanup(self.client.delete_project, project['id'])
- # Updating user details with new values
- u_name2 = data_utils.rand_name('user2')
- u_email2 = u_name2 + '@testmail.tm'
- u_description2 = u_name2 + ' description'
- update_user = self.client.update_user(
- user['id'], name=u_name2, description=u_description2,
- project_id=project['id'],
- email=u_email2, enabled=False)['user']
- self.assertEqual(u_name2, update_user['name'])
- self.assertEqual(u_description2, update_user['description'])
- self.assertEqual(project['id'],
- update_user['project_id'])
- self.assertEqual(u_email2, update_user['email'])
- self.assertEqual(False, update_user['enabled'])
- # GET by id after updation
- new_user_get = self.client.show_user(user['id'])['user']
- # Assert response body of GET after updation
- self.assertEqual(u_name2, new_user_get['name'])
- self.assertEqual(u_description2, new_user_get['description'])
- self.assertEqual(project['id'],
- new_user_get['project_id'])
- self.assertEqual(u_email2, new_user_get['email'])
- self.assertEqual(False, new_user_get['enabled'])
+ self.addCleanup(self.users_client.delete_user, user['id'])
- @test.idempotent_id('2d223a0e-e457-4a70-9fb1-febe027a0ff9')
+ # Creating second project for updation
+ project = self.setup_test_project()
+
+ # Updating user details with new values
+ update_kwargs = {'name': data_utils.rand_name('user2'),
+ 'description': data_utils.rand_name('desc2'),
+ 'project_id': project['id'],
+ 'email': 'user2@testmail.tm',
+ 'enabled': False}
+ updated_user = self.users_client.update_user(
+ user['id'], **update_kwargs)['user']
+ for field in update_kwargs:
+ self.assertEqual(update_kwargs[field], updated_user[field])
+
+ # GET by id after updating
+ new_user_get = self.users_client.show_user(user['id'])['user']
+ # Assert response body of GET after updation
+ for field in update_kwargs:
+ self.assertEqual(update_kwargs[field], new_user_get[field])
+
+ @decorators.idempotent_id('2d223a0e-e457-4a70-9fb1-febe027a0ff9')
def test_update_user_password(self):
# Creating User to check password updation
u_name = data_utils.rand_name('user')
original_password = data_utils.rand_password()
- user = self.client.create_user(
- u_name, password=original_password)['user']
+ user = self.users_client.create_user(
+ name=u_name, password=original_password)['user']
# Delete the User at the end all test methods
- self.addCleanup(self.client.delete_user, user['id'])
+ self.addCleanup(self.users_client.delete_user, user['id'])
# Update user with new password
new_password = data_utils.rand_password()
- self.client.update_user_password(user['id'], password=new_password,
- original_password=original_password)
- # TODO(lbragstad): Sleeping after the response status has been checked
- # and the body loaded as JSON allows requests to fail-fast. The sleep
- # is necessary because keystone will err on the side of security and
- # invalidate tokens within a small margin of error (within the same
- # wall clock second) after a revocation event is issued (such as a
- # password change). Remove this once keystone and Fernet support
- # sub-second precision, see bug 1517697 for more details.
+ self.users_client.update_user_password(
+ user['id'], password=new_password,
+ original_password=original_password)
+ # NOTE(morganfainberg): Fernet tokens are not subsecond aware and
+ # Keystone should only be precise to the second. Sleep to ensure
+ # we are passing the second boundary.
time.sleep(1)
resp = self.token.auth(user_id=user['id'],
password=new_password).response
subject_token = resp['x-subject-token']
# Perform GET Token to verify and confirm password is updated
token_details = self.client.show_token(subject_token)['token']
- self.assertEqual(resp['x-subject-token'], subject_token)
self.assertEqual(token_details['user']['id'], user['id'])
self.assertEqual(token_details['user']['name'], u_name)
- @test.idempotent_id('a831e70c-e35b-430b-92ed-81ebbc5437b8')
+ @decorators.idempotent_id('a831e70c-e35b-430b-92ed-81ebbc5437b8')
def test_list_user_projects(self):
# List the projects that a user has access upon
assigned_project_ids = list()
fetched_project_ids = list()
- u_project = self.client.create_project(
- data_utils.rand_name('project'),
- description=data_utils.rand_name('project-desc'))['project']
- # Delete the Project at the end of this method
- self.addCleanup(self.client.delete_project, u_project['id'])
+ u_project = self.setup_test_project()
# Create a user.
u_name = data_utils.rand_name('user')
u_desc = u_name + 'description'
u_email = u_name + '@testmail.tm'
u_password = data_utils.rand_password()
- user_body = self.client.create_user(
- u_name, description=u_desc, password=u_password,
+ user_body = self.users_client.create_user(
+ name=u_name, description=u_desc, password=u_password,
email=u_email, enabled=False, project_id=u_project['id'])['user']
# Delete the User at the end of this method
- self.addCleanup(self.client.delete_user, user_body['id'])
+ self.addCleanup(self.users_client.delete_user, user_body['id'])
# Creating Role
- role_body = self.client.create_role(
- name=data_utils.rand_name('role'))['role']
- # Delete the Role at the end of this method
- self.addCleanup(self.client.delete_role, role_body['id'])
+ role_body = self.setup_test_role()
- user = self.client.show_user(user_body['id'])['user']
- role = self.client.show_role(role_body['id'])['role']
- for i in range(2):
+ user = self.users_client.show_user(user_body['id'])['user']
+ role = self.roles_client.show_role(role_body['id'])['role']
+ for _ in range(2):
# Creating project so as to assign role
- project_body = self.client.create_project(
- data_utils.rand_name('project'),
- description=data_utils.rand_name('project-desc'))['project']
- project = self.client.get_project(project_body['id'])['project']
- # Delete the Project at the end of this method
- self.addCleanup(self.client.delete_project, project_body['id'])
+ project_body = self.setup_test_project()
+ project = self.projects_client.show_project(
+ project_body['id'])['project']
# Assigning roles to user on project
- self.client.assign_user_role(project['id'],
- user['id'],
- role['id'])
+ self.roles_client.create_user_role_on_project(project['id'],
+ user['id'],
+ role['id'])
assigned_project_ids.append(project['id'])
- body = self.client.list_user_projects(user['id'])['projects']
+ body = self.users_client.list_user_projects(user['id'])['projects']
for i in body:
fetched_project_ids.append(i['id'])
# verifying the project ids in list
missing_projects =\
[p for p in assigned_project_ids
if p not in fetched_project_ids]
- self.assertEqual(0, len(missing_projects),
+ self.assertEmpty(missing_projects,
"Failed to find project %s in fetched list" %
', '.join(m_project for m_project
in missing_projects))
- @test.idempotent_id('c10dcd90-461d-4b16-8e23-4eb836c00644')
+ @decorators.idempotent_id('c10dcd90-461d-4b16-8e23-4eb836c00644')
def test_get_user(self):
# Get a user detail
- self.data.setup_test_v3_user()
- user = self.client.show_user(self.data.v3_user['id'])['user']
- self.assertEqual(self.data.v3_user['id'], user['id'])
+ user = self.setup_test_user()
+ fetched_user = self.users_client.show_user(user['id'])['user']
+ self.assertEqual(user['id'], fetched_user['id'])
+
+ @testtools.skipUnless(CONF.identity_feature_enabled.security_compliance,
+ 'Security compliance not available.')
+ @decorators.idempotent_id('568cd46c-ee6c-4ab4-a33a-d3791931979e')
+ def test_password_history_not_enforced_in_admin_reset(self):
+ old_password = self.os_primary.credentials.password
+ user_id = self.os_primary.credentials.user_id
+
+ new_password = data_utils.rand_password()
+ self.users_client.update_user(user_id, password=new_password)
+ # To be safe, we add this cleanup to restore the original password in
+ # case something goes wrong before it is restored later.
+ self.addCleanup(
+ self.users_client.update_user, user_id, password=old_password)
+
+ # Check authorization with new password
+ self.token.auth(user_id=user_id, password=new_password)
+
+ if CONF.identity.user_unique_last_password_count > 1:
+ # The password history is not enforced via the admin reset route.
+ # We can set the same password.
+ self.users_client.update_user(user_id, password=new_password)
+
+ # Restore original password
+ self.users_client.update_user(user_id, password=old_password)
+ # Check authorization with old password
+ self.token.auth(user_id=user_id, password=old_password)
diff --git a/tempest/api/identity/admin/v3/test_users_negative.py b/tempest/api/identity/admin/v3/test_users_negative.py
index 4c80bda..11dcdb0 100644
--- a/tempest/api/identity/admin/v3/test_users_negative.py
+++ b/tempest/api/identity/admin/v3/test_users_negative.py
@@ -13,34 +13,37 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.identity import base
-from tempest.common.utils import data_utils
-from tempest import test
+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 UsersNegativeTest(base.BaseIdentityV3AdminTest):
- @test.attr(type=['negative'])
- @test.idempotent_id('e75f006c-89cc-477b-874d-588e4eab4b17')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('e75f006c-89cc-477b-874d-588e4eab4b17')
def test_create_user_for_non_existent_domain(self):
# Attempt to create a user in a non-existent domain should fail
u_name = data_utils.rand_name('user')
u_email = u_name + '@testmail.tm'
u_password = data_utils.rand_password()
- self.assertRaises(lib_exc.NotFound, self.client.create_user,
- u_name, u_password,
+ self.assertRaises(lib_exc.NotFound, self.users_client.create_user,
+ name=u_name, password=u_password,
email=u_email,
domain_id=data_utils.rand_uuid_hex())
- @test.attr(type=['negative'])
- @test.idempotent_id('b3c9fccc-4134-46f5-b600-1da6fb0a3b1f')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('b3c9fccc-4134-46f5-b600-1da6fb0a3b1f')
def test_authentication_for_disabled_user(self):
# Attempt to authenticate for disabled user should fail
- self.data.setup_test_v3_user()
- self.disable_user(self.data.test_user)
+ password = data_utils.rand_password()
+ user = self.setup_test_user(password)
+ self.disable_user(user['name'], user['domain_id'])
self.assertRaises(lib_exc.Unauthorized, self.token.auth,
- username=self.data.test_user,
- password=self.data.test_password,
- user_domain_id='default')
+ username=user['name'],
+ password=password,
+ user_domain_id=CONF.identity.default_domain_id)
diff --git a/tempest/api/identity/base.py b/tempest/api/identity/base.py
index d68a204..68f2c07 100644
--- a/tempest/api/identity/base.py
+++ b/tempest/api/identity/base.py
@@ -13,24 +13,26 @@
# License for the specific language governing permissions and limitations
# under the License.
-from oslo_log import log as logging
-from tempest_lib import exceptions as lib_exc
-
-from tempest.common import credentials_factory as common_creds
-from tempest.common.utils import data_utils
from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
import tempest.test
CONF = config.CONF
-LOG = logging.getLogger(__name__)
class BaseIdentityTest(tempest.test.BaseTestCase):
@classmethod
+ def setup_credentials(cls):
+ # Create no network resources for these test.
+ cls.set_network_resources()
+ super(BaseIdentityTest, cls).setup_credentials()
+
+ @classmethod
def disable_user(cls, user_name):
user = cls.get_user_by_name(user_name)
- cls.client.enable_disable_user(user['id'], enabled=False)
+ cls.users_client.update_user_enabled(user['id'], enabled=False)
@classmethod
def disable_tenant(cls, tenant_name):
@@ -38,10 +40,14 @@
cls.tenants_client.update_tenant(tenant['id'], enabled=False)
@classmethod
- def get_user_by_name(cls, name):
- users = cls.client.list_users()['users']
+ def get_user_by_name(cls, name, domain_id=None):
+ if domain_id:
+ params = {'domain_id': domain_id}
+ users = cls.users_client.list_users(**params)['users']
+ else:
+ users = cls.users_client.list_users()['users']
user = [u for u in users if u['name'] == name]
- if len(user) > 0:
+ if user:
return user[0]
@classmethod
@@ -49,18 +55,46 @@
try:
tenants = cls.tenants_client.list_tenants()['tenants']
except AttributeError:
- tenants = cls.client.list_projects()['projects']
+ tenants = cls.projects_client.list_projects()['projects']
tenant = [t for t in tenants if t['name'] == name]
- if len(tenant) > 0:
+ if tenant:
return tenant[0]
@classmethod
def get_role_by_name(cls, name):
roles = cls.roles_client.list_roles()['roles']
role = [r for r in roles if r['name'] == name]
- if len(role) > 0:
+ if role:
return role[0]
+ def create_test_user(self, **kwargs):
+ if kwargs.get('password', None) is None:
+ kwargs['password'] = data_utils.rand_password()
+ if 'name' not in kwargs:
+ kwargs['name'] = data_utils.rand_name('test_user')
+ if 'email' not in kwargs:
+ kwargs['email'] = kwargs['name'] + '@testmail.tm'
+
+ user = self.users_client.create_user(**kwargs)['user']
+ # Delete the user at the end of the test
+ self.addCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ self.users_client.delete_user, user['id'])
+ return user
+
+ def setup_test_role(self, name=None, domain_id=None):
+ """Set up a test role."""
+ params = {'name': name or data_utils.rand_name('test_role')}
+ if domain_id:
+ params['domain_id'] = domain_id
+
+ role = self.roles_client.create_role(**params)['role']
+ # Delete the role at the end of the test
+ self.addCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ self.roles_client.delete_role, role['id'])
+ return role
+
class BaseIdentityV2Test(BaseIdentityTest):
@@ -73,45 +107,69 @@
@classmethod
def setup_clients(cls):
super(BaseIdentityV2Test, cls).setup_clients()
- cls.non_admin_client = cls.os.identity_public_client
- cls.non_admin_token_client = cls.os.token_client
- cls.non_admin_tenants_client = cls.os.tenants_public_client
- cls.non_admin_roles_client = cls.os.roles_public_client
-
- @classmethod
- def resource_setup(cls):
- super(BaseIdentityV2Test, cls).resource_setup()
-
- @classmethod
- def resource_cleanup(cls):
- super(BaseIdentityV2Test, cls).resource_cleanup()
+ cls.non_admin_client = cls.os_primary.identity_public_client
+ cls.non_admin_token_client = cls.os_primary.token_client
+ cls.non_admin_tenants_client = cls.os_primary.tenants_public_client
+ cls.non_admin_users_client = cls.os_primary.users_public_client
class BaseIdentityV2AdminTest(BaseIdentityV2Test):
credentials = ['primary', 'admin']
+ # NOTE(andreaf) Identity tests work with credentials, so it is safer
+ # for them to always use disposable credentials. Forcing dynamic creds
+ # on regular identity tests would be however to restrictive, since it
+ # would prevent any identity test from being executed against clouds where
+ # admin credentials are not available.
+ # Since All admin tests require admin credentials to be
+ # executed, so this will not impact the ability to execute tests.
+ force_tenant_isolation = True
+
+ @classmethod
+ def skip_checks(cls):
+ super(BaseIdentityV2AdminTest, cls).skip_checks()
+ if not CONF.identity_feature_enabled.api_v2_admin:
+ raise cls.skipException('Identity v2 admin not available')
+
@classmethod
def setup_clients(cls):
super(BaseIdentityV2AdminTest, cls).setup_clients()
- cls.client = cls.os_adm.identity_client
- cls.non_admin_client = cls.os.identity_client
- cls.token_client = cls.os_adm.token_client
- cls.tenants_client = cls.os_adm.tenants_client
- cls.non_admin_tenants_client = cls.os.tenants_client
- cls.roles_client = cls.os_adm.roles_client
- cls.non_admin_roles_client = cls.os.roles_client
+ cls.client = cls.os_admin.identity_client
+ cls.non_admin_client = cls.os_primary.identity_client
+ cls.token_client = cls.os_admin.token_client
+ cls.tenants_client = cls.os_admin.tenants_client
+ cls.non_admin_tenants_client = cls.os_primary.tenants_client
+ cls.roles_client = cls.os_admin.roles_client
+ cls.non_admin_roles_client = cls.os_primary.roles_client
+ cls.users_client = cls.os_admin.users_client
+ cls.non_admin_users_client = cls.os_primary.users_client
+ cls.services_client = cls.os_admin.identity_services_client
+ cls.endpoints_client = cls.os_admin.endpoints_client
@classmethod
def resource_setup(cls):
super(BaseIdentityV2AdminTest, cls).resource_setup()
- cls.data = DataGenerator(cls.client, cls.tenants_client,
- cls.roles_client)
+ cls.projects_client = cls.tenants_client
- @classmethod
- def resource_cleanup(cls):
- cls.data.teardown_all()
- super(BaseIdentityV2AdminTest, cls).resource_cleanup()
+ def setup_test_user(self, password=None):
+ """Set up a test user."""
+ tenant = self.setup_test_tenant()
+ user = self.create_test_user(tenantId=tenant['id'], password=password)
+ return user
+
+ def setup_test_tenant(self, **kwargs):
+ """Set up a test tenant."""
+ if 'name' not in kwargs:
+ kwargs['name'] = data_utils.rand_name('test_tenant')
+ if 'description' not in kwargs:
+ kwargs['description'] = data_utils.rand_name('desc')
+ tenant = self.projects_client.create_tenant(**kwargs)['tenant']
+ # Delete the tenant at the end of the test
+ self.addCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ self.tenants_client.delete_tenant, tenant['id'])
+ return tenant
class BaseIdentityV3Test(BaseIdentityTest):
@@ -125,189 +183,138 @@
@classmethod
def setup_clients(cls):
super(BaseIdentityV3Test, cls).setup_clients()
- cls.non_admin_client = cls.os.identity_v3_client
- cls.non_admin_token = cls.os.token_v3_client
-
- @classmethod
- def resource_cleanup(cls):
- super(BaseIdentityV3Test, cls).resource_cleanup()
+ cls.non_admin_client = cls.os_primary.identity_v3_client
+ cls.non_admin_users_client = cls.os_primary.users_v3_client
+ cls.non_admin_token = cls.os_primary.token_v3_client
+ cls.non_admin_projects_client = cls.os_primary.projects_client
+ 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):
credentials = ['primary', 'admin']
+ # NOTE(andreaf) Identity tests work with credentials, so it is safer
+ # for them to always use disposable credentials. Forcing dynamic creds
+ # on regular identity tests would be however to restrictive, since it
+ # would prevent any identity test from being executed against clouds where
+ # admin credentials are not available.
+ # Since All admin tests require admin credentials to be
+ # executed, so this will not impact the ability to execute tests.
+ force_tenant_isolation = True
+
@classmethod
def setup_clients(cls):
super(BaseIdentityV3AdminTest, cls).setup_clients()
- cls.client = cls.os_adm.identity_v3_client
- cls.token = cls.os_adm.token_v3_client
- cls.endpoints_client = cls.os_adm.endpoints_client
- cls.regions_client = cls.os_adm.regions_client
- cls.services_client = cls.os_adm.identity_services_client
- cls.policies_client = cls.os_adm.policies_client
- cls.creds_client = cls.os_adm.credentials_client
- cls.groups_client = cls.os_adm.groups_client
+ cls.client = cls.os_admin.identity_v3_client
+ cls.domains_client = cls.os_admin.domains_client
+ cls.users_client = cls.os_admin.users_v3_client
+ cls.trusts_client = cls.os_admin.trusts_client
+ cls.roles_client = cls.os_admin.roles_v3_client
+ cls.inherited_roles_client = cls.os_admin.inherited_roles_client
+ cls.token = cls.os_admin.token_v3_client
+ cls.endpoints_client = cls.os_admin.endpoints_v3_client
+ cls.regions_client = cls.os_admin.regions_client
+ cls.services_client = cls.os_admin.identity_services_v3_client
+ cls.policies_client = cls.os_admin.policies_client
+ cls.creds_client = cls.os_admin.credentials_client
+ cls.groups_client = cls.os_admin.groups_client
+ cls.projects_client = cls.os_admin.projects_client
+ cls.role_assignments = cls.os_admin.role_assignments_client
+ cls.oauth_consumers_client = cls.os_admin.oauth_consumers_client
+ cls.oauth_token_client = cls.os_admin.oauth_token_client
+ 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
- cls.data = DataGenerator(cls.client)
+ if CONF.identity.admin_domain_scope:
+ # NOTE(andreaf) When keystone policy requires it, the identity
+ # admin clients for these tests shall use 'domain' scoped tokens.
+ # As the client manager is already created by the base class,
+ # we set the scope for the inner auth provider.
+ cls.os_admin.auth_provider.scope = 'domain'
@classmethod
- def resource_cleanup(cls):
- cls.data.teardown_all()
- super(BaseIdentityV3AdminTest, cls).resource_cleanup()
+ def disable_user(cls, user_name, domain_id=None):
+ user = cls.get_user_by_name(user_name, domain_id)
+ cls.users_client.update_user(user['id'], name=user_name, enabled=False)
@classmethod
- def get_user_by_name(cls, name):
- users = cls.client.list_users()['users']
- user = [u for u in users if u['name'] == name]
- if len(user) > 0:
- return user[0]
+ def create_domain(cls, **kwargs):
+ """Create a domain."""
+ if 'name' not in kwargs:
+ kwargs['name'] = data_utils.rand_name('test_domain')
+ if 'description' not in kwargs:
+ kwargs['description'] = data_utils.rand_name('desc')
+ domain = cls.domains_client.create_domain(**kwargs)['domain']
+ cls.addClassResourceCleanup(test_utils.call_and_ignore_notfound_exc,
+ cls.delete_domain, domain['id'])
+ return domain
@classmethod
- def get_tenant_by_name(cls, name):
- tenants = cls.client.list_projects()['projects']
- tenant = [t for t in tenants if t['name'] == name]
- if len(tenant) > 0:
- return tenant[0]
-
- @classmethod
- def get_role_by_name(cls, name):
- roles = cls.client.list_roles()['roles']
- role = [r for r in roles if r['name'] == name]
- if len(role) > 0:
- return role[0]
-
- @classmethod
- def disable_user(cls, user_name):
- user = cls.get_user_by_name(user_name)
- cls.client.update_user(user['id'], user_name, enabled=False)
-
- def delete_domain(self, domain_id):
+ def delete_domain(cls, domain_id):
# NOTE(mpavlase) It is necessary to disable the domain before deleting
# otherwise it raises Forbidden exception
- self.client.update_domain(domain_id, enabled=False)
- self.client.delete_domain(domain_id)
+ cls.domains_client.update_domain(domain_id, enabled=False)
+ cls.domains_client.delete_domain(domain_id)
+
+ def setup_test_user(self, password=None):
+ """Set up a test user."""
+ project = self.setup_test_project()
+ user = self.create_test_user(project_id=project['id'],
+ password=password)
+ return user
+
+ def setup_test_project(self, **kwargs):
+ """Set up a test project."""
+ if 'name' not in kwargs:
+ kwargs['name'] = data_utils.rand_name('test_project')
+ if 'description' not in kwargs:
+ kwargs['description'] = data_utils.rand_name('test_description')
+ project = self.projects_client.create_project(**kwargs)['project']
+ # Delete the project at the end of the test
+ self.addCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ self.projects_client.delete_project, project['id'])
+ return project
+
+ def setup_test_domain(self):
+ """Set up a test domain."""
+ domain = self.create_domain()
+ # Delete the domain at the end of the test
+ self.addCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ self.delete_domain, domain['id'])
+ return domain
-class DataGenerator(object):
+class BaseApplicationCredentialsV3Test(BaseIdentityV3Test):
- def __init__(self, client, tenants_client=None, roles_client=None):
- self.client = client
- # TODO(dmellado) split Datagenerator for v2 and v3
- self.tenants_client = tenants_client
- self.roles_client = roles_client
- self.users = []
- self.tenants = []
- self.roles = []
- self.role_name = None
- self.v3_users = []
- self.projects = []
- self.v3_roles = []
- self.domains = []
+ @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")
- @property
- def test_credentials(self):
- return common_creds.get_credentials(username=self.test_user,
- user_id=self.user['id'],
- password=self.test_password,
- tenant_name=self.test_tenant,
- tenant_id=self.tenant['id'])
+ @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 setup_test_user(self):
- """Set up a test user."""
- self.setup_test_tenant()
- self.test_user = data_utils.rand_name('test_user')
- self.test_password = data_utils.rand_password()
- self.test_email = self.test_user + '@testmail.tm'
- self.user = self.client.create_user(self.test_user,
- self.test_password,
- self.tenant['id'],
- self.test_email)['user']
- self.users.append(self.user)
-
- def setup_test_tenant(self):
- """Set up a test tenant."""
- self.test_tenant = data_utils.rand_name('test_tenant')
- self.test_description = data_utils.rand_name('desc')
- self.tenant = self.tenants_client.create_tenant(
- name=self.test_tenant,
- description=self.test_description)['tenant']
- self.tenants.append(self.tenant)
-
- def setup_test_role(self):
- """Set up a test role."""
- self.test_role = data_utils.rand_name('role')
- self.role = self.roles_client.create_role(
- name=self.test_role)['role']
- self.roles.append(self.role)
-
- def setup_test_v3_user(self):
- """Set up a test v3 user."""
- self.setup_test_project()
- self.test_user = data_utils.rand_name('test_user')
- self.test_password = data_utils.rand_password()
- self.test_email = self.test_user + '@testmail.tm'
- self.v3_user = self.client.create_user(
- self.test_user,
- password=self.test_password,
- project_id=self.project['id'],
- email=self.test_email)['user']
- self.v3_users.append(self.v3_user)
-
- def setup_test_project(self):
- """Set up a test project."""
- self.test_project = data_utils.rand_name('test_project')
- self.test_description = data_utils.rand_name('desc')
- self.project = self.client.create_project(
- name=self.test_project,
- description=self.test_description)['project']
- self.projects.append(self.project)
-
- def setup_test_v3_role(self):
- """Set up a test v3 role."""
- self.test_role = data_utils.rand_name('role')
- self.v3_role = self.client.create_role(name=self.test_role)['role']
- self.v3_roles.append(self.v3_role)
-
- def setup_test_domain(self):
- """Set up a test domain."""
- self.test_domain = data_utils.rand_name('test_domain')
- self.test_description = data_utils.rand_name('desc')
- self.domain = self.client.create_domain(
- name=self.test_domain,
- description=self.test_description)['domain']
- self.domains.append(self.domain)
-
- @staticmethod
- def _try_wrapper(func, item, **kwargs):
- try:
- if kwargs:
- func(item['id'], **kwargs)
- else:
- func(item['id'])
- except lib_exc.NotFound:
- pass
- except Exception:
- LOG.exception("Unexpected exception occurred in %s deletion."
- " But ignored here." % item['id'])
-
- def teardown_all(self):
- # NOTE(masayukig): v3 client doesn't have v2 method.
- # (e.g. delete_tenant) So we need to check resources existence
- # before using client methods.
- for user in self.users:
- self._try_wrapper(self.client.delete_user, user)
- for tenant in self.tenants:
- self._try_wrapper(self.tenants_client.delete_tenant, tenant)
- for role in self.roles:
- self._try_wrapper(self.roles_client.delete_role, role)
- for v3_user in self.v3_users:
- self._try_wrapper(self.client.delete_user, v3_user)
- for v3_project in self.projects:
- self._try_wrapper(self.client.delete_project, v3_project)
- for v3_role in self.v3_roles:
- self._try_wrapper(self.client.delete_role, v3_role)
- for domain in self.domains:
- self._try_wrapper(self.client.update_domain, domain,
- enabled=False)
- self._try_wrapper(self.client.delete_domain, domain)
+ 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/test_extension.py b/tempest/api/identity/test_extension.py
deleted file mode 100644
index 01e5661..0000000
--- a/tempest/api/identity/test_extension.py
+++ /dev/null
@@ -1,31 +0,0 @@
-# Copyright 2014 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.identity import base
-from tempest import test
-
-
-class ExtensionTestJSON(base.BaseIdentityV2AdminTest):
-
- @test.idempotent_id('85f3f661-f54c-4d48-b563-72ae952b9383')
- def test_list_extensions(self):
- # List all the extensions
- body = self.non_admin_client.list_extensions()['extensions']['values']
- self.assertNotEmpty(body)
- keys = ['name', 'updated', 'alias', 'links',
- 'namespace', 'description']
- for value in body:
- for key in keys:
- self.assertIn(key, value)
diff --git a/tempest/api/identity/v2/test_api_discovery.py b/tempest/api/identity/v2/test_api_discovery.py
index ca807a4..5b9d38c 100644
--- a/tempest/api/identity/v2/test_api_discovery.py
+++ b/tempest/api/identity/v2/test_api_discovery.py
@@ -14,14 +14,14 @@
# under the License.
from tempest.api.identity import base
-from tempest import test
+from tempest.lib import decorators
class TestApiDiscovery(base.BaseIdentityV2Test):
"""Tests for API discovery features."""
- @test.attr(type='smoke')
- @test.idempotent_id('ea889a68-a15f-4166-bfb1-c12456eae853')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('ea889a68-a15f-4166-bfb1-c12456eae853')
def test_api_version_resources(self):
descr = self.non_admin_client.show_api_description()['version']
expected_resources = ('id', 'links', 'media-types', 'status',
@@ -31,8 +31,8 @@
for res in expected_resources:
self.assertIn(res, keys)
- @test.attr(type='smoke')
- @test.idempotent_id('007a0be0-78fe-4fdb-bbee-e9216cc17bb2')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('007a0be0-78fe-4fdb-bbee-e9216cc17bb2')
def test_api_media_types(self):
descr = self.non_admin_client.show_api_description()['version']
# Get MIME type bases and descriptions
@@ -46,8 +46,8 @@
for s_type in supported_types:
self.assertIn(s_type, media_types)
- @test.attr(type='smoke')
- @test.idempotent_id('77fd6be0-8801-48e6-b9bf-38cdd2f253ec')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('77fd6be0-8801-48e6-b9bf-38cdd2f253ec')
def test_api_version_statuses(self):
descr = self.non_admin_client.show_api_description()['version']
status = descr['status'].lower()
diff --git a/tempest/api/identity/v2/test_ec2_credentials.py b/tempest/api/identity/v2/test_ec2_credentials.py
index 88161a3..237e728 100644
--- a/tempest/api/identity/v2/test_ec2_credentials.py
+++ b/tempest/api/identity/v2/test_ec2_credentials.py
@@ -13,10 +13,10 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.identity import base
-from tempest import test
+from tempest.common import utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class EC2CredentialsTest(base.BaseIdentityV2Test):
@@ -24,55 +24,54 @@
@classmethod
def skip_checks(cls):
super(EC2CredentialsTest, cls).skip_checks()
- if not test.is_extension_enabled('OS-EC2', 'identity'):
+ if not utils.is_extension_enabled('OS-EC2', 'identity'):
msg = "OS-EC2 identity extension not enabled."
raise cls.skipException(msg)
@classmethod
def resource_setup(cls):
super(EC2CredentialsTest, cls).resource_setup()
- cls.creds = cls.os.credentials
+ cls.creds = cls.os_primary.credentials
- @test.idempotent_id('b580fab9-7ae9-46e8-8138-417260cb6f9f')
- def test_create_ec2_credentials(self):
- """Create user ec2 credentials."""
- resp = self.non_admin_client.create_user_ec2_credentials(
- self.creds.credentials.user_id,
- tenant_id=self.creds.credentials.tenant_id)["credential"]
+ @decorators.idempotent_id('b580fab9-7ae9-46e8-8138-417260cb6f9f')
+ def test_create_ec2_credential(self):
+ """Create user ec2 credential."""
+ resp = self.non_admin_users_client.create_user_ec2_credential(
+ self.creds.user_id,
+ tenant_id=self.creds.tenant_id)["credential"]
access = resp['access']
self.addCleanup(
- self.non_admin_client.delete_user_ec2_credentials,
- self.creds.credentials.user_id, access)
+ self.non_admin_users_client.delete_user_ec2_credential,
+ self.creds.user_id, access)
self.assertNotEmpty(resp['access'])
self.assertNotEmpty(resp['secret'])
- self.assertEqual(self.creds.credentials.user_id, resp['user_id'])
- self.assertEqual(self.creds.credentials.tenant_id, resp['tenant_id'])
+ self.assertEqual(self.creds.user_id, resp['user_id'])
+ self.assertEqual(self.creds.tenant_id, resp['tenant_id'])
- @test.idempotent_id('9e2ea42f-0a4f-468c-a768-51859ce492e0')
+ @decorators.idempotent_id('9e2ea42f-0a4f-468c-a768-51859ce492e0')
def test_list_ec2_credentials(self):
"""Get the list of user ec2 credentials."""
created_creds = []
- fetched_creds = []
# create first ec2 credentials
- creds1 = self.non_admin_client.create_user_ec2_credentials(
- self.creds.credentials.user_id,
- tenant_id=self.creds.credentials.tenant_id)["credential"]
+ creds1 = self.non_admin_users_client.create_user_ec2_credential(
+ self.creds.user_id,
+ tenant_id=self.creds.tenant_id)["credential"]
created_creds.append(creds1['access'])
# create second ec2 credentials
- creds2 = self.non_admin_client.create_user_ec2_credentials(
- self.creds.credentials.user_id,
- tenant_id=self.creds.credentials.tenant_id)["credential"]
+ creds2 = self.non_admin_users_client.create_user_ec2_credential(
+ self.creds.user_id,
+ tenant_id=self.creds.tenant_id)["credential"]
created_creds.append(creds2['access'])
# add credentials to be cleaned up
self.addCleanup(
- self.non_admin_client.delete_user_ec2_credentials,
- self.creds.credentials.user_id, creds1['access'])
+ self.non_admin_users_client.delete_user_ec2_credential,
+ self.creds.user_id, creds1['access'])
self.addCleanup(
- self.non_admin_client.delete_user_ec2_credentials,
- self.creds.credentials.user_id, creds2['access'])
+ self.non_admin_users_client.delete_user_ec2_credential,
+ self.creds.user_id, creds2['access'])
# get the list of user ec2 credentials
- resp = self.non_admin_client.list_user_ec2_credentials(
- self.creds.credentials.user_id)["credentials"]
+ resp = self.non_admin_users_client.list_user_ec2_credentials(
+ self.creds.user_id)["credentials"]
fetched_creds = [cred['access'] for cred in resp]
# created credentials should be in a fetched list
missing = [cred for cred in created_creds
@@ -81,33 +80,33 @@
"Failed to find ec2_credentials %s in fetched list" %
', '.join(cred for cred in missing))
- @test.idempotent_id('cb284075-b613-440d-83ca-fe0b33b3c2b8')
- def test_show_ec2_credentials(self):
- """Get the definite user ec2 credentials."""
- resp = self.non_admin_client.create_user_ec2_credentials(
- self.creds.credentials.user_id,
- tenant_id=self.creds.credentials.tenant_id)["credential"]
+ @decorators.idempotent_id('cb284075-b613-440d-83ca-fe0b33b3c2b8')
+ def test_show_ec2_credential(self):
+ """Get the definite user ec2 credential."""
+ resp = self.non_admin_users_client.create_user_ec2_credential(
+ self.creds.user_id,
+ tenant_id=self.creds.tenant_id)["credential"]
self.addCleanup(
- self.non_admin_client.delete_user_ec2_credentials,
- self.creds.credentials.user_id, resp['access'])
+ self.non_admin_users_client.delete_user_ec2_credential,
+ self.creds.user_id, resp['access'])
- ec2_creds = self.non_admin_client.show_user_ec2_credentials(
- self.creds.credentials.user_id, resp['access']
+ ec2_creds = self.non_admin_users_client.show_user_ec2_credential(
+ self.creds.user_id, resp['access']
)["credential"]
for key in ['access', 'secret', 'user_id', 'tenant_id']:
self.assertEqual(ec2_creds[key], resp[key])
- @test.idempotent_id('6aba0d4c-b76b-4e46-aa42-add79bc1551d')
- def test_delete_ec2_credentials(self):
- """Delete user ec2 credentials."""
- resp = self.non_admin_client.create_user_ec2_credentials(
- self.creds.credentials.user_id,
- tenant_id=self.creds.credentials.tenant_id)["credential"]
+ @decorators.idempotent_id('6aba0d4c-b76b-4e46-aa42-add79bc1551d')
+ def test_delete_ec2_credential(self):
+ """Delete user ec2 credential."""
+ resp = self.non_admin_users_client.create_user_ec2_credential(
+ self.creds.user_id,
+ tenant_id=self.creds.tenant_id)["credential"]
access = resp['access']
- self.non_admin_client.delete_user_ec2_credentials(
- self.creds.credentials.user_id, access)
+ self.non_admin_users_client.delete_user_ec2_credential(
+ self.creds.user_id, access)
self.assertRaises(
lib_exc.NotFound,
- self.non_admin_client.show_user_ec2_credentials,
- self.creds.credentials.user_id,
+ self.non_admin_users_client.show_user_ec2_credential,
+ self.creds.user_id,
access)
diff --git a/tempest/api/identity/v2/test_extension.py b/tempest/api/identity/v2/test_extension.py
new file mode 100644
index 0000000..c538c14
--- /dev/null
+++ b/tempest/api/identity/v2/test_extension.py
@@ -0,0 +1,31 @@
+# Copyright 2014 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.identity import base
+from tempest.lib import decorators
+
+
+class ExtensionTestJSON(base.BaseIdentityV2Test):
+
+ @decorators.idempotent_id('85f3f661-f54c-4d48-b563-72ae952b9383')
+ def test_list_extensions(self):
+ # List all the extensions
+ body = self.non_admin_client.list_extensions()['extensions']['values']
+ self.assertNotEmpty(body)
+ keys = ['name', 'updated', 'alias', 'links',
+ 'namespace', 'description']
+ for value in body:
+ for key in keys:
+ self.assertIn(key, value)
diff --git a/tempest/api/identity/v2/test_tenants.py b/tempest/api/identity/v2/test_tenants.py
index 4e31557..b2a6d13 100644
--- a/tempest/api/identity/v2/test_tenants.py
+++ b/tempest/api/identity/v2/test_tenants.py
@@ -13,19 +13,18 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.identity import base
-from tempest import test
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class IdentityTenantsTest(base.BaseIdentityV2Test):
credentials = ['primary', 'alt']
- @test.idempotent_id('ecae2459-243d-4ba1-ad02-65f15dc82b78')
+ @decorators.idempotent_id('ecae2459-243d-4ba1-ad02-65f15dc82b78')
def test_list_tenants_returns_only_authorized_tenants(self):
- alt_tenant_name = self.alt_manager.credentials.credentials.tenant_name
+ alt_tenant_name = self.os_alt.credentials.tenant_name
resp = self.non_admin_tenants_client.list_tenants()
# check that user can see only that tenants that he presents in so user
@@ -33,18 +32,19 @@
# from received tenants list
for tenant in resp['tenants']:
body = self.non_admin_token_client.auth(
- self.os.credentials.username,
- self.os.credentials.password,
+ self.os_primary.credentials.username,
+ self.os_primary.credentials.password,
tenant['name'])
self.assertNotEmpty(body['token']['id'])
self.assertEqual(body['token']['tenant']['id'], tenant['id'])
self.assertEqual(body['token']['tenant']['name'], tenant['name'])
- self.assertEqual(body['user']['id'], self.os.credentials.user_id)
+ self.assertEqual(
+ body['user']['id'], self.os_primary.credentials.user_id)
# check that user cannot log in to alt user's tenant
self.assertRaises(
lib_exc.Unauthorized,
self.non_admin_token_client.auth,
- self.os.credentials.username,
- self.os.credentials.password,
+ self.os_primary.credentials.username,
+ self.os_primary.credentials.password,
alt_tenant_name)
diff --git a/tempest/api/identity/v2/test_tokens.py b/tempest/api/identity/v2/test_tokens.py
index 3b508f4..64b81c2 100644
--- a/tempest/api/identity/v2/test_tokens.py
+++ b/tempest/api/identity/v2/test_tokens.py
@@ -16,18 +16,18 @@
from oslo_utils import timeutils
import six
from tempest.api.identity import base
-from tempest import test
+from tempest.lib import decorators
class TokensTest(base.BaseIdentityV2Test):
- @test.idempotent_id('65ae3b78-91ff-467b-a705-f6678863b8ec')
+ @decorators.idempotent_id('65ae3b78-91ff-467b-a705-f6678863b8ec')
def test_create_token(self):
token_client = self.non_admin_token_client
# get a token for the user
- creds = self.os.credentials
+ creds = self.os_primary.credentials
username = creds.username
password = creds.password
tenant_name = creds.tenant_name
@@ -43,8 +43,8 @@
self.assertGreater(expires_at, now)
self.assertEqual(body['token']['tenant']['id'],
- creds.credentials.tenant_id)
+ creds.tenant_id)
self.assertEqual(body['token']['tenant']['name'],
tenant_name)
- self.assertEqual(body['user']['id'], creds.credentials.user_id)
+ self.assertEqual(body['user']['id'], creds.user_id)
diff --git a/tempest/api/identity/v2/test_users.py b/tempest/api/identity/v2/test_users.py
index 98a2e68..9c77fff 100644
--- a/tempest/api/identity/v2/test_users.py
+++ b/tempest/api/identity/v2/test_users.py
@@ -13,15 +13,16 @@
# License for the specific language governing permissions and limitations
# under the License.
-import copy
import time
-from tempest_lib.common.utils import data_utils
-from tempest_lib import exceptions
-
from tempest.api.identity import base
-from tempest import manager
-from tempest import test
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions
+
+
+CONF = config.CONF
class IdentityUsersTest(base.BaseIdentityV2Test):
@@ -29,50 +30,71 @@
@classmethod
def resource_setup(cls):
super(IdentityUsersTest, cls).resource_setup()
- cls.creds = cls.os.credentials
+ cls.creds = cls.os_primary.credentials
cls.username = cls.creds.username
cls.password = cls.creds.password
cls.tenant_name = cls.creds.tenant_name
- @test.idempotent_id('165859c9-277f-4124-9479-a7d1627b0ca7')
- def test_user_update_own_password(self):
- self.new_creds = copy.copy(self.creds.credentials)
- self.new_creds.password = data_utils.rand_password()
- # we need new non-admin Identity Client with new credentials, since
- # current non_admin_client token will be revoked after updating
- # password
- self.non_admin_client_for_cleanup = copy.copy(self.non_admin_client)
- self.non_admin_client_for_cleanup.auth_provider = (
- manager.get_auth_provider(self.new_creds))
- user_id = self.creds.credentials.user_id
- old_pass = self.creds.credentials.password
- new_pass = self.new_creds.password
+ def _update_password(self, user_id, original_password, password):
+ self.non_admin_users_client.update_user_own_password(
+ user_id, password=password, original_password=original_password)
- # to change password back. important for allow_tenant_isolation = false
- self.addCleanup(
- self.non_admin_client_for_cleanup.update_user_own_password,
- user_id, original_password=new_pass, password=old_pass)
- # user updates own password
- self.non_admin_client.update_user_own_password(
- user_id, password=new_pass, original_password=old_pass)
- # TODO(lbragstad): Sleeping after the response status has been checked
- # and the body loaded as JSON allows requests to fail-fast. The sleep
- # is necessary because keystone will err on the side of security and
- # invalidate tokens within a small margin of error (within the same
- # wall clock second) after a revocation event is issued (such as a
- # password change). Remove this once keystone and Fernet support
- # sub-second precision.
+ # NOTE(morganfainberg): Fernet tokens are not subsecond aware and
+ # Keystone should only be precise to the second. Sleep to ensure
+ # we are passing the second boundary.
time.sleep(1)
# check authorization with new password
self.non_admin_token_client.auth(self.username,
- new_pass,
+ password,
self.tenant_name)
+ # Reset auth to get a new token with the new password
+ self.non_admin_users_client.auth_provider.clear_auth()
+ self.non_admin_users_client.auth_provider.credentials.password = (
+ password)
+
+ def _restore_password(self, user_id, old_pass, new_pass):
+ if CONF.identity_feature_enabled.security_compliance:
+ # First we need to clear the password history
+ unique_count = CONF.identity.user_unique_last_password_count
+ for _ in range(unique_count):
+ random_pass = data_utils.rand_password()
+ self._update_password(
+ user_id, original_password=new_pass, password=random_pass)
+ new_pass = random_pass
+
+ self._update_password(
+ user_id, original_password=new_pass, password=old_pass)
+ # Reset auth again to verify the password restore does work.
+ # Clear auth restores the original credentials and deletes
+ # cached auth data
+ self.non_admin_users_client.auth_provider.clear_auth()
+ # NOTE(lbragstad): Fernet tokens are not subsecond aware and
+ # Keystone should only be precise to the second. Sleep to ensure we
+ # are passing the second boundary before attempting to
+ # authenticate.
+ time.sleep(1)
+ self.non_admin_users_client.auth_provider.set_auth()
+
+ @decorators.idempotent_id('165859c9-277f-4124-9479-a7d1627b0ca7')
+ def test_user_update_own_password(self):
+ old_pass = self.creds.password
+ old_token = self.non_admin_users_client.token
+ new_pass = data_utils.rand_password()
+ user_id = self.creds.user_id
+
+ # to change password back. important for allow_tenant_isolation = false
+ self.addCleanup(self._restore_password, user_id, old_pass, new_pass)
+
+ # user updates own password
+ self._update_password(
+ user_id, original_password=old_pass, password=new_pass)
+
# authorize with old token should lead to Unauthorized
self.assertRaises(exceptions.Unauthorized,
self.non_admin_token_client.auth_token,
- self.non_admin_client.token)
+ old_token)
# authorize with old password should lead to Unauthorized
self.assertRaises(exceptions.Unauthorized,
diff --git a/tempest/api/identity/v3/test_api_discovery.py b/tempest/api/identity/v3/test_api_discovery.py
index 74e9ec1..c04c21b 100644
--- a/tempest/api/identity/v3/test_api_discovery.py
+++ b/tempest/api/identity/v3/test_api_discovery.py
@@ -14,14 +14,28 @@
# under the License.
from tempest.api.identity import base
-from tempest import test
+from tempest.lib import decorators
class TestApiDiscovery(base.BaseIdentityV3Test):
"""Tests for API discovery features."""
- @test.attr(type='smoke')
- @test.idempotent_id('b9232f5e-d9e5-4d97-b96c-28d3db4de1bd')
+ @decorators.idempotent_id('721f480f-35b6-46c7-846e-047e6acea0dc')
+ @decorators.attr(type='smoke')
+ def test_list_api_versions(self):
+ # NOTE: Actually this API doesn't depend on v3 API at all, because
+ # the API operation is "GET /" without v3's endpoint. The reason of
+ # this test path is just v3 API is CURRENT on Keystone side.
+ versions = self.non_admin_versions_client.list_versions()
+ expected_resources = ('id', 'links', 'media-types', 'status',
+ 'updated')
+
+ for version in versions['versions']["values"]:
+ for res in expected_resources:
+ self.assertIn(res, version)
+
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('b9232f5e-d9e5-4d97-b96c-28d3db4de1bd')
def test_api_version_resources(self):
descr = self.non_admin_client.show_api_description()['version']
expected_resources = ('id', 'links', 'media-types', 'status',
@@ -31,8 +45,8 @@
for res in expected_resources:
self.assertIn(res, keys)
- @test.attr(type='smoke')
- @test.idempotent_id('657c1970-4722-4189-8831-7325f3bc4265')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('657c1970-4722-4189-8831-7325f3bc4265')
def test_api_media_types(self):
descr = self.non_admin_client.show_api_description()['version']
# Get MIME type bases and descriptions
@@ -46,8 +60,8 @@
for s_type in supported_types:
self.assertIn(s_type, media_types)
- @test.attr(type='smoke')
- @test.idempotent_id('8879a470-abfb-47bb-bb8d-5a7fd279ad1e')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('8879a470-abfb-47bb-bb8d-5a7fd279ad1e')
def test_api_version_statuses(self):
descr = self.non_admin_client.show_api_description()['version']
status = descr['status'].lower()
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/identity/v3/test_catalog.py b/tempest/api/identity/v3/test_catalog.py
new file mode 100644
index 0000000..deec2dc
--- /dev/null
+++ b/tempest/api/identity/v3/test_catalog.py
@@ -0,0 +1,41 @@
+# 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 IdentityCatalogTest(base.BaseIdentityV3Test):
+
+ @decorators.idempotent_id('56b57ced-22b8-4127-9b8a-565dfb0207e2')
+ def test_catalog_standardization(self):
+ # http://git.openstack.org/cgit/openstack/service-types-authority
+ # /tree/service-types.yaml
+ standard_service_values = [{'name': 'keystone', 'type': 'identity'},
+ {'name': 'nova', 'type': 'compute'},
+ {'name': 'glance', 'type': 'image'},
+ {'name': 'swift', 'type': 'object-store'}]
+ # next, we need to GET the catalog using the catalog client
+ catalog = self.non_admin_catalog_client.show_catalog()['catalog']
+ # get list of the service types present in the catalog
+ catalog_services = []
+ for service in catalog:
+ catalog_services.append(service['type'])
+ for service in standard_service_values:
+ # if service enabled, check if it has a standard typevalue
+ if service['name'] == 'keystone' or\
+ getattr(CONF.service_available, service['name']):
+ self.assertIn(service['type'], catalog_services)
diff --git a/tempest/api/identity/v3/test_projects.py b/tempest/api/identity/v3/test_projects.py
index a547b06..bbb4013 100644
--- a/tempest/api/identity/v3/test_projects.py
+++ b/tempest/api/identity/v3/test_projects.py
@@ -13,41 +13,46 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.identity import base
-from tempest import test
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class IdentityV3ProjectsTest(base.BaseIdentityV3Test):
credentials = ['primary', 'alt']
- @test.idempotent_id('86128d46-e170-4644-866a-cc487f699e1d')
+ @decorators.idempotent_id('86128d46-e170-4644-866a-cc487f699e1d')
def test_list_projects_returns_only_authorized_projects(self):
- alt_project_name =\
- self.alt_manager.credentials.credentials.project_name
- resp = self.non_admin_client.list_user_projects(
- self.os.credentials.user_id)
+ alt_project_name = self.os_alt.credentials.project_name
+ resp = self.non_admin_users_client.list_user_projects(
+ self.os_primary.credentials.user_id)
# check that user can see only that projects that he presents in so
# user can successfully authenticate using his credentials and
# project name from received projects list
for project in resp['projects']:
+ # 'user_domain_id' needs to be specified otherwise tempest.lib
+ # assumes it to be 'default'
token_id, body = self.non_admin_token.get_token(
- username=self.os.credentials.username,
- password=self.os.credentials.password,
+ username=self.os_primary.credentials.username,
+ user_domain_id=self.os_primary.credentials.user_domain_id,
+ password=self.os_primary.credentials.password,
project_name=project['name'],
+ project_domain_id=project['domain_id'],
auth_data=True)
self.assertNotEmpty(token_id)
self.assertEqual(body['project']['id'], project['id'])
self.assertEqual(body['project']['name'], project['name'])
- self.assertEqual(body['user']['id'], self.os.credentials.user_id)
+ self.assertEqual(
+ body['user']['id'], self.os_primary.credentials.user_id)
# check that user cannot log in to alt user's project
self.assertRaises(
lib_exc.Unauthorized,
self.non_admin_token.get_token,
- username=self.os.credentials.username,
- password=self.os.credentials.password,
- project_name=alt_project_name)
+ username=self.os_primary.credentials.username,
+ user_domain_id=self.os_primary.credentials.user_domain_id,
+ password=self.os_primary.credentials.password,
+ project_name=alt_project_name,
+ project_domain_id=project['domain_id'])
diff --git a/tempest/api/identity/v3/test_tokens.py b/tempest/api/identity/v3/test_tokens.py
index 3151763..4c72d82 100644
--- a/tempest/api/identity/v3/test_tokens.py
+++ b/tempest/api/identity/v3/test_tokens.py
@@ -15,23 +15,56 @@
from oslo_utils import timeutils
import six
+
from tempest.api.identity import base
-from tempest import test
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class TokensV3Test(base.BaseIdentityV3Test):
- @test.idempotent_id('6f8e4436-fc96-4282-8122-e41df57197a9')
- def test_create_token(self):
-
- creds = self.os.credentials
+ @decorators.idempotent_id('a9512ac3-3909-48a4-b395-11f438e16260')
+ def test_validate_token(self):
+ creds = self.os_primary.credentials
user_id = creds.user_id
username = creds.username
password = creds.password
+ user_domain_id = creds.user_domain_id
+ # GET and validate token
+ subject_token, token_body = self.non_admin_token.get_token(
+ user_id=user_id,
+ username=username,
+ user_domain_id=user_domain_id,
+ password=password,
+ auth_data=True)
+ authenticated_token = self.non_admin_client.show_token(
+ subject_token)['token']
+ # sanity checking to make sure they are indeed the same token
+ self.assertEqual(authenticated_token, token_body)
+ # test to see if token has been properly authenticated
+ self.assertEqual(authenticated_token['user']['id'], user_id)
+ self.assertEqual(authenticated_token['user']['name'], username)
+ self.non_admin_client.delete_token(subject_token)
+ self.assertRaises(
+ lib_exc.NotFound, self.non_admin_client.show_token, subject_token)
- token_id, resp = self.non_admin_token.get_token(user_id=user_id,
- password=password,
- auth_data=True)
+ @decorators.idempotent_id('6f8e4436-fc96-4282-8122-e41df57197a9')
+ def test_create_token(self):
+
+ creds = self.os_primary.credentials
+ user_id = creds.user_id
+ username = creds.username
+ password = creds.password
+ user_domain_id = creds.user_domain_id
+
+ # 'user_domain_id' needs to be specified otherwise tempest.lib assumes
+ # it to be 'default'
+ token_id, resp = self.non_admin_token.get_token(
+ user_id=user_id,
+ username=username,
+ user_domain_id=user_domain_id,
+ password=password,
+ auth_data=True)
self.assertNotEmpty(token_id)
self.assertIsInstance(token_id, six.string_types)
@@ -44,9 +77,17 @@
self.assertGreater(expires_at, now)
subject_id = resp['user']['id']
- self.assertEqual(subject_id, user_id)
+ if user_id:
+ self.assertEqual(subject_id, user_id)
+ else:
+ # Expect a user ID, but don't know what it will be.
+ self.assertIsNotNone(subject_id, 'Expected user ID in token.')
subject_name = resp['user']['name']
- self.assertEqual(subject_name, username)
+ if username:
+ self.assertEqual(subject_name, username)
+ else:
+ # Expect a user name, but don't know what it will be.
+ self.assertIsNotNone(subject_name, 'Expected user name in token.')
self.assertEqual(resp['methods'][0], 'password')
diff --git a/tempest/api/identity/v3/test_users.py b/tempest/api/identity/v3/test_users.py
index 93814d3..1f099df 100644
--- a/tempest/api/identity/v3/test_users.py
+++ b/tempest/api/identity/v3/test_users.py
@@ -13,15 +13,18 @@
# License for the specific language governing permissions and limitations
# under the License.
-import copy
import time
-from tempest_lib.common.utils import data_utils
-from tempest_lib import exceptions
+import testtools
from tempest.api.identity import base
-from tempest import manager
-from tempest import test
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions
+
+
+CONF = config.CONF
class IdentityV3UsersTest(base.BaseIdentityV3Test):
@@ -29,54 +32,127 @@
@classmethod
def resource_setup(cls):
super(IdentityV3UsersTest, cls).resource_setup()
- cls.creds = cls.os.credentials
+ cls.creds = cls.os_primary.credentials
cls.user_id = cls.creds.user_id
- cls.username = cls.creds.username
- cls.password = cls.creds.password
- @test.idempotent_id('ad71bd23-12ad-426b-bb8b-195d2b635f27')
- def test_user_update_own_password(self):
- self.new_creds = copy.copy(self.creds.credentials)
- self.new_creds.password = data_utils.rand_password()
- # we need new non-admin Identity V3 Client with new credentials, since
- # current non_admin_client token will be revoked after updating
- # password
- self.non_admin_client_for_cleanup = copy.copy(self.non_admin_client)
- self.non_admin_client_for_cleanup.auth_provider = (
- manager.get_auth_provider(self.new_creds))
- user_id = self.creds.credentials.user_id
- old_pass = self.creds.credentials.password
- new_pass = self.new_creds.password
- # to change password back. important for allow_tenant_isolation = false
- self.addCleanup(
- self.non_admin_client_for_cleanup.update_user_password,
- user_id,
- password=old_pass,
- original_password=new_pass)
+ def _update_password(self, original_password, password):
+ self.non_admin_users_client.update_user_password(
+ self.user_id,
+ password=password,
+ original_password=original_password)
- # user updates own password
- self.non_admin_client.update_user_password(
- user_id, password=new_pass, original_password=old_pass)
-
- # TODO(lbragstad): Sleeping after the response status has been checked
- # and the body loaded as JSON allows requests to fail-fast. The sleep
- # is necessary because keystone will err on the side of security and
- # invalidate tokens within a small margin of error (within the same
- # wall clock second) after a revocation event is issued (such as a
- # password change). Remove this once keystone and Fernet support
- # sub-second precision.
+ # NOTE(morganfainberg): Fernet tokens are not subsecond aware and
+ # Keystone should only be precise to the second. Sleep to ensure
+ # we are passing the second boundary.
time.sleep(1)
# check authorization with new password
- self.non_admin_token.auth(user_id=self.user_id, password=new_pass)
+ self.non_admin_token.auth(user_id=self.user_id, password=password)
+
+ # Reset auth to get a new token with the new password
+ self.non_admin_users_client.auth_provider.clear_auth()
+ self.non_admin_users_client.auth_provider.credentials.password = (
+ password)
+
+ def _restore_password(self, old_pass, new_pass):
+ if CONF.identity_feature_enabled.security_compliance:
+ # First we need to clear the password history
+ unique_count = CONF.identity.user_unique_last_password_count
+ for _ in range(unique_count):
+ random_pass = data_utils.rand_password()
+ self._update_password(
+ original_password=new_pass, password=random_pass)
+ new_pass = random_pass
+
+ self._update_password(original_password=new_pass, password=old_pass)
+ # Reset auth again to verify the password restore does work.
+ # Clear auth restores the original credentials and deletes
+ # cached auth data
+ self.non_admin_users_client.auth_provider.clear_auth()
+ # NOTE(lbragstad): Fernet tokens are not subsecond aware and
+ # Keystone should only be precise to the second. Sleep to ensure we
+ # are passing the second boundary before attempting to
+ # authenticate.
+ time.sleep(1)
+ self.non_admin_users_client.auth_provider.set_auth()
+
+ @decorators.idempotent_id('ad71bd23-12ad-426b-bb8b-195d2b635f27')
+ def test_user_update_own_password(self):
+ old_pass = self.creds.password
+ old_token = self.non_admin_client.token
+ new_pass = data_utils.rand_password()
+
+ # to change password back. important for allow_tenant_isolation = false
+ self.addCleanup(self._restore_password, old_pass, new_pass)
+
+ # user updates own password
+ self._update_password(original_password=old_pass, password=new_pass)
# authorize with old token should lead to IdentityError (404 code)
self.assertRaises(exceptions.IdentityError,
self.non_admin_token.auth,
- token=self.non_admin_client.token)
+ token=old_token)
# authorize with old password should lead to Unauthorized
self.assertRaises(exceptions.Unauthorized,
self.non_admin_token.auth,
user_id=self.user_id,
password=old_pass)
+
+ @testtools.skipUnless(CONF.identity_feature_enabled.security_compliance,
+ 'Security compliance not available.')
+ @decorators.idempotent_id('941784ee-5342-4571-959b-b80dd2cea516')
+ def test_password_history_check_self_service_api(self):
+ old_pass = self.creds.password
+ new_pass1 = data_utils.rand_password()
+ new_pass2 = data_utils.rand_password()
+
+ self.addCleanup(self._restore_password, old_pass, new_pass2)
+
+ # Update password
+ self._update_password(original_password=old_pass, password=new_pass1)
+
+ if CONF.identity.user_unique_last_password_count > 1:
+ # Can not reuse a previously set password
+ self.assertRaises(exceptions.BadRequest,
+ self.non_admin_users_client.update_user_password,
+ self.user_id,
+ password=new_pass1,
+ original_password=new_pass1)
+
+ self.assertRaises(exceptions.BadRequest,
+ self.non_admin_users_client.update_user_password,
+ self.user_id,
+ password=old_pass,
+ original_password=new_pass1)
+
+ # A different password can be set
+ self._update_password(original_password=new_pass1, password=new_pass2)
+
+ @testtools.skipUnless(CONF.identity_feature_enabled.security_compliance,
+ 'Security compliance not available.')
+ @decorators.idempotent_id('a7ad8bbf-2cff-4520-8c1d-96332e151658')
+ def test_user_account_lockout(self):
+ password = self.creds.password
+
+ # First, we login using the correct credentials
+ self.non_admin_token.auth(user_id=self.user_id, password=password)
+
+ # Lock user account by using the wrong password to login
+ bad_password = data_utils.rand_password()
+ for _ in range(CONF.identity.user_lockout_failure_attempts):
+ self.assertRaises(exceptions.Unauthorized,
+ self.non_admin_token.auth,
+ user_id=self.user_id,
+ password=bad_password)
+
+ # The user account must be locked, so now it is not possible to login
+ # even using the correct password
+ self.assertRaises(exceptions.Unauthorized,
+ self.non_admin_token.auth,
+ user_id=self.user_id,
+ password=password)
+
+ # If we wait the required time, the user account will be unlocked
+ time.sleep(CONF.identity.user_lockout_duration + 1)
+ self.non_admin_token.auth(user_id=self.user_id, password=password)
diff --git a/tempest/api/image/admin/v2/test_images.py b/tempest/api/image/admin/v2/test_images.py
deleted file mode 100644
index b171da3..0000000
--- a/tempest/api/image/admin/v2/test_images.py
+++ /dev/null
@@ -1,60 +0,0 @@
-# Copyright 2015 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 six import moves
-from tempest_lib.common.utils import data_utils
-import testtools
-
-from tempest.api.image import base
-from tempest import config
-from tempest import test
-from tempest_lib import exceptions as lib_exc
-
-CONF = config.CONF
-
-
-class BasicAdminOperationsImagesTest(base.BaseV2ImageAdminTest):
- """Here we test admin operations of images"""
-
- @testtools.skipUnless(CONF.image_feature_enabled.deactivate_image,
- 'deactivate-image is not available.')
- @test.idempotent_id('951ebe01-969f-4ea9-9898-8a3f1f442ab0')
- def test_admin_deactivate_reactivate_image(self):
- # Create image by non-admin tenant
- image_name = data_utils.rand_name('image')
- body = self.client.create_image(name=image_name,
- container_format='bare',
- disk_format='raw',
- visibility='private')
- image_id = body['id']
- self.addCleanup(self.client.delete_image, image_id)
- # upload an image file
- content = data_utils.random_bytes()
- image_file = moves.cStringIO(content)
- self.client.store_image_file(image_id, image_file)
- # deactivate image
- self.admin_client.deactivate_image(image_id)
- body = self.client.show_image(image_id)
- self.assertEqual("deactivated", body['status'])
- # non-admin user unable to download deactivated image
- self.assertRaises(lib_exc.Forbidden, self.client.show_image_file,
- image_id)
- # reactivate image
- self.admin_client.reactivate_image(image_id)
- body = self.client.show_image(image_id)
- self.assertEqual("active", body['status'])
- # non-admin user able to download image after reactivation by admin
- body = self.client.show_image_file(image_id)
- self.assertEqual(content, body.data)
diff --git a/tempest/api/image/base.py b/tempest/api/image/base.py
index c3205ce..ae7b3e4 100644
--- a/tempest/api/image/base.py
+++ b/tempest/api/image/base.py
@@ -12,18 +12,16 @@
# License for the specific language governing permissions and limitations
# under the License.
-from oslo_log import log as logging
-from six import moves
-from tempest_lib import exceptions as lib_exc
+import six
-from tempest.common.utils import data_utils
+from tempest.common import image as common_image
from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
import tempest.test
CONF = config.CONF
-LOG = logging.getLogger(__name__)
-
class BaseImageTest(tempest.test.BaseTestCase):
"""Base test class for Image API tests."""
@@ -48,39 +46,35 @@
cls.created_images = []
@classmethod
- def resource_cleanup(cls):
- for image_id in cls.created_images:
- try:
- cls.client.delete_image(image_id)
- except lib_exc.NotFound:
- pass
-
- for image_id in cls.created_images:
- cls.client.wait_for_resource_deletion(image_id)
- super(BaseImageTest, cls).resource_cleanup()
-
- @classmethod
- def create_image(cls, **kwargs):
+ def create_image(cls, data=None, **kwargs):
"""Wrapper that returns a test image."""
- name = data_utils.rand_name(cls.__name__ + "-instance")
- if 'name' in kwargs:
- name = kwargs.pop('name')
+ if 'name' not in kwargs:
+ name = data_utils.rand_name(cls.__name__ + "-image")
+ kwargs['name'] = name
- container_format = kwargs.pop('container_format')
- disk_format = kwargs.pop('disk_format')
+ params = cls._get_create_params(**kwargs)
+ if data:
+ # NOTE: On glance v1 API, the data should be passed on
+ # a header. Then here handles the data separately.
+ params['data'] = data
- image = cls.client.create_image(name=name,
- container_format=container_format,
- disk_format=disk_format,
- **kwargs)
+ image = cls.client.create_image(**params)
# Image objects returned by the v1 client have the image
# data inside a dict that is keyed against 'image'.
if 'image' in image:
image = image['image']
cls.created_images.append(image['id'])
+ cls.addClassResourceCleanup(cls.client.wait_for_resource_deletion,
+ image['id'])
+ cls.addClassResourceCleanup(test_utils.call_and_ignore_notfound_exc,
+ cls.client.delete_image, image['id'])
return image
+ @classmethod
+ def _get_create_params(cls, **kwargs):
+ return kwargs
+
class BaseV1ImageTest(BaseImageTest):
@@ -94,7 +88,11 @@
@classmethod
def setup_clients(cls):
super(BaseV1ImageTest, cls).setup_clients()
- cls.client = cls.os.image_client
+ cls.client = cls.os_primary.image_client
+
+ @classmethod
+ def _get_create_params(cls, **kwargs):
+ return {'headers': common_image.image_meta_to_headers(**kwargs)}
class BaseV1ImageMembersTest(BaseV1ImageTest):
@@ -104,21 +102,22 @@
@classmethod
def setup_clients(cls):
super(BaseV1ImageMembersTest, cls).setup_clients()
+ cls.image_member_client = cls.os_primary.image_member_client
+ cls.alt_image_member_client = cls.os_alt.image_member_client
cls.alt_img_cli = cls.os_alt.image_client
@classmethod
def resource_setup(cls):
super(BaseV1ImageMembersTest, cls).resource_setup()
- cls.alt_tenant_id = cls.alt_img_cli.tenant_id
+ cls.alt_tenant_id = cls.alt_image_member_client.tenant_id
def _create_image(self):
- image_file = moves.cStringIO(data_utils.random_bytes())
+ image_file = six.BytesIO(data_utils.random_bytes())
image = self.create_image(container_format='bare',
disk_format='raw',
is_public=False,
data=image_file)
- image_id = image['id']
- return image_id
+ return image['id']
class BaseV2ImageTest(BaseImageTest):
@@ -133,7 +132,28 @@
@classmethod
def setup_clients(cls):
super(BaseV2ImageTest, cls).setup_clients()
- cls.client = cls.os.image_client_v2
+ cls.client = cls.os_primary.image_client_v2
+ cls.namespaces_client = cls.os_primary.namespaces_client
+ cls.resource_types_client = cls.os_primary.resource_types_client
+ cls.namespace_properties_client =\
+ cls.os_primary.namespace_properties_client
+ cls.namespace_objects_client = cls.os_primary.namespace_objects_client
+ cls.namespace_tags_client = cls.os_primary.namespace_tags_client
+ cls.schemas_client = cls.os_primary.schemas_client
+ cls.versions_client = cls.os_primary.image_versions_client
+
+ def create_namespace(self, namespace_name=None, visibility='public',
+ description='Tempest', protected=False,
+ **kwargs):
+ if not namespace_name:
+ namespace_name = data_utils.rand_name('test-ns')
+ kwargs.setdefault('display_name', namespace_name)
+ namespace = self.namespaces_client.create_namespace(
+ namespace=namespace_name, visibility=visibility,
+ description=description, protected=protected, **kwargs)
+ self.addCleanup(self.namespaces_client.delete_namespace,
+ namespace_name)
+ return namespace
class BaseV2MemberImageTest(BaseV2ImageTest):
@@ -143,13 +163,14 @@
@classmethod
def setup_clients(cls):
super(BaseV2MemberImageTest, cls).setup_clients()
- cls.os_img_client = cls.os.image_client_v2
+ cls.image_member_client = cls.os_primary.image_member_client_v2
+ cls.alt_image_member_client = cls.os_alt.image_member_client_v2
cls.alt_img_client = cls.os_alt.image_client_v2
@classmethod
def resource_setup(cls):
super(BaseV2MemberImageTest, cls).resource_setup()
- cls.alt_tenant_id = cls.alt_img_client.tenant_id
+ cls.alt_tenant_id = cls.alt_image_member_client.tenant_id
def _list_image_ids_as_alt(self):
image_list = self.alt_img_client.list_images()['images']
@@ -157,30 +178,19 @@
return image_ids
def _create_image(self):
- name = data_utils.rand_name('image')
- image = self.os_img_client.create_image(name=name,
- container_format='bare',
- disk_format='raw')
- image_id = image['id']
- self.addCleanup(self.os_img_client.delete_image, image_id)
- return image_id
+ name = data_utils.rand_name(self.__class__.__name__ + '-image')
+ image = self.client.create_image(name=name,
+ container_format='bare',
+ disk_format='raw')
+ self.addCleanup(self.client.delete_image, image['id'])
+ return image['id']
-class BaseV1ImageAdminTest(BaseImageTest):
- credentials = ['admin', 'primary']
+class BaseV2ImageAdminTest(BaseV2ImageTest):
- @classmethod
- def setup_clients(cls):
- super(BaseV1ImageAdminTest, cls).setup_clients()
- cls.client = cls.os.image_client
- cls.admin_client = cls.os_adm.image_client
-
-
-class BaseV2ImageAdminTest(BaseImageTest):
credentials = ['admin', 'primary']
@classmethod
def setup_clients(cls):
super(BaseV2ImageAdminTest, cls).setup_clients()
- cls.client = cls.os.image_client_v2
- cls.admin_client = cls.os_adm.image_client_v2
+ cls.admin_client = cls.os_admin.image_client_v2
diff --git a/tempest/api/image/v1/test_image_members.py b/tempest/api/image/v1/test_image_members.py
index eb6969b..bf2e510 100644
--- a/tempest/api/image/v1/test_image_members.py
+++ b/tempest/api/image/v1/test_image_members.py
@@ -14,39 +14,46 @@
from tempest.api.image import base
-from tempest import test
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class ImageMembersTest(base.BaseV1ImageMembersTest):
- @test.idempotent_id('1d6ef640-3a20-4c84-8710-d95828fdb6ad')
+ @decorators.idempotent_id('1d6ef640-3a20-4c84-8710-d95828fdb6ad')
def test_add_image_member(self):
image = self._create_image()
- self.client.add_member(self.alt_tenant_id, image)
- body = self.client.list_image_members(image)
+ self.image_member_client.create_image_member(image, self.alt_tenant_id)
+ body = self.image_member_client.list_image_members(image)
members = body['members']
- members = map(lambda x: x['member_id'], members)
+ members = [member['member_id'] for member in members]
self.assertIn(self.alt_tenant_id, members)
# get image as alt user
self.alt_img_cli.show_image(image)
- @test.idempotent_id('6a5328a5-80e8-4b82-bd32-6c061f128da9')
+ @decorators.idempotent_id('6a5328a5-80e8-4b82-bd32-6c061f128da9')
def test_get_shared_images(self):
image = self._create_image()
- self.client.add_member(self.alt_tenant_id, image)
+ self.image_member_client.create_image_member(image, self.alt_tenant_id)
share_image = self._create_image()
- self.client.add_member(self.alt_tenant_id, share_image)
- body = self.client.list_shared_images(self.alt_tenant_id)
+ self.image_member_client.create_image_member(share_image,
+ self.alt_tenant_id)
+ body = self.image_member_client.list_shared_images(
+ self.alt_tenant_id)
images = body['shared_images']
- images = map(lambda x: x['image_id'], images)
+ images = [img['image_id'] for img in images]
self.assertIn(share_image, images)
self.assertIn(image, images)
- @test.idempotent_id('a76a3191-8948-4b44-a9d6-4053e5f2b138')
+ @decorators.idempotent_id('a76a3191-8948-4b44-a9d6-4053e5f2b138')
def test_remove_member(self):
image_id = self._create_image()
- self.client.add_member(self.alt_tenant_id, image_id)
- self.client.delete_member(self.alt_tenant_id, image_id)
- body = self.client.list_image_members(image_id)
+ self.image_member_client.create_image_member(image_id,
+ self.alt_tenant_id)
+ self.image_member_client.delete_image_member(image_id,
+ self.alt_tenant_id)
+ body = self.image_member_client.list_image_members(image_id)
members = body['members']
- self.assertEqual(0, len(members), str(members))
+ self.assertEmpty(members)
+ self.assertRaises(
+ lib_exc.NotFound, self.alt_img_cli.show_image, image_id)
diff --git a/tempest/api/image/v1/test_image_members_negative.py b/tempest/api/image/v1/test_image_members_negative.py
index 50f5048..2748bd5 100644
--- a/tempest/api/image/v1/test_image_members_negative.py
+++ b/tempest/api/image/v1/test_image_members_negative.py
@@ -12,42 +12,44 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.image import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class ImageMembersNegativeTest(base.BaseV1ImageMembersTest):
- @test.attr(type=['negative'])
- @test.idempotent_id('147a9536-18e3-45da-91ea-b037a028f364')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('147a9536-18e3-45da-91ea-b037a028f364')
def test_add_member_with_non_existing_image(self):
# Add member with non existing image.
non_exist_image = data_utils.rand_uuid()
- self.assertRaises(lib_exc.NotFound, self.client.add_member,
- self.alt_tenant_id, non_exist_image)
+ self.assertRaises(lib_exc.NotFound,
+ self.image_member_client.create_image_member,
+ non_exist_image, self.alt_tenant_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('e1559f05-b667-4f1b-a7af-518b52dc0c0f')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('e1559f05-b667-4f1b-a7af-518b52dc0c0f')
def test_delete_member_with_non_existing_image(self):
# Delete member with non existing image.
non_exist_image = data_utils.rand_uuid()
- self.assertRaises(lib_exc.NotFound, self.client.delete_member,
- self.alt_tenant_id, non_exist_image)
+ self.assertRaises(lib_exc.NotFound,
+ self.image_member_client.delete_image_member,
+ non_exist_image, self.alt_tenant_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('f5720333-dd69-4194-bb76-d2f048addd56')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('f5720333-dd69-4194-bb76-d2f048addd56')
def test_delete_member_with_non_existing_tenant(self):
# Delete member with non existing tenant.
image_id = self._create_image()
non_exist_tenant = data_utils.rand_uuid_hex()
- self.assertRaises(lib_exc.NotFound, self.client.delete_member,
- non_exist_tenant, image_id)
+ self.assertRaises(lib_exc.NotFound,
+ self.image_member_client.delete_image_member,
+ image_id, non_exist_tenant)
- @test.attr(type=['negative'])
- @test.idempotent_id('f25f89e4-0b6c-453b-a853-1f80b9d7ef26')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('f25f89e4-0b6c-453b-a853-1f80b9d7ef26')
def test_get_image_without_membership(self):
# Image is hidden from another tenants.
image_id = self._create_image()
diff --git a/tempest/api/image/v1/test_images.py b/tempest/api/image/v1/test_images.py
index 64f3174..2432c8b 100644
--- a/tempest/api/image/v1/test_images.py
+++ b/tempest/api/image/v1/test_images.py
@@ -13,52 +13,80 @@
# License for the specific language governing permissions and limitations
# under the License.
-from six import moves
+import six
from tempest.api.image import base
-from tempest.common.utils import data_utils
+from tempest.common import image as common_image
+from tempest.common import waiters
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions
CONF = config.CONF
+def get_container_and_disk_format():
+ a_formats = ['ami', 'ari', 'aki']
+
+ container_format = CONF.image.container_formats[0]
+
+ # In v1, If container_format is one of ['ami', 'ari', 'aki'], then
+ # disk_format must be same with container_format.
+ # If they are of different item sequence in tempest.conf, such as:
+ # container_formats = ami,ari,aki,bare
+ # disk_formats = ari,ami,aki,vhd
+ # we can select one in disk_format list that is same with container_format.
+ if container_format in a_formats:
+ if container_format in CONF.image.disk_formats:
+ disk_format = container_format
+ else:
+ msg = ("The container format and the disk format don't match. "
+ "Container format: %(container)s, Disk format: %(disk)s." %
+ {'container': container_format, 'disk':
+ CONF.image.disk_formats})
+ raise exceptions.InvalidConfiguration(msg)
+ else:
+ disk_format = CONF.image.disk_formats[0]
+
+ return container_format, disk_format
+
+
class CreateRegisterImagesTest(base.BaseV1ImageTest):
"""Here we test the registration and creation of images."""
- @test.idempotent_id('3027f8e6-3492-4a11-8575-c3293017af4d')
+ @decorators.idempotent_id('3027f8e6-3492-4a11-8575-c3293017af4d')
def test_register_then_upload(self):
# Register, then upload an image
properties = {'prop1': 'val1'}
- body = self.create_image(name='New Name',
- container_format='bare',
- disk_format='raw',
- is_public=False,
- properties=properties)
- self.assertIn('id', body)
- image_id = body.get('id')
- self.assertEqual('New Name', body.get('name'))
- self.assertFalse(body.get('is_public'))
- self.assertEqual('queued', body.get('status'))
+ container_format, disk_format = get_container_and_disk_format()
+ image = self.create_image(name='New Name',
+ container_format=container_format,
+ disk_format=disk_format,
+ is_public=False,
+ properties=properties)
+ self.assertEqual('New Name', image.get('name'))
+ self.assertFalse(image.get('is_public'))
+ self.assertEqual('queued', image.get('status'))
for key, val in properties.items():
- self.assertEqual(val, body.get('properties')[key])
+ self.assertEqual(val, image.get('properties')[key])
# Now try uploading an image file
- image_file = moves.cStringIO(data_utils.random_bytes())
- body = self.client.update_image(image_id, data=image_file)['image']
+ image_file = six.BytesIO(data_utils.random_bytes())
+ body = self.client.update_image(image['id'], data=image_file)['image']
self.assertIn('size', body)
self.assertEqual(1024, body.get('size'))
- @test.idempotent_id('69da74d9-68a9-404b-9664-ff7164ccb0f5')
+ @decorators.idempotent_id('69da74d9-68a9-404b-9664-ff7164ccb0f5')
def test_register_remote_image(self):
# Register a new remote image
+ container_format, disk_format = get_container_and_disk_format()
body = self.create_image(name='New Remote Image',
- container_format='bare',
- disk_format='raw', is_public=False,
+ container_format=container_format,
+ disk_format=disk_format, is_public=False,
location=CONF.image.http_image,
properties={'key1': 'value1',
'key2': 'value2'})
- self.assertIn('id', body)
self.assertEqual('New Remote Image', body.get('name'))
self.assertFalse(body.get('is_public'))
self.assertEqual('active', body.get('status'))
@@ -66,30 +94,29 @@
self.assertEqual(properties['key1'], 'value1')
self.assertEqual(properties['key2'], 'value2')
- @test.idempotent_id('6d0e13a7-515b-460c-b91f-9f4793f09816')
+ @decorators.idempotent_id('6d0e13a7-515b-460c-b91f-9f4793f09816')
def test_register_http_image(self):
- body = self.create_image(name='New Http Image',
- container_format='bare',
- disk_format='raw', is_public=False,
- copy_from=CONF.image.http_image)
- self.assertIn('id', body)
- image_id = body.get('id')
- self.assertEqual('New Http Image', body.get('name'))
- self.assertFalse(body.get('is_public'))
- self.client.wait_for_image_status(image_id, 'active')
- self.client.show_image(image_id)
+ container_format, disk_format = get_container_and_disk_format()
+ image = self.create_image(name='New Http Image',
+ container_format=container_format,
+ disk_format=disk_format, is_public=False,
+ copy_from=CONF.image.http_image)
+ self.assertEqual('New Http Image', image.get('name'))
+ self.assertFalse(image.get('is_public'))
+ waiters.wait_for_image_status(self.client, image['id'], 'active')
+ self.client.show_image(image['id'])
- @test.idempotent_id('05b19d55-140c-40d0-b36b-fafd774d421b')
+ @decorators.idempotent_id('05b19d55-140c-40d0-b36b-fafd774d421b')
def test_register_image_with_min_ram(self):
# Register an image with min ram
+ container_format, disk_format = get_container_and_disk_format()
properties = {'prop1': 'val1'}
body = self.create_image(name='New_image_with_min_ram',
- container_format='bare',
- disk_format='raw',
+ container_format=container_format,
+ disk_format=disk_format,
is_public=False,
min_ram=40,
properties=properties)
- self.assertIn('id', body)
self.assertEqual('New_image_with_min_ram', body.get('name'))
self.assertFalse(body.get('is_public'))
self.assertEqual('queued', body.get('status'))
@@ -103,22 +130,51 @@
"""Here we test the listing of image information"""
@classmethod
+ def skip_checks(cls):
+ super(ListImagesTest, cls).skip_checks()
+ 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)
+
+ @classmethod
def resource_setup(cls):
super(ListImagesTest, cls).resource_setup()
# We add a few images here to test the listing functionality of
# the images API
- img1 = cls._create_remote_image('one', 'bare', 'raw')
- img2 = cls._create_remote_image('two', 'ami', 'ami')
- img3 = cls._create_remote_image('dup', 'bare', 'raw')
- img4 = cls._create_remote_image('dup', 'bare', 'raw')
- img5 = cls._create_standard_image('1', 'ami', 'ami', 42)
- img6 = cls._create_standard_image('2', 'ami', 'ami', 142)
- img7 = cls._create_standard_image('33', 'bare', 'raw', 142)
- img8 = cls._create_standard_image('33', 'bare', 'raw', 142)
+ a_formats = ['ami', 'ari', 'aki']
+
+ (cls.container_format,
+ container_format_alt) = CONF.image.container_formats[:2]
+ cls.disk_format, cls.disk_format_alt = CONF.image.disk_formats[:2]
+ if cls.container_format in a_formats:
+ cls.disk_format = cls.container_format
+ if container_format_alt in a_formats:
+ cls.disk_format_alt = container_format_alt
+
+ img1 = cls._create_remote_image('one', cls.container_format,
+ cls.disk_format)
+ img2 = cls._create_remote_image('two', container_format_alt,
+ cls.disk_format_alt)
+ img3 = cls._create_remote_image('dup', cls.container_format,
+ cls.disk_format)
+ img4 = cls._create_remote_image('dup', cls.container_format,
+ cls.disk_format)
+ img5 = cls._create_standard_image('1', container_format_alt,
+ cls.disk_format_alt, 42)
+ img6 = cls._create_standard_image('2', container_format_alt,
+ cls.disk_format_alt, 142)
+ img7 = cls._create_standard_image('33', cls.container_format,
+ cls.disk_format, 142)
+ img8 = cls._create_standard_image('33', cls.container_format,
+ cls.disk_format, 142)
cls.created_set = set(cls.created_images)
- # 5x bare, 3x ami
- cls.bare_set = set((img1, img3, img4, img7, img8))
- cls.ami_set = set((img2, img5, img6))
+ # same container format
+ cls.same_container_format_set = set((img1, img3, img4, img7, img8))
+ # same disk format
+ cls.same_disk_format_set = set((img2, img5, img6))
+
# 1x with size 42
cls.size42_set = set((img5,))
# 3x with size 142
@@ -137,8 +193,7 @@
disk_format=disk_format,
is_public=False,
location=location)
- image_id = image['id']
- return image_id
+ return image['id']
@classmethod
def _create_standard_image(cls, name, container_format,
@@ -148,61 +203,63 @@
Note that the size of the new image is a random number between
1024 and 4096
"""
- image_file = moves.cStringIO(data_utils.random_bytes(size))
+ image_file = six.BytesIO(data_utils.random_bytes(size))
name = 'New Standard Image %s' % name
image = cls.create_image(name=name,
container_format=container_format,
disk_format=disk_format,
is_public=False, data=image_file)
- image_id = image['id']
- return image_id
+ return image['id']
- @test.idempotent_id('246178ab-3b33-4212-9a4b-a7fe8261794d')
+ @decorators.idempotent_id('246178ab-3b33-4212-9a4b-a7fe8261794d')
def test_index_no_params(self):
# Simple test to see all fixture images returned
images_list = self.client.list_images()['images']
- image_list = map(lambda x: x['id'], images_list)
+ image_list = [image['id'] for image in images_list]
for image_id in self.created_images:
self.assertIn(image_id, image_list)
- @test.idempotent_id('f1755589-63d6-4468-b098-589820eb4031')
+ @decorators.idempotent_id('f1755589-63d6-4468-b098-589820eb4031')
def test_index_disk_format(self):
- images_list = self.client.list_images(disk_format='ami')['images']
+ images_list = self.client.list_images(
+ disk_format=self.disk_format_alt)['images']
for image in images_list:
- self.assertEqual(image['disk_format'], 'ami')
+ self.assertEqual(image['disk_format'], self.disk_format_alt)
result_set = set(map(lambda x: x['id'], images_list))
- self.assertTrue(self.ami_set <= result_set)
- self.assertFalse(self.created_set - self.ami_set <= result_set)
+ self.assertTrue(self.same_disk_format_set <= result_set)
+ self.assertFalse(self.created_set - self.same_disk_format_set <=
+ result_set)
- @test.idempotent_id('2143655d-96d9-4bec-9188-8674206b4b3b')
+ @decorators.idempotent_id('2143655d-96d9-4bec-9188-8674206b4b3b')
def test_index_container_format(self):
- images_list = (self.client.list_images(container_format='bare')
- ['images'])
+ images_list = self.client.list_images(
+ container_format=self.container_format)['images']
for image in images_list:
- self.assertEqual(image['container_format'], 'bare')
+ self.assertEqual(image['container_format'], self.container_format)
result_set = set(map(lambda x: x['id'], images_list))
- self.assertTrue(self.bare_set <= result_set)
- self.assertFalse(self.created_set - self.bare_set <= result_set)
+ self.assertTrue(self.same_container_format_set <= result_set)
+ self.assertFalse(self.created_set - self.same_container_format_set <=
+ result_set)
- @test.idempotent_id('feb32ac6-22bb-4a16-afd8-9454bb714b14')
+ @decorators.idempotent_id('feb32ac6-22bb-4a16-afd8-9454bb714b14')
def test_index_max_size(self):
images_list = self.client.list_images(size_max=42)['images']
for image in images_list:
- self.assertTrue(image['size'] <= 42)
+ self.assertLessEqual(image['size'], 42)
result_set = set(map(lambda x: x['id'], images_list))
self.assertTrue(self.size42_set <= result_set)
self.assertFalse(self.created_set - self.size42_set <= result_set)
- @test.idempotent_id('6ffc16d0-4cbf-4401-95c8-4ac63eac34d8')
+ @decorators.idempotent_id('6ffc16d0-4cbf-4401-95c8-4ac63eac34d8')
def test_index_min_size(self):
images_list = self.client.list_images(size_min=142)['images']
for image in images_list:
- self.assertTrue(image['size'] >= 142)
+ self.assertGreaterEqual(image['size'], 142)
result_set = set(map(lambda x: x['id'], images_list))
self.assertTrue(self.size142_set <= result_set)
self.assertFalse(self.size42_set <= result_set)
- @test.idempotent_id('e5dc26d9-9aa2-48dd-bda5-748e1445da98')
+ @decorators.idempotent_id('e5dc26d9-9aa2-48dd-bda5-748e1445da98')
def test_index_status_active_detail(self):
images_list = self.client.list_images(detail=True,
status='active',
@@ -211,11 +268,11 @@
top_size = images_list[0]['size'] # We have non-zero sized images
for image in images_list:
size = image['size']
- self.assertTrue(size <= top_size)
+ self.assertLessEqual(size, top_size)
top_size = size
self.assertEqual(image['status'], 'active')
- @test.idempotent_id('097af10a-bae8-4342-bff4-edf89969ed2a')
+ @decorators.idempotent_id('097af10a-bae8-4342-bff4-edf89969ed2a')
def test_index_name(self):
images_list = self.client.list_images(
detail=True,
@@ -231,40 +288,43 @@
@classmethod
def resource_setup(cls):
super(UpdateImageMetaTest, cls).resource_setup()
- cls.image_id = cls._create_standard_image('1', 'ami', 'ami', 42)
+ container_format, disk_format = get_container_and_disk_format()
+ cls.image_id = cls._create_standard_image('1', container_format,
+ disk_format, 42)
@classmethod
def _create_standard_image(cls, name, container_format,
disk_format, size):
"""Create a new standard image and return newly-registered image-id"""
- image_file = moves.cStringIO(data_utils.random_bytes(size))
+ image_file = six.BytesIO(data_utils.random_bytes(size))
name = 'New Standard Image %s' % name
image = cls.create_image(name=name,
container_format=container_format,
disk_format=disk_format,
is_public=False, data=image_file,
properties={'key1': 'value1'})
- image_id = image['id']
- return image_id
+ return image['id']
- @test.idempotent_id('01752c1c-0275-4de3-9e5b-876e44541928')
+ @decorators.idempotent_id('01752c1c-0275-4de3-9e5b-876e44541928')
def test_list_image_metadata(self):
# All metadata key/value pairs for an image should be returned
- resp_metadata = self.client.get_image_meta(self.image_id)
+ resp = self.client.check_image(self.image_id)
+ resp_metadata = common_image.get_image_meta_from_headers(resp)
expected = {'key1': 'value1'}
self.assertEqual(expected, resp_metadata['properties'])
- @test.idempotent_id('d6d7649c-08ce-440d-9ea7-e3dda552f33c')
+ @decorators.idempotent_id('d6d7649c-08ce-440d-9ea7-e3dda552f33c')
def test_update_image_metadata(self):
# The metadata for the image should match the updated values
req_metadata = {'key1': 'alt1', 'key2': 'value2'}
- metadata = self.client.get_image_meta(self.image_id)
+ resp = self.client.check_image(self.image_id)
+ metadata = common_image.get_image_meta_from_headers(resp)
self.assertEqual(metadata['properties'], {'key1': 'value1'})
metadata['properties'].update(req_metadata)
- metadata = self.client.update_image(
- self.image_id, properties=metadata['properties'])['image']
-
- resp_metadata = self.client.get_image_meta(self.image_id)
- expected = {'key1': 'alt1', 'key2': 'value2'}
- self.assertEqual(expected, resp_metadata['properties'])
+ headers = common_image.image_meta_to_headers(
+ properties=metadata['properties'])
+ self.client.update_image(self.image_id, headers=headers)
+ resp = self.client.check_image(self.image_id)
+ resp_metadata = common_image.get_image_meta_from_headers(resp)
+ self.assertEqual(req_metadata, resp_metadata['properties'])
diff --git a/tempest/api/image/v1/test_images_negative.py b/tempest/api/image/v1/test_images_negative.py
index 3d94408..690b8da 100644
--- a/tempest/api/image/v1/test_images_negative.py
+++ b/tempest/api/image/v1/test_images_negative.py
@@ -13,67 +13,66 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
from tempest.api.image import base
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class CreateDeleteImagesNegativeTest(base.BaseV1ImageTest):
"""Here are negative tests for the deletion and creation of images."""
- @test.attr(type=['negative'])
- @test.idempotent_id('036ede36-6160-4463-8c01-c781eee6369d')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('036ede36-6160-4463-8c01-c781eee6369d')
def test_register_with_invalid_container_format(self):
# Negative tests for invalid data supplied to POST /images
self.assertRaises(lib_exc.BadRequest, self.client.create_image,
- 'test', 'wrong', 'vhd')
+ headers={'x-image-meta-name': 'test',
+ 'x-image-meta-container_format': 'wrong',
+ 'x-image-meta-disk_format': 'vhd'})
- @test.attr(type=['negative'])
- @test.idempotent_id('993face5-921d-4e84-aabf-c1bba4234a67')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('993face5-921d-4e84-aabf-c1bba4234a67')
def test_register_with_invalid_disk_format(self):
self.assertRaises(lib_exc.BadRequest, self.client.create_image,
- 'test', 'bare', 'wrong')
+ headers={'x-image-meta-name': 'test',
+ 'x-image-meta-container_format': 'bare',
+ 'x-image-meta-disk_format': 'wrong'})
- @test.attr(type=['negative'])
- @test.idempotent_id('bb016f15-0820-4f27-a92d-09b2f67d2488')
- def test_delete_image_with_invalid_image_id(self):
- # An image should not be deleted with invalid image id
- self.assertRaises(lib_exc.NotFound, self.client.delete_image,
- '!@$%^&*()')
-
- @test.attr(type=['negative'])
- @test.idempotent_id('ec652588-7e3c-4b67-a2f2-0fa96f57c8fc')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('ec652588-7e3c-4b67-a2f2-0fa96f57c8fc')
def test_delete_non_existent_image(self):
# Return an error while trying to delete a non-existent image
- non_existent_image_id = '11a22b9-12a9-5555-cc11-00ab112223fa'
+ non_existent_image_id = data_utils.rand_uuid()
self.assertRaises(lib_exc.NotFound, self.client.delete_image,
non_existent_image_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('04f72aa3-fcec-45a3-81a3-308ef7cc82bc')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('04f72aa3-fcec-45a3-81a3-308ef7cc82bc')
def test_delete_image_blank_id(self):
# Return an error while trying to delete an image with blank Id
self.assertRaises(lib_exc.NotFound, self.client.delete_image, '')
- @test.attr(type=['negative'])
- @test.idempotent_id('950e5054-a3c7-4dee-ada5-e576f1087abd')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('950e5054-a3c7-4dee-ada5-e576f1087abd')
def test_delete_image_non_hex_string_id(self):
# Return an error while trying to delete an image with non hex id
- image_id = '11a22b9-120q-5555-cc11-00ab112223gj'
+ invalid_image_id = data_utils.rand_uuid()[:-1] + "j"
self.assertRaises(lib_exc.NotFound, self.client.delete_image,
- image_id)
+ invalid_image_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('4ed757cd-450c-44b1-9fd1-c819748c650d')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('4ed757cd-450c-44b1-9fd1-c819748c650d')
def test_delete_image_negative_image_id(self):
# Return an error while trying to delete an image with negative id
self.assertRaises(lib_exc.NotFound, self.client.delete_image, -1)
- @test.attr(type=['negative'])
- @test.idempotent_id('a4a448ab-3db2-4d2d-b9b2-6a1271241dfe')
- def test_delete_image_id_is_over_35_character_limit(self):
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('a4a448ab-3db2-4d2d-b9b2-6a1271241dfe')
+ def test_delete_image_id_over_character_limit(self):
# Return an error while trying to delete image with id over limit
+ overlimit_image_id = data_utils.rand_uuid() + "1"
self.assertRaises(lib_exc.NotFound, self.client.delete_image,
- '11a22b9-12a9-5555-cc11-00ab112223fa-3fac')
+ overlimit_image_id)
diff --git a/tempest/api/image/admin/__init__.py b/tempest/api/image/v2/admin/__init__.py
similarity index 100%
rename from tempest/api/image/admin/__init__.py
rename to tempest/api/image/v2/admin/__init__.py
diff --git a/tempest/api/image/v2/admin/test_images.py b/tempest/api/image/v2/admin/test_images.py
new file mode 100644
index 0000000..dbb8c58
--- /dev/null
+++ b/tempest/api/image/v2/admin/test_images.py
@@ -0,0 +1,51 @@
+# 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.api.image import base
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+
+
+class BasicOperationsImagesAdminTest(base.BaseV2ImageAdminTest):
+
+ @decorators.related_bug('1420008')
+ @decorators.idempotent_id('646a6eaa-135f-4493-a0af-12583021224e')
+ def test_create_image_owner_param(self):
+ # NOTE: Create image with owner different from tenant owner by
+ # using "owner" parameter requires an admin privileges.
+ random_id = data_utils.rand_uuid_hex()
+ image = self.admin_client.create_image(
+ container_format='bare', disk_format='raw', owner=random_id)
+ self.addCleanup(self.admin_client.delete_image, image['id'])
+ image_info = self.admin_client.show_image(image['id'])
+ self.assertEqual(random_id, image_info['owner'])
+
+ @decorators.related_bug('1420008')
+ @decorators.idempotent_id('525ba546-10ef-4aad-bba1-1858095ce553')
+ def test_update_image_owner_param(self):
+ random_id_1 = data_utils.rand_uuid_hex()
+ image = self.admin_client.create_image(
+ container_format='bare', disk_format='raw', owner=random_id_1)
+ self.addCleanup(self.admin_client.delete_image, image['id'])
+ created_image_info = self.admin_client.show_image(image['id'])
+
+ random_id_2 = data_utils.rand_uuid_hex()
+ self.admin_client.update_image(
+ image['id'], [dict(replace="/owner", value=random_id_2)])
+ updated_image_info = self.admin_client.show_image(image['id'])
+
+ self.assertEqual(random_id_2, updated_image_info['owner'])
+ self.assertNotEqual(created_image_info['owner'],
+ updated_image_info['owner'])
diff --git a/tempest/api/image/v2/test_images.py b/tempest/api/image/v2/test_images.py
index 2e6c268..aa57daf 100644
--- a/tempest/api/image/v2/test_images.py
+++ b/tempest/api/image/v2/test_images.py
@@ -16,13 +16,14 @@
import random
-from six import moves
+import six
from oslo_log import log as logging
from tempest.api.image import base
-from tempest.common.utils import data_utils
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
CONF = config.CONF
LOG = logging.getLogger(__name__)
@@ -31,8 +32,8 @@
class BasicOperationsImagesTest(base.BaseV2ImageTest):
"""Here we test the basic operations of images"""
- @test.attr(type='smoke')
- @test.idempotent_id('139b765e-7f3d-4b3d-8b37-3ca3876ee318')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('139b765e-7f3d-4b3d-8b37-3ca3876ee318')
def test_register_upload_get_image_file(self):
"""Here we test these functionalities
@@ -44,39 +45,41 @@
image_name = data_utils.rand_name('image')
container_format = CONF.image.container_formats[0]
disk_format = CONF.image.disk_formats[0]
- body = self.create_image(name=image_name,
- container_format=container_format,
- disk_format=disk_format,
- visibility='private',
- ramdisk_id=uuid)
- self.assertIn('id', body)
- image_id = body.get('id')
- self.assertIn('name', body)
- self.assertEqual(image_name, body['name'])
- self.assertIn('visibility', body)
- self.assertEqual('private', body['visibility'])
- self.assertIn('status', body)
- self.assertEqual('queued', body['status'])
+ image = self.create_image(name=image_name,
+ container_format=container_format,
+ disk_format=disk_format,
+ visibility='private',
+ ramdisk_id=uuid)
+ self.assertIn('name', image)
+ self.assertEqual(image_name, image['name'])
+ self.assertIn('visibility', image)
+ self.assertEqual('private', image['visibility'])
+ self.assertIn('status', image)
+ self.assertEqual('queued', image['status'])
# Now try uploading an image file
file_content = data_utils.random_bytes()
- image_file = moves.cStringIO(file_content)
- self.client.store_image_file(image_id, image_file)
+ image_file = six.BytesIO(file_content)
+ self.client.store_image_file(image['id'], image_file)
# Now try to get image details
- body = self.client.show_image(image_id)
- self.assertEqual(image_id, body['id'])
+ body = self.client.show_image(image['id'])
+ self.assertEqual(image['id'], body['id'])
self.assertEqual(image_name, body['name'])
self.assertEqual(uuid, body['ramdisk_id'])
self.assertIn('size', body)
self.assertEqual(1024, body.get('size'))
# Now try get image file
- body = self.client.show_image_file(image_id)
+ # 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)
- @test.attr(type='smoke')
- @test.idempotent_id('f848bb94-1c6e-45a4-8726-39e3a5b23535')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('f848bb94-1c6e-45a4-8726-39e3a5b23535')
def test_delete_image(self):
# Deletes an image by image_id
@@ -84,23 +87,21 @@
image_name = data_utils.rand_name('image')
container_format = CONF.image.container_formats[0]
disk_format = CONF.image.disk_formats[0]
- body = self.client.create_image(name=image_name,
- container_format=container_format,
- disk_format=disk_format,
- visibility='private')
- image_id = body['id']
-
+ image = self.create_image(name=image_name,
+ container_format=container_format,
+ disk_format=disk_format,
+ visibility='private')
# Delete Image
- self.client.delete_image(image_id)
- self.client.wait_for_resource_deletion(image_id)
+ self.client.delete_image(image['id'])
+ self.client.wait_for_resource_deletion(image['id'])
# Verifying deletion
images = self.client.list_images()['images']
images_id = [item['id'] for item in images]
- self.assertNotIn(image_id, images_id)
+ self.assertNotIn(image['id'], images_id)
- @test.attr(type='smoke')
- @test.idempotent_id('f66891a7-a35c-41a8-b590-a065c2a1caa6')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('f66891a7-a35c-41a8-b590-a065c2a1caa6')
def test_update_image(self):
# Updates an image by image_id
@@ -108,36 +109,73 @@
image_name = data_utils.rand_name('image')
container_format = CONF.image.container_formats[0]
disk_format = CONF.image.disk_formats[0]
- body = self.client.create_image(name=image_name,
- container_format=container_format,
- disk_format=disk_format,
- visibility='private')
- self.addCleanup(self.client.delete_image, body['id'])
- self.assertEqual('queued', body['status'])
- image_id = body['id']
+ image = self.create_image(name=image_name,
+ container_format=container_format,
+ disk_format=disk_format,
+ 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 = moves.cStringIO(data_utils.random_bytes())
- self.client.store_image_file(image_id, image_file)
+ image_file = six.BytesIO(data_utils.random_bytes())
+ self.client.store_image_file(image['id'], image_file)
# Update Image
new_image_name = data_utils.rand_name('new-image')
- body = self.client.update_image(image_id, [
+ self.client.update_image(image['id'], [
dict(replace='/name', value=new_image_name)])
# Verifying updating
- body = self.client.show_image(image_id)
- self.assertEqual(image_id, body['id'])
+ body = self.client.show_image(image['id'])
+ self.assertEqual(image['id'], body['id'])
self.assertEqual(new_image_name, body['name'])
+ @decorators.idempotent_id('951ebe01-969f-4ea9-9898-8a3f1f442ab0')
+ def test_deactivate_reactivate_image(self):
+ # Create image
+ image_name = data_utils.rand_name('image')
+ image = self.create_image(name=image_name,
+ container_format='bare',
+ disk_format='raw',
+ visibility='private')
-class ListImagesTest(base.BaseV2ImageTest):
+ # Upload an image file
+ content = data_utils.random_bytes()
+ image_file = six.BytesIO(content)
+ self.client.store_image_file(image['id'], image_file)
+
+ # Deactivate image
+ self.client.deactivate_image(image['id'])
+ body = self.client.show_image(image['id'])
+ self.assertEqual("deactivated", body['status'])
+
+ # User unable to download deactivated image
+ self.assertRaises(lib_exc.Forbidden, self.client.show_image_file,
+ image['id'])
+
+ # Reactivate image
+ self.client.reactivate_image(image['id'])
+ body = self.client.show_image(image['id'])
+ self.assertEqual("active", body['status'])
+
+ # User able to download image after reactivation
+ body = self.client.show_image_file(image['id'])
+ self.assertEqual(content, body.data)
+
+
+class ListUserImagesTest(base.BaseV2ImageTest):
"""Here we test the listing of image information"""
@classmethod
def resource_setup(cls):
- super(ListImagesTest, cls).resource_setup()
+ super(ListUserImagesTest, cls).resource_setup()
# We add a few images here to test the listing functionality of
# the images API
container_fmts = CONF.image.container_formats
@@ -147,7 +185,7 @@
for disk_fmt in disk_fmts]
for (container_fmt, disk_fmt) in all_pairs[:6]:
- LOG.debug("Creating a image"
+ LOG.debug("Creating an image"
"(Container format: %s, Disk format: %s).",
container_fmt, disk_fmt)
cls._create_standard_image(container_fmt, disk_fmt)
@@ -160,55 +198,73 @@
1024 and 4096
"""
size = random.randint(1024, 4096)
- image_file = moves.cStringIO(data_utils.random_bytes(size))
- name = data_utils.rand_name('image')
- body = cls.create_image(name=name,
- container_format=container_format,
- disk_format=disk_format,
- visibility='private')
- image_id = body['id']
- cls.client.store_image_file(image_id, data=image_file)
+ image_file = six.BytesIO(data_utils.random_bytes(size))
+ tags = [data_utils.rand_name('tag'), data_utils.rand_name('tag')]
+ image = cls.create_image(container_format=container_format,
+ disk_format=disk_format,
+ visibility='private',
+ tags=tags)
+ cls.client.store_image_file(image['id'], data=image_file)
+ # Keep the data of one test image so it can be used to filter lists
+ cls.test_data = image
- return image_id
+ return image['id']
def _list_by_param_value_and_assert(self, params):
"""Perform list action with given params and validates result."""
-
+ # Retrieve the list of images that meet the filter
images_list = self.client.list_images(params=params)['images']
# Validating params of fetched images
+ msg = 'No images were found that met the filter criteria.'
+ self.assertNotEmpty(images_list, msg)
for image in images_list:
for key in params:
msg = "Failed to list images by %s" % key
self.assertEqual(params[key], image[key], msg)
- @test.idempotent_id('1e341d7a-90a9-494c-b143-2cdf2aeb6aee')
+ def _list_sorted_by_image_size_and_assert(self, params, desc=False):
+ """Validate an image list that has been sorted by size
+
+ Perform list action with given params and validates the results are
+ sorted by image size in either ascending or descending order.
+ """
+ # Retrieve the list of images that meet the filter
+ images_list = self.client.list_images(params=params)['images']
+ # Validate that the list was fetched sorted accordingly
+ msg = 'No images were found that met the filter criteria.'
+ self.assertNotEmpty(images_list, msg)
+ sorted_list = [image['size'] for image in images_list]
+ msg = 'The list of images was not sorted correctly.'
+ self.assertEqual(sorted(sorted_list, reverse=desc), sorted_list, msg)
+
+ @decorators.idempotent_id('1e341d7a-90a9-494c-b143-2cdf2aeb6aee')
def test_list_no_params(self):
# Simple test to see all fixture images returned
images_list = self.client.list_images()['images']
- image_list = map(lambda x: x['id'], images_list)
+ image_list = [image['id'] for image in images_list]
for image in self.created_images:
self.assertIn(image, image_list)
- @test.idempotent_id('9959ca1d-1aa7-4b7a-a1ea-0fff0499b37e')
+ @decorators.idempotent_id('9959ca1d-1aa7-4b7a-a1ea-0fff0499b37e')
def test_list_images_param_container_format(self):
- # Test to get all images with container_format='bare'
- params = {"container_format": "bare"}
+ # Test to get all images with a specific container_format
+ params = {"container_format": self.test_data['container_format']}
self._list_by_param_value_and_assert(params)
- @test.idempotent_id('4a4735a7-f22f-49b6-b0d9-66e1ef7453eb')
+ @decorators.idempotent_id('4a4735a7-f22f-49b6-b0d9-66e1ef7453eb')
def test_list_images_param_disk_format(self):
# Test to get all images with disk_format = raw
params = {"disk_format": "raw"}
self._list_by_param_value_and_assert(params)
- @test.idempotent_id('7a95bb92-d99e-4b12-9718-7bc6ab73e6d2')
+ @decorators.idempotent_id('7a95bb92-d99e-4b12-9718-7bc6ab73e6d2')
def test_list_images_param_visibility(self):
# Test to get all images with visibility = private
params = {"visibility": "private"}
self._list_by_param_value_and_assert(params)
- @test.idempotent_id('cf1b9a48-8340-480e-af7b-fe7e17690876')
+ @decorators.idempotent_id('cf1b9a48-8340-480e-af7b-fe7e17690876')
def test_list_images_param_size(self):
# Test to get all images by size
image_id = self.created_images[0]
@@ -218,7 +274,7 @@
params = {"size": image['size']}
self._list_by_param_value_and_assert(params)
- @test.idempotent_id('4ad8c157-971a-4ba8-aa84-ed61154b1e7f')
+ @decorators.idempotent_id('4ad8c157-971a-4ba8-aa84-ed61154b1e7f')
def test_list_images_param_min_max_size(self):
# Test to get all images with size between 2000 to 3000
image_id = self.created_images[0]
@@ -231,17 +287,18 @@
image_size_list = map(lambda x: x['size'], images_list)
for image_size in image_size_list:
- self.assertTrue(image_size >= params['size_min'] and
- image_size <= params['size_max'],
- "Failed to get images by size_min and size_max")
+ self.assertGreaterEqual(image_size, params['size_min'],
+ "Failed to get images by size_min")
+ self.assertLessEqual(image_size, params['size_max'],
+ "Failed to get images by size_max")
- @test.idempotent_id('7fc9e369-0f58-4d05-9aa5-0969e2d59d15')
+ @decorators.idempotent_id('7fc9e369-0f58-4d05-9aa5-0969e2d59d15')
def test_list_images_param_status(self):
# Test to get all active images
params = {"status": "active"}
self._list_by_param_value_and_assert(params)
- @test.idempotent_id('e914a891-3cc8-4b40-ad32-e0a39ffbddbb')
+ @decorators.idempotent_id('e914a891-3cc8-4b40-ad32-e0a39ffbddbb')
def test_list_images_param_limit(self):
# Test to get images by limit
params = {"limit": 1}
@@ -250,16 +307,91 @@
self.assertEqual(len(images_list), params['limit'],
"Failed to get images by limit")
- @test.idempotent_id('622b925c-479f-4736-860d-adeaf13bc371')
+ @decorators.idempotent_id('e9a44b91-31c8-4b40-a332-e0a39ffb4dbb')
+ def test_list_image_param_owner(self):
+ # Test to get images by owner
+ image_id = self.created_images[0]
+ # Get image metadata
+ image = self.client.show_image(image_id)
+
+ params = {"owner": image['owner']}
+ self._list_by_param_value_and_assert(params)
+
+ @decorators.idempotent_id('55c8f5f5-bfed-409d-a6d5-4caeda985d7b')
+ def test_list_images_param_name(self):
+ # Test to get images by name
+ params = {'name': self.test_data['name']}
+ self._list_by_param_value_and_assert(params)
+
+ @decorators.idempotent_id('aa8ac4df-cff9-418b-8d0f-dd9c67b072c9')
+ def test_list_images_param_tag(self):
+ # Test to get images matching a tag
+ params = {'tag': self.test_data['tags'][0]}
+ images_list = self.client.list_images(params=params)['images']
+ # Validating properties of fetched images
+ self.assertNotEmpty(images_list)
+ for image in images_list:
+ msg = ("The image {image_name} does not have the expected tag "
+ "{expected_tag} among its tags: {observerd_tags}."
+ .format(image_name=image['name'],
+ expected_tag=self.test_data['tags'][0],
+ observerd_tags=image['tags']))
+ self.assertIn(self.test_data['tags'][0], image['tags'], msg)
+
+ @decorators.idempotent_id('eeadce49-04e0-43b7-aec7-52535d903e7a')
+ def test_list_images_param_sort(self):
+ params = {'sort': 'size:desc'}
+ self._list_sorted_by_image_size_and_assert(params, desc=True)
+
+ @decorators.idempotent_id('9faaa0c2-c3a5-43e1-8f61-61c54b409a49')
+ def test_list_images_param_sort_key_dir(self):
+ params = {'sort_key': 'size', 'sort_dir': 'desc'}
+ self._list_sorted_by_image_size_and_assert(params, desc=True)
+
+ @decorators.idempotent_id('622b925c-479f-4736-860d-adeaf13bc371')
def test_get_image_schema(self):
# Test to get image schema
schema = "image"
- body = self.client.show_schema(schema)
+ body = self.schemas_client.show_schema(schema)
self.assertEqual("image", body['name'])
- @test.idempotent_id('25c8d7b2-df21-460f-87ac-93130bcdc684')
+ @decorators.idempotent_id('25c8d7b2-df21-460f-87ac-93130bcdc684')
def test_get_images_schema(self):
# Test to get images schema
schema = "images"
- body = self.client.show_schema(schema)
+ body = self.schemas_client.show_schema(schema)
self.assertEqual("images", body['name'])
+
+
+class ListSharedImagesTest(base.BaseV2ImageTest):
+ """Here we test the listing of a shared image information"""
+
+ credentials = ['primary', 'alt']
+
+ @classmethod
+ def setup_clients(cls):
+ super(ListSharedImagesTest, cls).setup_clients()
+ cls.image_member_client = cls.os_primary.image_member_client_v2
+ cls.alt_img_client = cls.os_alt.image_client_v2
+
+ @decorators.idempotent_id('3fa50be4-8e38-4c02-a8db-7811bb780122')
+ def test_list_images_param_member_status(self):
+ # Create an image to be shared using default visibility
+ image_file = six.BytesIO(data_utils.random_bytes(2048))
+ container_format = CONF.image.container_formats[0]
+ disk_format = CONF.image.disk_formats[0]
+ image = self.create_image(container_format=container_format,
+ disk_format=disk_format)
+ self.client.store_image_file(image['id'], data=image_file)
+
+ # Share the image created with the alt user
+ self.image_member_client.create_image_member(
+ image_id=image['id'], member=self.alt_img_client.tenant_id)
+
+ # As an image consumer you need to provide the member_status parameter
+ # along with the visibility=shared parameter in order for it to show
+ # results
+ params = {'member_status': 'pending', 'visibility': 'shared'}
+ fetched_images = self.alt_img_client.list_images(params)['images']
+ self.assertEqual(1, len(fetched_images))
+ self.assertEqual(image['id'], fetched_images[0]['id'])
diff --git a/tempest/api/image/v2/test_images_member.py b/tempest/api/image/v2/test_images_member.py
index bb73318..0208780 100644
--- a/tempest/api/image/v2/test_images_member.py
+++ b/tempest/api/image/v2/test_images_member.py
@@ -11,25 +11,27 @@
# under the License.
from tempest.api.image import base
-from tempest import test
+from tempest.lib import decorators
class ImagesMemberTest(base.BaseV2MemberImageTest):
- @test.idempotent_id('5934c6ea-27dc-4d6e-9421-eeb5e045494a')
+ @decorators.idempotent_id('5934c6ea-27dc-4d6e-9421-eeb5e045494a')
def test_image_share_accept(self):
image_id = self._create_image()
- member = self.os_img_client.create_image_member(
+ member = self.image_member_client.create_image_member(
image_id, member=self.alt_tenant_id)
+ self.addCleanup(self.image_member_client.delete_image_member,
+ image_id, self.alt_tenant_id)
self.assertEqual(member['member_id'], self.alt_tenant_id)
self.assertEqual(member['image_id'], image_id)
self.assertEqual(member['status'], 'pending')
self.assertNotIn(image_id, self._list_image_ids_as_alt())
- self.alt_img_client.update_image_member(image_id,
- self.alt_tenant_id,
- status='accepted')
+ self.alt_image_member_client.update_image_member(image_id,
+ self.alt_tenant_id,
+ status='accepted')
self.assertIn(image_id, self._list_image_ids_as_alt())
- body = self.os_img_client.list_image_members(image_id)
+ body = self.image_member_client.list_image_members(image_id)
members = body['members']
member = members[0]
self.assertEqual(len(members), 1, str(members))
@@ -37,71 +39,60 @@
self.assertEqual(member['image_id'], image_id)
self.assertEqual(member['status'], 'accepted')
- @test.idempotent_id('d9e83e5f-3524-4b38-a900-22abcb26e90e')
+ @decorators.idempotent_id('d9e83e5f-3524-4b38-a900-22abcb26e90e')
def test_image_share_reject(self):
image_id = self._create_image()
- member = self.os_img_client.create_image_member(
+ member = self.image_member_client.create_image_member(
image_id, member=self.alt_tenant_id)
+ self.addCleanup(self.image_member_client.delete_image_member,
+ image_id, self.alt_tenant_id)
self.assertEqual(member['member_id'], self.alt_tenant_id)
self.assertEqual(member['image_id'], image_id)
self.assertEqual(member['status'], 'pending')
self.assertNotIn(image_id, self._list_image_ids_as_alt())
- self.alt_img_client.update_image_member(image_id,
- self.alt_tenant_id,
- status='rejected')
+ self.alt_image_member_client.update_image_member(image_id,
+ self.alt_tenant_id,
+ status='rejected')
self.assertNotIn(image_id, self._list_image_ids_as_alt())
- @test.idempotent_id('a6ee18b9-4378-465e-9ad9-9a6de58a3287')
+ @decorators.idempotent_id('a6ee18b9-4378-465e-9ad9-9a6de58a3287')
def test_get_image_member(self):
image_id = self._create_image()
- self.os_img_client.create_image_member(
+ self.image_member_client.create_image_member(
image_id, member=self.alt_tenant_id)
- self.alt_img_client.update_image_member(image_id,
- self.alt_tenant_id,
- status='accepted')
+ self.addCleanup(self.image_member_client.delete_image_member,
+ image_id, self.alt_tenant_id)
+ self.alt_image_member_client.update_image_member(image_id,
+ self.alt_tenant_id,
+ status='accepted')
self.assertIn(image_id, self._list_image_ids_as_alt())
- member = self.os_img_client.show_image_member(image_id,
- self.alt_tenant_id)
+ member = self.image_member_client.show_image_member(
+ image_id, self.alt_tenant_id)
self.assertEqual(self.alt_tenant_id, member['member_id'])
self.assertEqual(image_id, member['image_id'])
self.assertEqual('accepted', member['status'])
- @test.idempotent_id('72989bc7-2268-48ed-af22-8821e835c914')
+ @decorators.idempotent_id('72989bc7-2268-48ed-af22-8821e835c914')
def test_remove_image_member(self):
image_id = self._create_image()
- self.os_img_client.create_image_member(
+ self.image_member_client.create_image_member(
image_id, member=self.alt_tenant_id)
- self.alt_img_client.update_image_member(image_id,
- self.alt_tenant_id,
- status='accepted')
+ self.alt_image_member_client.update_image_member(image_id,
+ self.alt_tenant_id,
+ status='accepted')
self.assertIn(image_id, self._list_image_ids_as_alt())
- self.os_img_client.delete_image_member(image_id, self.alt_tenant_id)
+ self.image_member_client.delete_image_member(image_id,
+ self.alt_tenant_id)
self.assertNotIn(image_id, self._list_image_ids_as_alt())
- @test.idempotent_id('634dcc3f-f6e2-4409-b8fd-354a0bb25d83')
+ @decorators.idempotent_id('634dcc3f-f6e2-4409-b8fd-354a0bb25d83')
def test_get_image_member_schema(self):
- body = self.os_img_client.show_schema("member")
+ body = self.schemas_client.show_schema("member")
self.assertEqual("member", body['name'])
- @test.idempotent_id('6ae916ef-1052-4e11-8d36-b3ae14853cbb')
+ @decorators.idempotent_id('6ae916ef-1052-4e11-8d36-b3ae14853cbb')
def test_get_image_members_schema(self):
- body = self.os_img_client.show_schema("members")
+ body = self.schemas_client.show_schema("members")
self.assertEqual("members", body['name'])
-
- @test.idempotent_id('cb961424-3f68-4d21-8e36-30ad66fb6bfb')
- def test_get_private_image(self):
- image_id = self._create_image()
- member = self.os_img_client.create_image_member(
- image_id, member=self.alt_tenant_id)
- self.assertEqual(member['member_id'], self.alt_tenant_id)
- self.assertEqual(member['image_id'], image_id)
- self.assertEqual(member['status'], 'pending')
- self.assertNotIn(image_id, self._list_image_ids_as_alt())
- self.alt_img_client.update_image_member(image_id,
- self.alt_tenant_id,
- status='accepted')
- self.assertIn(image_id, self._list_image_ids_as_alt())
- self.os_img_client.delete_image_member(image_id, self.alt_tenant_id)
- self.assertNotIn(image_id, self._list_image_ids_as_alt())
diff --git a/tempest/api/image/v2/test_images_member_negative.py b/tempest/api/image/v2/test_images_member_negative.py
index eb90719..caa90f9 100644
--- a/tempest/api/image/v2/test_images_member_negative.py
+++ b/tempest/api/image/v2/test_images_member_negative.py
@@ -10,35 +10,34 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.image import base
-from tempest import test
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class ImagesMemberNegativeTest(base.BaseV2MemberImageTest):
- @test.attr(type=['negative'])
- @test.idempotent_id('b79efb37-820d-4cf0-b54c-308b00cf842c')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('b79efb37-820d-4cf0-b54c-308b00cf842c')
def test_image_share_invalid_status(self):
image_id = self._create_image()
- member = self.os_img_client.create_image_member(
+ member = self.image_member_client.create_image_member(
image_id, member=self.alt_tenant_id)
self.assertEqual(member['status'], 'pending')
self.assertRaises(lib_exc.BadRequest,
- self.alt_img_client.update_image_member,
+ self.alt_image_member_client.update_image_member,
image_id, self.alt_tenant_id,
status='notavalidstatus')
- @test.attr(type=['negative'])
- @test.idempotent_id('27002f74-109e-4a37-acd0-f91cd4597967')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('27002f74-109e-4a37-acd0-f91cd4597967')
def test_image_share_owner_cannot_accept(self):
image_id = self._create_image()
- member = self.os_img_client.create_image_member(
+ member = self.image_member_client.create_image_member(
image_id, member=self.alt_tenant_id)
self.assertEqual(member['status'], 'pending')
self.assertNotIn(image_id, self._list_image_ids_as_alt())
self.assertRaises(lib_exc.Forbidden,
- self.os_img_client.update_image_member,
+ self.image_member_client.update_image_member,
image_id, self.alt_tenant_id, status='accepted')
self.assertNotIn(image_id, self._list_image_ids_as_alt())
diff --git a/tempest/api/image/v2/test_images_metadefs_namespace_objects.py b/tempest/api/image/v2/test_images_metadefs_namespace_objects.py
new file mode 100644
index 0000000..80f8112
--- /dev/null
+++ b/tempest/api/image/v2/test_images_metadefs_namespace_objects.py
@@ -0,0 +1,73 @@
+# 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.image import base
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+
+
+class MetadataNamespaceObjectsTest(base.BaseV2ImageTest):
+ """Test the Metadata definition namespace objects basic functionality"""
+
+ def _create_namespace_object(self, namespace):
+ object_name = data_utils.rand_name(self.__class__.__name__ + '-object')
+ namespace_object = self.namespace_objects_client.\
+ create_namespace_object(namespace['namespace'], name=object_name)
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.namespace_objects_client.delete_namespace_object,
+ namespace['namespace'], object_name)
+ return namespace_object
+
+ @decorators.idempotent_id('b1a3775e-3b5c-4f6a-a3b4-1ba3574ae718')
+ def test_create_update_delete_meta_namespace_objects(self):
+ # Create a namespace
+ namespace = self.create_namespace()
+ # Create a namespace object
+ body = self._create_namespace_object(namespace)
+ # Update a namespace object
+ up_object_name = data_utils.rand_name('update-object')
+ body = self.namespace_objects_client.update_namespace_object(
+ namespace['namespace'], body['name'],
+ name=up_object_name)
+ self.assertEqual(up_object_name, body['name'])
+ # Delete a namespace object
+ self.namespace_objects_client.delete_namespace_object(
+ namespace['namespace'], up_object_name)
+ # List namespace objects and validate deletion
+ namespace_objects = [
+ namespace_object['name'] for namespace_object in
+ self.namespace_objects_client.list_namespace_objects(
+ namespace['namespace'])['objects']]
+ self.assertNotIn(up_object_name, namespace_objects)
+
+ @decorators.idempotent_id('a2a3615e-3b5c-3f6a-a2b1-1ba3574ae738')
+ def test_list_meta_namespace_objects(self):
+ # Create a namespace object
+ namespace = self.create_namespace()
+ meta_namespace_object = self._create_namespace_object(namespace)
+ # List namespace objects
+ namespace_objects = [
+ namespace_object['name'] for namespace_object in
+ self.namespace_objects_client.list_namespace_objects(
+ namespace['namespace'])['objects']]
+ self.assertIn(meta_namespace_object['name'], namespace_objects)
+
+ @decorators.idempotent_id('b1a3674e-3b4c-3f6a-a3b4-1ba3573ca768')
+ def test_show_meta_namespace_objects(self):
+ # Create a namespace object
+ namespace = self.create_namespace()
+ namespace_object = self._create_namespace_object(namespace)
+ # Show a namespace object
+ body = self.namespace_objects_client.show_namespace_object(
+ namespace['namespace'], namespace_object['name'])
+ self.assertEqual(namespace_object['name'], body['name'])
diff --git a/tempest/api/image/v2/test_images_metadefs_namespace_properties.py b/tempest/api/image/v2/test_images_metadefs_namespace_properties.py
new file mode 100644
index 0000000..ed91726
--- /dev/null
+++ b/tempest/api/image/v2/test_images_metadefs_namespace_properties.py
@@ -0,0 +1,57 @@
+# 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.image import base
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+
+
+class MetadataNamespacePropertiesTest(base.BaseV2ImageTest):
+ """Test the Metadata definition namespace property basic functionality"""
+
+ @decorators.idempotent_id('b1a3765e-3a5d-4f6d-a3a7-3ca3476ae768')
+ def test_basic_meta_def_namespace_property(self):
+ # Get the available resource types and use one resource_type
+ body = self.resource_types_client.list_resource_types()
+ resource_name = body['resource_types'][0]['name']
+ enum = ["xen", "qemu", "kvm", "lxc", "uml", "vmware", "hyperv"]
+ # Create a namespace
+ namespace = self.create_namespace()
+ # Create resource type association
+ body = self.resource_types_client.create_resource_type_association(
+ namespace['namespace'], name=resource_name)
+ # Create a property
+ property_title = data_utils.rand_name('property')
+ body = self.namespace_properties_client.create_namespace_property(
+ namespace=namespace['namespace'], title=property_title,
+ name=resource_name, type="string", enum=enum)
+ self.assertEqual(property_title, body['title'])
+ # Show namespace property
+ body = self.namespace_properties_client.show_namespace_properties(
+ namespace['namespace'], resource_name)
+ self.assertEqual(resource_name, body['name'])
+ # Update namespace property
+ update_property_title = data_utils.rand_name('update-property')
+ body = self.namespace_properties_client.update_namespace_properties(
+ namespace['namespace'], resource_name,
+ title=update_property_title, type="string",
+ enum=enum, name=resource_name)
+ self.assertEqual(update_property_title, body['title'])
+ # Delete namespace property
+ self.namespace_properties_client.delete_namespace_property(
+ namespace['namespace'], resource_name)
+ # List namespace properties and validate deletion
+ namespace_property = [
+ namespace_property['title'] for namespace_property in
+ self.namespace_properties_client.list_namespace_properties(
+ namespace['namespace'])['properties']]
+ self.assertNotIn(update_property_title, namespace_property)
diff --git a/tempest/api/image/v2/test_images_metadefs_namespace_tags.py b/tempest/api/image/v2/test_images_metadefs_namespace_tags.py
new file mode 100644
index 0000000..482e808
--- /dev/null
+++ b/tempest/api/image/v2/test_images_metadefs_namespace_tags.py
@@ -0,0 +1,90 @@
+# 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.image import base
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+
+
+class MetadataNamespaceTagsTest(base.BaseV2ImageTest):
+ """Test the Metadata definition namespace tags basic functionality"""
+
+ tags = [
+ {
+ "name": "sample-tag1"
+ },
+ {
+ "name": "sample-tag2"
+ },
+ {
+ "name": "sample-tag3"
+ }
+ ]
+ tag_list = ["sample-tag1", "sample-tag2", "sample-tag3"]
+
+ def _create_namespace_tags(self, namespace):
+ # Create a namespace
+ namespace_tags = self.namespace_tags_client.create_namespace_tags(
+ namespace['namespace'], tags=self.tags)
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.namespace_tags_client.delete_namespace_tags,
+ namespace['namespace'])
+ return namespace_tags
+
+ @decorators.idempotent_id('a2a3765e-3a6d-4f6d-a3a7-3cc3476aa876')
+ def test_create_list_delete_namespace_tags(self):
+ # Create a namespace
+ namespace = self.create_namespace()
+ self._create_namespace_tags(namespace)
+ # List namespace tags
+ body = self.namespace_tags_client.list_namespace_tags(
+ namespace['namespace'])
+ self.assertEqual(3, len(body['tags']))
+ self.assertIn(body['tags'][0]['name'], self.tag_list)
+ self.assertIn(body['tags'][1]['name'], self.tag_list)
+ self.assertIn(body['tags'][2]['name'], self.tag_list)
+ # Delete all tag definitions
+ self.namespace_tags_client.delete_namespace_tags(
+ namespace['namespace'])
+ body = self.namespace_tags_client.list_namespace_tags(
+ namespace['namespace'])
+ self.assertEmpty(body['tags'])
+
+ @decorators.idempotent_id('a2a3765e-1a2c-3f6d-a3a7-3cc3466ab875')
+ def test_create_update_delete_tag(self):
+ # Create a namespace
+ namespace = self.create_namespace()
+ self._create_namespace_tags(namespace)
+ # Create a tag
+ tag_name = data_utils.rand_name('tag_name')
+ self.namespace_tags_client.create_namespace_tag(
+ namespace=namespace['namespace'], tag_name=tag_name)
+
+ body = self.namespace_tags_client.show_namespace_tag(
+ namespace['namespace'], tag_name)
+ self.assertEqual(tag_name, body['name'])
+ # Update tag definition
+ update_tag_definition = data_utils.rand_name('update-tag')
+ body = self.namespace_tags_client.update_namespace_tag(
+ namespace['namespace'], tag_name=tag_name,
+ name=update_tag_definition)
+ self.assertEqual(update_tag_definition, body['name'])
+ # Delete tag definition
+ self.namespace_tags_client.delete_namespace_tag(
+ namespace['namespace'], update_tag_definition)
+ # List namespace tags and validate deletion
+ namespace_tags = [
+ namespace_tag['name'] for namespace_tag in
+ self.namespace_tags_client.list_namespace_tags(
+ namespace['namespace'])['tags']]
+ self.assertNotIn(update_tag_definition, namespace_tags)
diff --git a/tempest/api/image/v2/test_images_metadefs_namespaces.py b/tempest/api/image/v2/test_images_metadefs_namespaces.py
index efb7b8b..f71b16c 100644
--- a/tempest/api/image/v2/test_images_metadefs_namespaces.py
+++ b/tempest/api/image/v2/test_images_metadefs_namespaces.py
@@ -14,57 +14,63 @@
# under the License.
from tempest.api.image import base
-from tempest.common.utils import data_utils
-from tempest import test
-from tempest_lib import exceptions as lib_exc
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class MetadataNamespacesTest(base.BaseV2ImageTest):
"""Test the Metadata definition Namespaces basic functionality"""
- @test.idempotent_id('319b765e-7f3d-4b3d-8b37-3ca3876ee768')
+ @decorators.idempotent_id('319b765e-7f3d-4b3d-8b37-3ca3876ee768')
def test_basic_metadata_definition_namespaces(self):
# get the available resource types and use one resource_type
- body = self.client.list_resource_types()
+ body = self.resource_types_client.list_resource_types()
resource_name = body['resource_types'][0]['name']
name = [{'name': resource_name}]
namespace_name = data_utils.rand_name('namespace')
# create the metadef namespace
- body = self.client.create_namespace(namespace=namespace_name,
- visibility='public',
- description='Tempest',
- display_name=namespace_name,
- resource_type_associations=name,
- protected=True)
- self.addCleanup(self._cleanup_namespace, namespace_name)
+ body = self.namespaces_client.create_namespace(
+ namespace=namespace_name,
+ visibility='public',
+ description='Tempest',
+ display_name=namespace_name,
+ resource_type_associations=name,
+ protected=True)
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self._cleanup_namespace, namespace_name)
+ # list namespaces
+ bodys = self.namespaces_client.list_namespaces()['namespaces']
+ body = [namespace['namespace'] for namespace in bodys]
+ self.assertIn(namespace_name, body)
# get namespace details
- body = self.client.show_namespace(namespace_name)
+ body = self.namespaces_client.show_namespace(namespace_name)
self.assertEqual(namespace_name, body['namespace'])
self.assertEqual('public', body['visibility'])
# unable to delete protected namespace
- self.assertRaises(lib_exc.Forbidden, self.client.delete_namespace,
+ self.assertRaises(lib_exc.Forbidden,
+ self.namespaces_client.delete_namespace,
namespace_name)
# update the visibility to private and protected to False
- body = self.client.update_namespace(namespace=namespace_name,
- description='Tempest',
- visibility='private',
- display_name=namespace_name,
- protected=False)
+ body = self.namespaces_client.update_namespace(
+ namespace=namespace_name,
+ description='Tempest',
+ visibility='private',
+ display_name=namespace_name,
+ protected=False)
self.assertEqual('private', body['visibility'])
self.assertEqual(False, body['protected'])
# now able to delete the non-protected namespace
- self.client.delete_namespace(namespace_name)
+ self.namespaces_client.delete_namespace(namespace_name)
def _cleanup_namespace(self, namespace_name):
- # this is used to cleanup the resources
- try:
- body = self.client.show_namespace(namespace_name)
- self.assertEqual(namespace_name, body['namespace'])
- body = self.client.update_namespace(namespace=namespace_name,
- description='Tempest',
- visibility='private',
- display_name=namespace_name,
- protected=False)
- self.client.delete_namespace(namespace_name)
- except lib_exc.NotFound:
- pass
+ body = self.namespaces_client.show_namespace(namespace_name)
+ self.assertEqual(namespace_name, body['namespace'])
+ body = self.namespaces_client.update_namespace(
+ namespace=namespace_name,
+ description='Tempest',
+ visibility='private',
+ display_name=namespace_name,
+ protected=False)
+ self.namespaces_client.delete_namespace(namespace_name)
diff --git a/tempest/api/image/v2/test_images_metadefs_resource_types.py b/tempest/api/image/v2/test_images_metadefs_resource_types.py
new file mode 100644
index 0000000..c60b3f7
--- /dev/null
+++ b/tempest/api/image/v2/test_images_metadefs_resource_types.py
@@ -0,0 +1,54 @@
+# Copyright 2016 Ericsson India Global Services Private Limited
+# 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.image import base
+from tempest.lib import decorators
+
+
+class MetadataResourceTypesTest(base.BaseV2ImageTest):
+ """Test the Metadata definition resource types basic functionality"""
+
+ @decorators.idempotent_id('6f358a4e-5ef0-11e6-a795-080027d0d606')
+ def test_basic_meta_def_resource_type_association(self):
+ # Get the available resource types and use one resource_type
+ body = self.resource_types_client.list_resource_types()
+ resource_name = body['resource_types'][0]['name']
+ # Create a namespace
+ namespace = self.create_namespace()
+ # Create resource type association
+ body = self.resource_types_client.create_resource_type_association(
+ namespace['namespace'], name=resource_name)
+ self.assertEqual(body['name'], resource_name)
+ # NOTE(raiesmh08): Here intentionally I have not added addcleanup
+ # method for resource type dissociation because its a metadata add and
+ # being cleaned as soon as namespace is cleaned at test case level.
+ # When namespace cleans, resource type association will automatically
+ # clean without any error or dependency.
+
+ # List resource type associations and validate creation
+ rs_type_associations = [
+ rs_type_association['name'] for rs_type_association in
+ self.resource_types_client.list_resource_type_association(
+ namespace['namespace'])['resource_type_associations']]
+ self.assertIn(resource_name, rs_type_associations)
+ # Delete resource type association
+ self.resource_types_client.delete_resource_type_association(
+ namespace['namespace'], resource_name)
+ # List resource type associations and validate deletion
+ rs_type_associations = [
+ rs_type_association['name'] for rs_type_association in
+ self.resource_types_client.list_resource_type_association(
+ namespace['namespace'])['resource_type_associations']]
+ self.assertNotIn(resource_name, rs_type_associations)
diff --git a/tempest/api/image/v2/test_images_metadefs_schema.py b/tempest/api/image/v2/test_images_metadefs_schema.py
new file mode 100644
index 0000000..95cc310
--- /dev/null
+++ b/tempest/api/image/v2/test_images_metadefs_schema.py
@@ -0,0 +1,81 @@
+# Copyright 2016 EasyStack.
+# 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.image import base
+from tempest.lib import decorators
+
+
+class MetadataSchemaTest(base.BaseV2ImageTest):
+ """Test to get metadata schema"""
+
+ @decorators.idempotent_id('e9e44891-3cb8-3b40-a532-e0a39fea3dab')
+ def test_get_metadata_namespace_schema(self):
+ # Test to get namespace schema
+ body = self.schemas_client.show_schema("metadefs/namespace")
+ self.assertEqual("namespace", body['name'])
+
+ @decorators.idempotent_id('ffe44891-678b-3ba0-a3e2-e0a3967b3aeb')
+ def test_get_metadata_namespaces_schema(self):
+ # Test to get namespaces schema
+ body = self.schemas_client.show_schema("metadefs/namespaces")
+ self.assertEqual("namespaces", body['name'])
+
+ @decorators.idempotent_id('fde34891-678b-3b40-ae32-e0a3e67b6beb')
+ def test_get_metadata_resource_type_schema(self):
+ # Test to get resource_type schema
+ body = self.schemas_client.show_schema("metadefs/resource_type")
+ self.assertEqual("resource_type_association", body['name'])
+
+ @decorators.idempotent_id('dfe4a891-b38b-3bf0-a3b2-e03ee67b3a3a')
+ def test_get_metadata_resources_types_schema(self):
+ # Test to get resource_types schema
+ body = self.schemas_client.show_schema("metadefs/resource_types")
+ self.assertEqual("resource_type_associations", body['name'])
+
+ @decorators.idempotent_id('dff4a891-b38b-3bf0-a3b2-e03ee67b3a3b')
+ def test_get_metadata_object_schema(self):
+ # Test to get object schema
+ body = self.schemas_client.show_schema("metadefs/object")
+ self.assertEqual("object", body['name'])
+
+ @decorators.idempotent_id('dee4a891-b38b-3bf0-a3b2-e03ee67b3a3c')
+ def test_get_metadata_objects_schema(self):
+ # Test to get objects schema
+ body = self.schemas_client.show_schema("metadefs/objects")
+ self.assertEqual("objects", body['name'])
+
+ @decorators.idempotent_id('dae4a891-b38b-3bf0-a3b2-e03ee67b3a3d')
+ def test_get_metadata_property_schema(self):
+ # Test to get property schema
+ body = self.schemas_client.show_schema("metadefs/property")
+ self.assertEqual("property", body['name'])
+
+ @decorators.idempotent_id('dce4a891-b38b-3bf0-a3b2-e03ee67b3a3e')
+ def test_get_metadata_properties_schema(self):
+ # Test to get properties schema
+ body = self.schemas_client.show_schema("metadefs/properties")
+ self.assertEqual("properties", body['name'])
+
+ @decorators.idempotent_id('dde4a891-b38b-3bf0-a3b2-e03ee67b3a3e')
+ def test_get_metadata_tag_schema(self):
+ # Test to get tag schema
+ body = self.schemas_client.show_schema("metadefs/tag")
+ self.assertEqual("tag", body['name'])
+
+ @decorators.idempotent_id('cde4a891-b38b-3bf0-a3b2-e03ee67b3a3a')
+ def test_get_metadata_tags_schema(self):
+ # Test to get tags schema
+ body = self.schemas_client.show_schema("metadefs/tags")
+ self.assertEqual("tags", body['name'])
diff --git a/tempest/api/image/v2/test_images_negative.py b/tempest/api/image/v2/test_images_negative.py
index 485942e..b4baf05 100644
--- a/tempest/api/image/v2/test_images_negative.py
+++ b/tempest/api/image/v2/test_images_negative.py
@@ -14,12 +14,10 @@
# License for the specific language governing permissions and limitations
# under the License.
-import uuid
-
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.image import base
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class ImagesNegativeTest(base.BaseV2ImageTest):
@@ -31,71 +29,84 @@
** get image with image_id=NULL
** get the deleted image
** delete non-existent image
- ** delete rimage with image_id=NULL
+ ** delete image with image_id=NULL
** delete the deleted image
"""
- @test.attr(type=['negative'])
- @test.idempotent_id('668743d5-08ad-4480-b2b8-15da34f81d9f')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('668743d5-08ad-4480-b2b8-15da34f81d9f')
def test_get_non_existent_image(self):
# get the non-existent image
- non_existent_id = str(uuid.uuid4())
+ non_existent_id = data_utils.rand_uuid()
self.assertRaises(lib_exc.NotFound, self.client.show_image,
non_existent_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('ef45000d-0a72-4781-866d-4cb7bf2562ad')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('ef45000d-0a72-4781-866d-4cb7bf2562ad')
def test_get_image_null_id(self):
# get image with image_id = NULL
image_id = ""
self.assertRaises(lib_exc.NotFound, self.client.show_image, image_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('e57fc127-7ba0-4693-92d7-1d8a05ebcba9')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('e57fc127-7ba0-4693-92d7-1d8a05ebcba9')
def test_get_delete_deleted_image(self):
# get and delete the deleted image
# create and delete image
- body = self.client.create_image(name='test',
- container_format='bare',
- disk_format='raw')
- image_id = body['id']
- self.client.delete_image(image_id)
- self.client.wait_for_resource_deletion(image_id)
+ image = self.client.create_image(name='test',
+ container_format='bare',
+ disk_format='raw')
+ self.client.delete_image(image['id'])
+ self.client.wait_for_resource_deletion(image['id'])
# get the deleted image
- self.assertRaises(lib_exc.NotFound, self.client.show_image, image_id)
+ self.assertRaises(lib_exc.NotFound,
+ self.client.show_image, image['id'])
# delete the deleted image
self.assertRaises(lib_exc.NotFound, self.client.delete_image,
- image_id)
+ image['id'])
- @test.attr(type=['negative'])
- @test.idempotent_id('6fe40f1c-57bd-4918-89cc-8500f850f3de')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('6fe40f1c-57bd-4918-89cc-8500f850f3de')
def test_delete_non_existing_image(self):
# delete non-existent image
- non_existent_image_id = str(uuid.uuid4())
+ non_existent_image_id = data_utils.rand_uuid()
self.assertRaises(lib_exc.NotFound, self.client.delete_image,
non_existent_image_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('32248db1-ab88-4821-9604-c7c369f1f88c')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('32248db1-ab88-4821-9604-c7c369f1f88c')
def test_delete_image_null_id(self):
# delete image with image_id=NULL
image_id = ""
self.assertRaises(lib_exc.NotFound, self.client.delete_image,
image_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('292bd310-369b-41c7-a7a3-10276ef76753')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('292bd310-369b-41c7-a7a3-10276ef76753')
def test_register_with_invalid_container_format(self):
# Negative tests for invalid data supplied to POST /images
self.assertRaises(lib_exc.BadRequest, self.client.create_image,
name='test', container_format='wrong',
disk_format='vhd')
- @test.attr(type=['negative'])
- @test.idempotent_id('70c6040c-5a97-4111-9e13-e73665264ce1')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('70c6040c-5a97-4111-9e13-e73665264ce1')
def test_register_with_invalid_disk_format(self):
self.assertRaises(lib_exc.BadRequest, self.client.create_image,
name='test', container_format='bare',
disk_format='wrong')
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('ab980a34-8410-40eb-872b-f264752f46e5')
+ def test_delete_protected_image(self):
+ # Create a protected image
+ image = self.create_image(protected=True)
+ self.addCleanup(self.client.update_image, image['id'],
+ [dict(replace="/protected", value=False)])
+
+ # Try deleting the protected image
+ self.assertRaises(lib_exc.Forbidden,
+ self.client.delete_image,
+ image['id'])
diff --git a/tempest/api/image/v2/test_images_tags.py b/tempest/api/image/v2/test_images_tags.py
index 42a4b87..601826e 100644
--- a/tempest/api/image/v2/test_images_tags.py
+++ b/tempest/api/image/v2/test_images_tags.py
@@ -13,27 +13,26 @@
# under the License.
from tempest.api.image import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
class ImagesTagsTest(base.BaseV2ImageTest):
- @test.idempotent_id('10407036-6059-4f95-a2cd-cbbbee7ed329')
+ @decorators.idempotent_id('10407036-6059-4f95-a2cd-cbbbee7ed329')
def test_update_delete_tags_for_image(self):
- body = self.create_image(container_format='bare',
- disk_format='raw',
- visibility='private')
- image_id = body['id']
+ image = self.create_image(container_format='bare',
+ disk_format='raw',
+ visibility='private')
tag = data_utils.rand_name('tag')
- self.addCleanup(self.client.delete_image, image_id)
+ self.addCleanup(self.client.delete_image, image['id'])
# Creating image tag and verify it.
- self.client.add_image_tag(image_id, tag)
- body = self.client.show_image(image_id)
+ self.client.add_image_tag(image['id'], tag)
+ body = self.client.show_image(image['id'])
self.assertIn(tag, body['tags'])
# Deleting image tag and verify it.
- self.client.delete_image_tag(image_id, tag)
- body = self.client.show_image(image_id)
+ self.client.delete_image_tag(image['id'], tag)
+ body = self.client.show_image(image['id'])
self.assertNotIn(tag, body['tags'])
diff --git a/tempest/api/image/v2/test_images_tags_negative.py b/tempest/api/image/v2/test_images_tags_negative.py
index a3f4ca8..440fa36 100644
--- a/tempest/api/image/v2/test_images_tags_negative.py
+++ b/tempest/api/image/v2/test_images_tags_negative.py
@@ -12,36 +12,32 @@
# License for the specific language governing permissions and limitations
# under the License.
-import uuid
-
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.image import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class ImagesTagsNegativeTest(base.BaseV2ImageTest):
- @test.attr(type=['negative'])
- @test.idempotent_id('8cd30f82-6f9a-4c6e-8034-c1b51fba43d9')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('8cd30f82-6f9a-4c6e-8034-c1b51fba43d9')
def test_update_tags_for_non_existing_image(self):
# Update tag with non existing image.
tag = data_utils.rand_name('tag')
- non_exist_image = str(uuid.uuid4())
+ non_exist_image = data_utils.rand_uuid()
self.assertRaises(lib_exc.NotFound, self.client.add_image_tag,
non_exist_image, tag)
- @test.attr(type=['negative'])
- @test.idempotent_id('39c023a2-325a-433a-9eea-649bf1414b19')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('39c023a2-325a-433a-9eea-649bf1414b19')
def test_delete_non_existing_tag(self):
# Delete non existing tag.
- body = self.create_image(container_format='bare',
- disk_format='raw',
- visibility='private'
- )
- image_id = body['id']
+ image = self.create_image(container_format='bare',
+ disk_format='raw',
+ visibility='private'
+ )
tag = data_utils.rand_name('non-exist-tag')
- self.addCleanup(self.client.delete_image, image_id)
+ self.addCleanup(self.client.delete_image, image['id'])
self.assertRaises(lib_exc.NotFound, self.client.delete_image_tag,
- image_id, tag)
+ image['id'], tag)
diff --git a/tempest/api/image/v2/test_versions.py b/tempest/api/image/v2/test_versions.py
new file mode 100644
index 0000000..84f1068
--- /dev/null
+++ b/tempest/api/image/v2/test_versions.py
@@ -0,0 +1,29 @@
+# Copyright 2017 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.image import base
+from tempest.lib import decorators
+
+
+class VersionsTest(base.BaseV2ImageTest):
+
+ @decorators.idempotent_id('659ea30a-a17c-4317-832c-0f68ed23c31d')
+ @decorators.attr(type='smoke')
+ def test_list_versions(self):
+ versions = self.versions_client.list_versions()['versions']
+ expected_resources = ('id', 'links', 'status')
+
+ for version in versions:
+ for res in expected_resources:
+ self.assertIn(res, version)
diff --git a/tempest/api/messaging/__init__.py b/tempest/api/messaging/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/api/messaging/__init__.py
+++ /dev/null
diff --git a/tempest/api/messaging/base.py b/tempest/api/messaging/base.py
deleted file mode 100644
index 528fbea..0000000
--- a/tempest/api/messaging/base.py
+++ /dev/null
@@ -1,165 +0,0 @@
-# Copyright (c) 2014 Rackspace, Inc.
-#
-# 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_log import log as logging
-
-from tempest.common.utils import data_utils
-from tempest import config
-from tempest import test
-
-CONF = config.CONF
-
-LOG = logging.getLogger(__name__)
-
-
-class BaseMessagingTest(test.BaseTestCase):
-
- """Base class for the Messaging (Zaqar) tests
-
- It is assumed that the following option is defined in the
- [service_available] section of etc/tempest.conf
-
- messaging as True
- """
-
- credentials = ['primary']
-
- @classmethod
- def skip_checks(cls):
- super(BaseMessagingTest, cls).skip_checks()
- if not CONF.service_available.zaqar:
- raise cls.skipException("Zaqar support is required")
-
- @classmethod
- def setup_clients(cls):
- super(BaseMessagingTest, cls).setup_clients()
- cls.client = cls.os.messaging_client
-
- @classmethod
- def resource_setup(cls):
- super(BaseMessagingTest, cls).resource_setup()
- cls.messaging_cfg = CONF.messaging
-
- @classmethod
- def create_queue(cls, queue_name):
- """Wrapper utility that returns a test queue."""
- resp, body = cls.client.create_queue(queue_name)
- return resp, body
-
- @classmethod
- def delete_queue(cls, queue_name):
- """Wrapper utility that deletes a test queue."""
- resp, body = cls.client.delete_queue(queue_name)
- return resp, body
-
- @classmethod
- def check_queue_exists(cls, queue_name):
- """Wrapper utility that checks the existence of a test queue."""
- resp, body = cls.client.show_queue(queue_name)
- return resp, body
-
- @classmethod
- def check_queue_exists_head(cls, queue_name):
- """Wrapper utility checks the head of a queue via http HEAD."""
- resp, body = cls.client.head_queue(queue_name)
- return resp, body
-
- @classmethod
- def list_queues(cls):
- """Wrapper utility that lists queues."""
- resp, body = cls.client.list_queues()
- return resp, body
-
- @classmethod
- def get_queue_stats(cls, queue_name):
- """Wrapper utility that returns the queue stats."""
- resp, body = cls.client.show_queue_stats(queue_name)
- return resp, body
-
- @classmethod
- def get_queue_metadata(cls, queue_name):
- """Wrapper utility that gets a queue metadata."""
- resp, body = cls.client.show_queue_metadata(queue_name)
- return resp, body
-
- @classmethod
- def set_queue_metadata(cls, queue_name, rbody):
- """Wrapper utility that sets the metadata of a queue."""
- resp, body = cls.client.set_queue_metadata(queue_name, rbody)
- return resp, body
-
- @classmethod
- def post_messages(cls, queue_name, rbody):
- """Wrapper utility that posts messages to a queue."""
- resp, body = cls.client.post_messages(queue_name, rbody)
-
- return resp, body
-
- @classmethod
- def list_messages(cls, queue_name):
- """Wrapper utility that lists the messages in a queue."""
- resp, body = cls.client.list_messages(queue_name)
-
- return resp, body
-
- @classmethod
- def delete_messages(cls, message_uri):
- """Wrapper utility that deletes messages."""
- resp, body = cls.client.delete_messages(message_uri)
-
- return resp, body
-
- @classmethod
- def post_claims(cls, queue_name, rbody, url_params=False):
- """Wrapper utility that claims messages."""
- resp, body = cls.client.post_claims(
- queue_name, rbody, url_params=False)
-
- return resp, body
-
- @classmethod
- def query_claim(cls, claim_uri):
- """Wrapper utility that gets a claim."""
- resp, body = cls.client.query_claim(claim_uri)
-
- return resp, body
-
- @classmethod
- def update_claim(cls, claim_uri, rbody):
- """Wrapper utility that updates a claim."""
- resp, body = cls.client.update_claim(claim_uri, rbody)
-
- return resp, body
-
- @classmethod
- def release_claim(cls, claim_uri):
- """Wrapper utility that deletes a claim."""
- resp, body = cls.client.delete_claim(claim_uri)
-
- return resp, body
-
- @classmethod
- def generate_message_body(cls, repeat=1):
- """Wrapper utility that sets the metadata of a queue."""
- message_ttl = data_utils.\
- rand_int_id(start=60, end=CONF.messaging.max_message_ttl)
-
- key = data_utils.arbitrary_string(size=20, base_text='MessagingKey')
- value = data_utils.arbitrary_string(size=20,
- base_text='MessagingValue')
- message_body = {key: value}
-
- rbody = ([{'body': message_body, 'ttl': message_ttl}] * repeat)
- return rbody
diff --git a/tempest/api/messaging/test_claims.py b/tempest/api/messaging/test_claims.py
deleted file mode 100644
index 57b8c7f..0000000
--- a/tempest/api/messaging/test_claims.py
+++ /dev/null
@@ -1,128 +0,0 @@
-# Copyright (c) 2014 Rackspace, Inc.
-#
-# 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 logging
-
-from six.moves.urllib import parse as urlparse
-from tempest_lib import decorators
-
-from tempest.api.messaging import base
-from tempest.common.utils import data_utils
-from tempest import config
-from tempest import test
-
-
-LOG = logging.getLogger(__name__)
-CONF = config.CONF
-
-
-class TestClaims(base.BaseMessagingTest):
-
- @classmethod
- def resource_setup(cls):
- super(TestClaims, cls).resource_setup()
- cls.queue_name = data_utils.rand_name('Queues-Test')
- # Create Queue
- cls.create_queue(cls.queue_name)
-
- def _post_and_claim_messages(self, queue_name, repeat=1):
- # Post Messages
- message_body = self.generate_message_body(repeat=repeat)
- self.client.post_messages(queue_name=self.queue_name,
- rbody=message_body)
-
- # Post Claim
- claim_ttl = data_utils.rand_int_id(start=60,
- end=CONF.messaging.max_claim_ttl)
- claim_grace = data_utils.\
- rand_int_id(start=60, end=CONF.messaging.max_claim_grace)
- claim_body = {"ttl": claim_ttl, "grace": claim_grace}
- resp, body = self.client.post_claims(queue_name=self.queue_name,
- rbody=claim_body)
-
- return resp, body
-
- @test.attr(type='smoke')
- @test.idempotent_id('936cb1ca-b7af-44dd-a752-805e8c98156f')
- def test_post_claim(self):
- _, body = self._post_and_claim_messages(queue_name=self.queue_name)
- claimed_message_uri = body[0]['href']
-
- # Skipping this step till bug-1331517 is fixed
- # Get posted claim
- # self.client.query_claim(claimed_message_uri)
-
- # Delete Claimed message
- self.client.delete_messages(claimed_message_uri)
-
- @decorators.skip_because(bug="1331517")
- @test.attr(type='smoke')
- @test.idempotent_id('84e491f4-68c6-451f-9846-b8f868eb27c5')
- def test_query_claim(self):
- # Post a Claim
- resp, body = self._post_and_claim_messages(queue_name=self.queue_name)
-
- # Query Claim
- claim_uri = resp['location']
- self.client.query_claim(claim_uri)
-
- # Delete Claimed message
- claimed_message_uri = body[0]['href']
- self.delete_messages(claimed_message_uri)
-
- @decorators.skip_because(bug="1328111")
- @test.attr(type='smoke')
- @test.idempotent_id('420ef0c5-9bd6-4b82-b06d-d9da330fefd3')
- def test_update_claim(self):
- # Post a Claim
- resp, body = self._post_and_claim_messages(queue_name=self.queue_name)
-
- claim_uri = resp['location']
- claimed_message_uri = body[0]['href']
-
- # Update Claim
- claim_ttl = data_utils.rand_int_id(start=60,
- end=CONF.messaging.max_claim_ttl)
- update_rbody = {"ttl": claim_ttl}
-
- self.client.update_claim(claim_uri, rbody=update_rbody)
-
- # Verify claim ttl >= updated ttl value
- _, body = self.client.query_claim(claim_uri)
- updated_claim_ttl = body["ttl"]
- self.assertTrue(updated_claim_ttl >= claim_ttl)
-
- # Delete Claimed message
- self.client.delete_messages(claimed_message_uri)
-
- @test.attr(type='smoke')
- @test.idempotent_id('fd4c7921-cb3f-4ed8-9ac8-e8f1e74c44aa')
- def test_release_claim(self):
- # Post a Claim
- resp, body = self._post_and_claim_messages(queue_name=self.queue_name)
- claim_uri = resp['location']
-
- # Release Claim
- self.client.delete_claim(claim_uri)
-
- # Delete Claimed message
- # This will implicitly verify that the claim is deleted.
- message_uri = urlparse.urlparse(claim_uri).path
- self.client.delete_messages(message_uri)
-
- @classmethod
- def resource_cleanup(cls):
- cls.delete_queue(cls.queue_name)
- super(TestClaims, cls).resource_cleanup()
diff --git a/tempest/api/messaging/test_messages.py b/tempest/api/messaging/test_messages.py
deleted file mode 100644
index efbbf56..0000000
--- a/tempest/api/messaging/test_messages.py
+++ /dev/null
@@ -1,127 +0,0 @@
-# Copyright (c) 2014 Rackspace, Inc.
-#
-# 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 logging
-
-from tempest.api.messaging import base
-from tempest.common.utils import data_utils
-from tempest import config
-from tempest import test
-
-
-LOG = logging.getLogger(__name__)
-CONF = config.CONF
-
-
-class TestMessages(base.BaseMessagingTest):
-
- @classmethod
- def resource_setup(cls):
- super(TestMessages, cls).resource_setup()
- cls.queue_name = data_utils.rand_name('Queues-Test')
- # Create Queue
- cls.client.create_queue(cls.queue_name)
-
- def _post_messages(self, repeat=CONF.messaging.max_messages_per_page):
- message_body = self.generate_message_body(repeat=repeat)
- resp, body = self.post_messages(queue_name=self.queue_name,
- rbody=message_body)
- return resp, body
-
- @test.attr(type='smoke')
- @test.idempotent_id('93867172-a414-4eb3-a639-96e943c516b4')
- def test_post_messages(self):
- # Post Messages
- resp, _ = self._post_messages()
-
- # Get on the posted messages
- message_uri = resp['location']
- resp, _ = self.client.show_multiple_messages(message_uri)
- # The test has an assertion here, because the response cannot be 204
- # in this case (the client allows 200 or 204 for this API call).
- self.assertEqual('200', resp['status'])
-
- @test.attr(type='smoke')
- @test.idempotent_id('c967d59a-e919-41cb-994b-1c4300452c80')
- def test_list_messages(self):
- # Post Messages
- self._post_messages()
-
- # List Messages
- resp, _ = self.list_messages(queue_name=self.queue_name)
- # The test has an assertion here, because the response cannot be 204
- # in this case (the client allows 200 or 204 for this API call).
- self.assertEqual('200', resp['status'])
-
- @test.attr(type='smoke')
- @test.idempotent_id('2a68e3de-24df-47c3-9039-ec4156656bf8')
- def test_get_message(self):
- # Post Messages
- _, body = self._post_messages()
- message_uri = body['resources'][0]
-
- # Get posted message
- resp, _ = self.client.show_single_message(message_uri)
- # The test has an assertion here, because the response cannot be 204
- # in this case (the client allows 200 or 204 for this API call).
- self.assertEqual('200', resp['status'])
-
- @test.attr(type='smoke')
- @test.idempotent_id('c4b0a30b-efda-4b87-a395-0c43140df74d')
- def test_get_multiple_messages(self):
- # Post Messages
- resp, _ = self._post_messages()
- message_uri = resp['location']
-
- # Get posted messages
- resp, _ = self.client.show_multiple_messages(message_uri)
- # The test has an assertion here, because the response cannot be 204
- # in this case (the client allows 200 or 204 for this API call).
- self.assertEqual('200', resp['status'])
-
- @test.attr(type='smoke')
- @test.idempotent_id('fc0fca47-dd8b-4ecc-8522-d9c191f9bc9f')
- def test_delete_single_message(self):
- # Post Messages
- _, body = self._post_messages()
- message_uri = body['resources'][0]
-
- # Delete posted message & verify the delete operration
- self.client.delete_messages(message_uri)
-
- message_uri = message_uri.replace('/messages/', '/messages?ids=')
- resp, _ = self.client.show_multiple_messages(message_uri)
- # The test has an assertion here, because the response has to be 204
- # in this case (the client allows 200 or 204 for this API call).
- self.assertEqual('204', resp['status'])
-
- @test.attr(type='smoke')
- @test.idempotent_id('00cca069-5c8f-4b42-bff1-c577da2a4546')
- def test_delete_multiple_messages(self):
- # Post Messages
- resp, _ = self._post_messages()
- message_uri = resp['location']
-
- # Delete multiple messages
- self.client.delete_messages(message_uri)
- resp, _ = self.client.show_multiple_messages(message_uri)
- # The test has an assertion here, because the response has to be 204
- # in this case (the client allows 200 or 204 for this API call).
- self.assertEqual('204', resp['status'])
-
- @classmethod
- def resource_cleanup(cls):
- cls.delete_queue(cls.queue_name)
- super(TestMessages, cls).resource_cleanup()
diff --git a/tempest/api/messaging/test_queues.py b/tempest/api/messaging/test_queues.py
deleted file mode 100644
index df49663..0000000
--- a/tempest/api/messaging/test_queues.py
+++ /dev/null
@@ -1,128 +0,0 @@
-# Copyright (c) 2014 Rackspace, Inc.
-#
-# 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 logging
-
-from six import moves
-from tempest_lib import exceptions as lib_exc
-from testtools import matchers
-
-from tempest.api.messaging import base
-from tempest.common.utils import data_utils
-from tempest import test
-
-
-LOG = logging.getLogger(__name__)
-
-
-class TestQueues(base.BaseMessagingTest):
-
- @test.attr(type='smoke')
- @test.idempotent_id('9f1c4c72-80c5-4dac-acf3-188cef42e36c')
- def test_create_delete_queue(self):
- # Create & Delete Queue
- queue_name = data_utils.rand_name('test')
- _, body = self.create_queue(queue_name)
-
- self.addCleanup(self.client.delete_queue, queue_name)
- # NOTE(gmann): create_queue returns response status code as 201
- # so specifically checking the expected empty response body as
- # this is not going to be checked in response_checker().
- self.assertEqual('', body)
-
- self.delete_queue(queue_name)
- self.assertRaises(lib_exc.NotFound,
- self.client.show_queue,
- queue_name)
-
-
-class TestManageQueue(base.BaseMessagingTest):
-
- @classmethod
- def resource_setup(cls):
- super(TestManageQueue, cls).resource_setup()
- cls.queues = list()
- for _ in moves.xrange(5):
- queue_name = data_utils.rand_name('Queues-Test')
- cls.queues.append(queue_name)
- # Create Queue
- cls.client.create_queue(queue_name)
-
- @test.attr(type='smoke')
- @test.idempotent_id('ccd3d69e-f156-4c5f-8a12-b4f24bee44e1')
- def test_check_queue_existence(self):
- # Checking Queue Existence
- for queue_name in self.queues:
- self.check_queue_exists(queue_name)
-
- @test.attr(type='smoke')
- @test.idempotent_id('e27634d8-9c8f-47d8-a677-655c47658d3e')
- def test_check_queue_head(self):
- # Checking Queue Existence by calling HEAD
- for queue_name in self.queues:
- self.check_queue_exists_head(queue_name)
-
- @test.attr(type='smoke')
- @test.idempotent_id('0a0feeca-7768-4303-806d-82bbbb796ad3')
- def test_list_queues(self):
- # Listing queues
- _, body = self.list_queues()
- self.assertEqual(len(body['queues']), len(self.queues))
- for item in body['queues']:
- self.assertIn(item['name'], self.queues)
-
- @test.attr(type='smoke')
- @test.idempotent_id('8fb66602-077d-49d6-ae1a-5f2091739178')
- def test_get_queue_stats(self):
- # Retrieve random queue
- queue_name = self.queues[data_utils.rand_int_id(0,
- len(self.queues) - 1)]
- # Get Queue Stats for a newly created Queue
- _, body = self.get_queue_stats(queue_name)
- msgs = body['messages']
- for element in ('free', 'claimed', 'total'):
- self.assertEqual(0, msgs[element])
- for element in ('oldest', 'newest'):
- self.assertNotIn(element, msgs)
-
- @test.attr(type='smoke')
- @test.idempotent_id('0e2441e6-6593-4bdb-a3c0-20e66eeb3fff')
- def test_set_and_get_queue_metadata(self):
- # Retrieve random queue
- queue_name = self.queues[data_utils.rand_int_id(0,
- len(self.queues) - 1)]
- # Check the Queue has no metadata
- _, body = self.get_queue_metadata(queue_name)
- self.assertThat(body, matchers.HasLength(0))
- # Create metadata
- key3 = [0, 1, 2, 3, 4]
- key2 = data_utils.rand_name('value')
- req_body1 = dict()
- req_body1[data_utils.rand_name('key3')] = key3
- req_body1[data_utils.rand_name('key2')] = key2
- req_body = dict()
- req_body[data_utils.rand_name('key1')] = req_body1
- # Set Queue Metadata
- self.set_queue_metadata(queue_name, req_body)
-
- # Get Queue Metadata
- _, body = self.get_queue_metadata(queue_name)
- self.assertThat(body, matchers.Equals(req_body))
-
- @classmethod
- def resource_cleanup(cls):
- for queue_name in cls.queues:
- cls.client.delete_queue(queue_name)
- super(TestManageQueue, cls).resource_cleanup()
diff --git a/tempest/api/network/admin/test_agent_management.py b/tempest/api/network/admin/test_agent_management.py
index 61f8e15..5068fc4 100644
--- a/tempest/api/network/admin/test_agent_management.py
+++ b/tempest/api/network/admin/test_agent_management.py
@@ -14,7 +14,8 @@
from tempest.api.network import base
from tempest.common import tempest_fixtures as fixtures
-from tempest import test
+from tempest.common import utils
+from tempest.lib import decorators
class AgentManagementTestJSON(base.BaseAdminNetworkTest):
@@ -22,7 +23,7 @@
@classmethod
def skip_checks(cls):
super(AgentManagementTestJSON, cls).skip_checks()
- if not test.is_extension_enabled('agent', 'network'):
+ if not utils.is_extension_enabled('agent', 'network'):
msg = "agent extension not enabled."
raise cls.skipException(msg)
@@ -33,7 +34,7 @@
agents = body['agents']
cls.agent = agents[0]
- @test.idempotent_id('9c80f04d-11f3-44a4-8738-ed2f879b0ff4')
+ @decorators.idempotent_id('9c80f04d-11f3-44a4-8738-ed2f879b0ff4')
def test_list_agent(self):
body = self.admin_agents_client.list_agents()
agents = body['agents']
@@ -45,18 +46,18 @@
agent.pop('configurations', None)
self.assertIn(self.agent, agents)
- @test.idempotent_id('e335be47-b9a1-46fd-be30-0874c0b751e6')
+ @decorators.idempotent_id('e335be47-b9a1-46fd-be30-0874c0b751e6')
def test_list_agents_non_admin(self):
body = self.agents_client.list_agents()
- self.assertEqual(len(body["agents"]), 0)
+ self.assertEmpty(body["agents"])
- @test.idempotent_id('869bc8e8-0fda-4a30-9b71-f8a7cf58ca9f')
+ @decorators.idempotent_id('869bc8e8-0fda-4a30-9b71-f8a7cf58ca9f')
def test_show_agent(self):
body = self.admin_agents_client.show_agent(self.agent['id'])
agent = body['agent']
self.assertEqual(agent['id'], self.agent['id'])
- @test.idempotent_id('371dfc5b-55b9-4cb5-ac82-c40eadaac941')
+ @decorators.idempotent_id('371dfc5b-55b9-4cb5-ac82-c40eadaac941')
def test_update_agent_status(self):
origin_status = self.agent['admin_state_up']
# Try to update the 'admin_state_up' to the original
@@ -67,7 +68,7 @@
updated_status = body['agent']['admin_state_up']
self.assertEqual(origin_status, updated_status)
- @test.idempotent_id('68a94a14-1243-46e6-83bf-157627e31556')
+ @decorators.idempotent_id('68a94a14-1243-46e6-83bf-157627e31556')
def test_update_agent_description(self):
self.useFixture(fixtures.LockFixture('agent_description'))
description = 'description for update agent.'
diff --git a/tempest/api/network/admin/test_dhcp_agent_scheduler.py b/tempest/api/network/admin/test_dhcp_agent_scheduler.py
index 5ff465b..8315c5d 100644
--- a/tempest/api/network/admin/test_dhcp_agent_scheduler.py
+++ b/tempest/api/network/admin/test_dhcp_agent_scheduler.py
@@ -13,7 +13,8 @@
# under the License.
from tempest.api.network import base
-from tempest import test
+from tempest.common import utils
+from tempest.lib import decorators
class DHCPAgentSchedulersTestJSON(base.BaseAdminNetworkTest):
@@ -21,7 +22,7 @@
@classmethod
def skip_checks(cls):
super(DHCPAgentSchedulersTestJSON, cls).skip_checks()
- if not test.is_extension_enabled('dhcp_agent_scheduler', 'network'):
+ if not utils.is_extension_enabled('dhcp_agent_scheduler', 'network'):
msg = "dhcp_agent_scheduler extension not enabled."
raise cls.skipException(msg)
@@ -31,20 +32,20 @@
# Create a network and make sure it will be hosted by a
# dhcp agent: this is done by creating a regular port
cls.network = cls.create_network()
- cls.subnet = cls.create_subnet(cls.network)
+ cls.create_subnet(cls.network)
cls.port = cls.create_port(cls.network)
- @test.idempotent_id('5032b1fe-eb42-4a64-8f3b-6e189d8b5c7d')
+ @decorators.idempotent_id('5032b1fe-eb42-4a64-8f3b-6e189d8b5c7d')
def test_list_dhcp_agent_hosting_network(self):
- self.admin_client.list_dhcp_agent_hosting_network(
+ self.admin_networks_client.list_dhcp_agents_on_hosting_network(
self.network['id'])
- @test.idempotent_id('30c48f98-e45d-4ffb-841c-b8aad57c7587')
+ @decorators.idempotent_id('30c48f98-e45d-4ffb-841c-b8aad57c7587')
def test_list_networks_hosted_by_one_dhcp(self):
- body = self.admin_client.list_dhcp_agent_hosting_network(
+ body = self.admin_networks_client.list_dhcp_agents_on_hosting_network(
self.network['id'])
agents = body['agents']
- self.assertIsNotNone(agents)
+ self.assertNotEmpty(agents, "no dhcp agent")
agent = agents[0]
self.assertTrue(self._check_network_in_dhcp_agent(
self.network['id'], agent))
@@ -58,7 +59,7 @@
network_ids.append(network['id'])
return network_id in network_ids
- @test.idempotent_id('a0856713-6549-470c-a656-e97c8df9a14d')
+ @decorators.idempotent_id('a0856713-6549-470c-a656-e97c8df9a14d')
def test_add_remove_network_from_dhcp_agent(self):
# The agent is now bound to the network, we can free the port
self.ports_client.delete_port(self.port['id'])
@@ -84,7 +85,7 @@
self._remove_network_from_dhcp_agent(network_id, agent)
def _remove_network_from_dhcp_agent(self, network_id, agent):
- self.admin_agents_client.remove_network_from_dhcp_agent(
+ self.admin_agents_client.delete_network_from_dhcp_agent(
agent_id=agent['id'],
network_id=network_id)
self.assertFalse(self._check_network_in_dhcp_agent(
diff --git a/tempest/api/network/admin/test_external_network_extension.py b/tempest/api/network/admin/test_external_network_extension.py
index a32bfbc..49a9cdb 100644
--- a/tempest/api/network/admin/test_external_network_extension.py
+++ b/tempest/api/network/admin/test_external_network_extension.py
@@ -10,9 +10,15 @@
# License for the specific language governing permissions and limitations
# under the License.
+import testtools
+
from tempest.api.network import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+
+CONF = config.CONF
class ExternalNetworksTestJSON(base.BaseAdminNetworkTest):
@@ -32,7 +38,7 @@
self.admin_networks_client.delete_network, network['id'])
return network
- @test.idempotent_id('462be770-b310-4df9-9c42-773217e4c8b1')
+ @decorators.idempotent_id('462be770-b310-4df9-9c42-773217e4c8b1')
def test_create_external_network(self):
# Create a network as an admin user specifying the
# external network extension attribute
@@ -41,7 +47,7 @@
self.assertIsNotNone(ext_network['id'])
self.assertTrue(ext_network['router:external'])
- @test.idempotent_id('4db5417a-e11c-474d-a361-af00ebef57c5')
+ @decorators.idempotent_id('4db5417a-e11c-474d-a361-af00ebef57c5')
def test_update_external_network(self):
# Update a network as an admin user specifying the
# external network extension attribute
@@ -54,7 +60,7 @@
# Verify that router:external parameter was updated
self.assertTrue(updated_network['router:external'])
- @test.idempotent_id('39be4c9b-a57e-4ff9-b7c7-b218e209dfcc')
+ @decorators.idempotent_id('39be4c9b-a57e-4ff9-b7c7-b218e209dfcc')
def test_list_external_networks(self):
# Create external_net
external_network = self._create_network()
@@ -71,7 +77,7 @@
elif net['id'] == external_network['id']:
self.assertTrue(net['router:external'])
- @test.idempotent_id('2ac50ab2-7ebd-4e27-b3ce-a9e399faaea2')
+ @decorators.idempotent_id('2ac50ab2-7ebd-4e27-b3ce-a9e399faaea2')
def test_show_external_networks_attribute(self):
# Create external_net
external_network = self._create_network()
@@ -89,7 +95,9 @@
self.assertEqual(self.network['id'], show_net['id'])
self.assertFalse(show_net['router:external'])
- @test.idempotent_id('82068503-2cf2-4ed4-b3be-ecb89432e4bb')
+ @decorators.idempotent_id('82068503-2cf2-4ed4-b3be-ecb89432e4bb')
+ @testtools.skipUnless(CONF.network_feature_enabled.floating_ips,
+ 'Floating ips are not availabled')
def test_delete_external_networks_with_floating_ip(self):
# Verifies external network can be deleted while still holding
# (unassociated) floating IPs
@@ -97,7 +105,7 @@
body = self.admin_networks_client.create_network(
**{'router:external': True})
external_network = body['network']
- self.addCleanup(self._try_delete_resource,
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
self.admin_networks_client.delete_network,
external_network['id'])
subnet = self.create_subnet(
@@ -106,7 +114,7 @@
body = self.admin_floating_ips_client.create_floatingip(
floating_network_id=external_network['id'])
created_floating_ip = body['floatingip']
- self.addCleanup(self._try_delete_resource,
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
self.admin_floating_ips_client.delete_floatingip,
created_floating_ip['id'])
floatingip_list = self.admin_floating_ips_client.list_floatingips(
@@ -122,5 +130,3 @@
subnet_list = self.admin_subnets_client.list_subnets()
self.assertNotIn(subnet['id'],
(s['id'] for s in subnet_list))
- # Removes subnet from the cleanup list
- self.subnets.remove(subnet)
diff --git a/tempest/api/network/admin/test_external_networks_negative.py b/tempest/api/network/admin/test_external_networks_negative.py
index d031108..0709d2a 100644
--- a/tempest/api/network/admin/test_external_networks_negative.py
+++ b/tempest/api/network/admin/test_external_networks_negative.py
@@ -12,20 +12,23 @@
# 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 import exceptions as lib_exc
+import testtools
from tempest.api.network import base
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
CONF = config.CONF
class ExternalNetworksAdminNegativeTestJSON(base.BaseAdminNetworkTest):
- @test.attr(type=['negative'])
- @test.idempotent_id('d402ae6c-0be0-4d8e-833b-a738895d98d0')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('d402ae6c-0be0-4d8e-833b-a738895d98d0')
+ @testtools.skipUnless(CONF.network.public_network_id,
+ 'The public_network_id option must be specified.')
def test_create_port_with_precreated_floatingip_as_fixed_ip(self):
# NOTE: External networks can be used to create both floating-ip as
# well as instance-ip. So, creating an instance-ip with a value of a
@@ -35,7 +38,7 @@
body = self.admin_floating_ips_client.create_floatingip(
floating_network_id=CONF.network.public_network_id)
created_floating_ip = body['floatingip']
- self.addCleanup(self._try_delete_resource,
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
self.admin_floating_ips_client.delete_floatingip,
created_floating_ip['id'])
floating_ip_address = created_floating_ip['floating_ip_address']
diff --git a/tempest/api/network/admin/test_floating_ips_admin_actions.py b/tempest/api/network/admin/test_floating_ips_admin_actions.py
index 6ad374b..be0c4c6 100644
--- a/tempest/api/network/admin/test_floating_ips_admin_actions.py
+++ b/tempest/api/network/admin/test_floating_ips_admin_actions.py
@@ -14,22 +14,32 @@
# under the License.
from tempest.api.network import base
-from tempest.common.utils import data_utils
+from tempest.common import utils
from tempest import config
-from tempest import test
+from tempest.lib import decorators
CONF = config.CONF
class FloatingIPAdminTestJSON(base.BaseAdminNetworkTest):
- force_tenant_isolation = True
credentials = ['primary', 'alt', 'admin']
@classmethod
+ def skip_checks(cls):
+ super(FloatingIPAdminTestJSON, cls).skip_checks()
+ if not utils.is_extension_enabled('router', 'network'):
+ msg = "router extension not enabled."
+ raise cls.skipException(msg)
+ if not CONF.network.public_network_id:
+ msg = "The public_network_id option must be specified."
+ raise cls.skipException(msg)
+ if not CONF.network_feature_enabled.floating_ips:
+ raise cls.skipException("Floating ips are not available")
+
+ @classmethod
def setup_clients(cls):
super(FloatingIPAdminTestJSON, cls).setup_clients()
- cls.alt_client = cls.alt_manager.network_client
- cls.alt_floating_ips_client = cls.alt_manager.floating_ips_client
+ cls.alt_floating_ips_client = cls.os_alt.floating_ips_client
@classmethod
def resource_setup(cls):
@@ -37,13 +47,12 @@
cls.ext_net_id = CONF.network.public_network_id
cls.floating_ip = cls.create_floatingip(cls.ext_net_id)
cls.network = cls.create_network()
- cls.subnet = cls.create_subnet(cls.network)
- cls.router = cls.create_router(data_utils.rand_name('router-'),
- external_network_id=cls.ext_net_id)
- cls.create_router_interface(cls.router['id'], cls.subnet['id'])
+ subnet = cls.create_subnet(cls.network)
+ router = cls.create_router(external_network_id=cls.ext_net_id)
+ cls.create_router_interface(router['id'], subnet['id'])
cls.port = cls.create_port(cls.network)
- @test.idempotent_id('64f2100b-5471-4ded-b46c-ddeeeb4f231b')
+ @decorators.idempotent_id('64f2100b-5471-4ded-b46c-ddeeeb4f231b')
def test_list_floating_ips_from_admin_and_nonadmin(self):
# Create floating ip from admin user
floating_ip_admin = self.admin_floating_ips_client.create_floatingip(
@@ -68,13 +77,13 @@
body = self.floating_ips_client.list_floatingips()
floating_ip_ids = [f['id'] for f in body['floatingips']]
# Check that nonadmin user doesn't see floating ip created from admin
- # and floating ip that is created in another tenant (alt user)
+ # and floating ip that is created in another project (alt user)
self.assertIn(self.floating_ip['id'], floating_ip_ids)
self.assertNotIn(floating_ip_admin['floatingip']['id'],
floating_ip_ids)
self.assertNotIn(floating_ip_alt['id'], floating_ip_ids)
- @test.idempotent_id('32727cc3-abe2-4485-a16e-48f2d54c14f2')
+ @decorators.idempotent_id('32727cc3-abe2-4485-a16e-48f2d54c14f2')
def test_create_list_show_floating_ip_with_tenant_id_by_admin(self):
# Creates a floating IP
body = self.admin_floating_ips_client.create_floatingip(
diff --git a/tempest/api/network/admin/test_l3_agent_scheduler.py b/tempest/api/network/admin/test_l3_agent_scheduler.py
index 2e8fd50..206d867 100644
--- a/tempest/api/network/admin/test_l3_agent_scheduler.py
+++ b/tempest/api/network/admin/test_l3_agent_scheduler.py
@@ -13,10 +13,12 @@
# under the License.
from tempest.api.network import base
-from tempest.common.utils import data_utils
-from tempest import exceptions
-from tempest import test
+from tempest.common import utils
+from tempest import config
+from tempest.lib import decorators
+from tempest.lib import exceptions
+CONF = config.CONF
AGENT_TYPE = 'L3 agent'
AGENT_MODES = (
'legacy',
@@ -25,12 +27,7 @@
class L3AgentSchedulerTestJSON(base.BaseAdminNetworkTest):
- _agent_mode = 'legacy'
- is_dvr_router = False
-
- """
- Tests the following operations in the Neutron API using the REST client for
- Neutron:
+ """Tests the following operations in the Neutron API:
List routers that the given L3 agent is hosting.
List L3 agents hosting the given router.
@@ -44,68 +41,44 @@
@classmethod
def skip_checks(cls):
super(L3AgentSchedulerTestJSON, cls).skip_checks()
- if not test.is_extension_enabled('l3_agent_scheduler', 'network'):
+ if not utils.is_extension_enabled('l3_agent_scheduler', 'network'):
msg = "L3 Agent Scheduler Extension not enabled."
raise cls.skipException(msg)
@classmethod
def resource_setup(cls):
super(L3AgentSchedulerTestJSON, cls).resource_setup()
- body = cls.admin_agents_client.list_agents()
- agents = body['agents']
+ agents = cls.admin_agents_client.list_agents(
+ agent_type=AGENT_TYPE)['agents']
for agent in agents:
- # TODO(armax): falling back on default _agent_mode can be
- # dropped as soon as Icehouse is dropped.
- agent_mode = (
- agent['configurations'].get('agent_mode', cls._agent_mode))
- if agent['agent_type'] == AGENT_TYPE and agent_mode in AGENT_MODES:
+ if (agent['configurations']['agent_mode'] in AGENT_MODES and
+ agent['alive']):
cls.agent = agent
break
else:
msg = "L3 Agent Scheduler enabled in conf, but L3 Agent not found"
raise exceptions.InvalidConfiguration(msg)
- cls.router = cls.create_router(data_utils.rand_name('router'))
- # NOTE(armax): If DVR is an available extension, and the created router
- # is indeed a distributed one, more resources need to be provisioned
- # in order to bind the router to the L3 agent.
- # That said, let's preserve the existing test logic, where the extra
- # query and setup steps are only required if the extension is available
- # and only if the router's default type is distributed.
- if test.is_extension_enabled('dvr', 'network'):
- cls.is_dvr_router = cls.admin_client.show_router(
- cls.router['id'])['router'].get('distributed', False)
- if cls.is_dvr_router:
- cls.network = cls.create_network()
- cls.subnet = cls.create_subnet(cls.network)
- cls.port = cls.create_port(cls.network)
- cls.client.add_router_interface_with_port_id(
- cls.router['id'], cls.port['id'])
+ cls.router = cls.create_router()
- @classmethod
- def resource_cleanup(cls):
- if cls.is_dvr_router:
- cls.client.remove_router_interface_with_port_id(
- cls.router['id'], cls.port['id'])
- super(L3AgentSchedulerTestJSON, cls).resource_cleanup()
-
- @test.idempotent_id('b7ce6e89-e837-4ded-9b78-9ed3c9c6a45a')
+ @decorators.idempotent_id('b7ce6e89-e837-4ded-9b78-9ed3c9c6a45a')
def test_list_routers_on_l3_agent(self):
self.admin_agents_client.list_routers_on_l3_agent(self.agent['id'])
- @test.idempotent_id('9464e5e7-8625-49c3-8fd1-89c52be59d66')
+ @decorators.idempotent_id('9464e5e7-8625-49c3-8fd1-89c52be59d66')
def test_add_list_remove_router_on_l3_agent(self):
l3_agent_ids = list()
- self.admin_agents_client.add_router_to_l3_agent(
+ self.admin_agents_client.create_router_on_l3_agent(
self.agent['id'],
router_id=self.router['id'])
body = (
- self.admin_client.list_l3_agents_hosting_router(self.router['id']))
+ self.admin_routers_client.list_l3_agents_hosting_router(
+ self.router['id']))
for agent in body['agents']:
l3_agent_ids.append(agent['id'])
self.assertIn('agent_type', agent)
self.assertEqual('L3 agent', agent['agent_type'])
self.assertIn(self.agent['id'], l3_agent_ids)
- body = self.admin_agents_client.remove_router_from_l3_agent(
+ body = self.admin_agents_client.delete_router_from_l3_agent(
self.agent['id'],
self.router['id'])
# NOTE(afazekas): The deletion not asserted, because neutron
diff --git a/tempest/api/network/admin/test_metering_extensions.py b/tempest/api/network/admin/test_metering_extensions.py
new file mode 100644
index 0000000..5063fef
--- /dev/null
+++ b/tempest/api/network/admin/test_metering_extensions.py
@@ -0,0 +1,174 @@
+# Copyright (C) 2014 eNovance SAS <licensing@enovance.com>
+#
+# 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.network import base
+from tempest.common import utils
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+
+
+class MeteringTestJSON(base.BaseAdminNetworkTest):
+ """Tests the following operations in the Neutron API:
+
+ List, Show, Create, Delete Metering labels
+ List, Show, Create, Delete Metering labels rules
+ """
+
+ @classmethod
+ def skip_checks(cls):
+ super(MeteringTestJSON, cls).skip_checks()
+ if not utils.is_extension_enabled('metering', 'network'):
+ msg = "metering extension not enabled."
+ raise cls.skipException(msg)
+
+ @classmethod
+ def resource_setup(cls):
+ super(MeteringTestJSON, cls).resource_setup()
+ description = "metering label created by tempest"
+ name = data_utils.rand_name("metering-label")
+ cls.metering_label = cls.create_metering_label(name, description)
+ remote_ip_prefix = ("10.0.0.0/24" if cls._ip_version == 4
+ else "fd02::/64")
+ direction = "ingress"
+ cls.metering_label_rule = cls.create_metering_label_rule(
+ remote_ip_prefix, direction,
+ metering_label_id=cls.metering_label['id'])
+
+ @classmethod
+ def create_metering_label(cls, name, description):
+ """Wrapper utility that returns a test metering label."""
+ body = cls.admin_metering_labels_client.create_metering_label(
+ description=description,
+ name=name)
+ metering_label = body['metering_label']
+ cls.addClassResourceCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ cls.admin_metering_labels_client.delete_metering_label,
+ metering_label['id'])
+ return metering_label
+
+ @classmethod
+ def create_metering_label_rule(cls, remote_ip_prefix, direction,
+ metering_label_id):
+ """Wrapper utility that returns a test metering label rule."""
+ client = cls.admin_metering_label_rules_client
+ body = client.create_metering_label_rule(
+ remote_ip_prefix=remote_ip_prefix, direction=direction,
+ metering_label_id=metering_label_id)
+ metering_label_rule = body['metering_label_rule']
+ cls.addClassResourceCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ client.delete_metering_label_rule, metering_label_rule['id'])
+ return metering_label_rule
+
+ def _delete_metering_label(self, metering_label_id):
+ # Deletes a label and verifies if it is deleted or not
+ self.admin_metering_labels_client.delete_metering_label(
+ metering_label_id)
+ # Asserting that the label is not found in list after deletion
+ labels = self.admin_metering_labels_client.list_metering_labels(
+ id=metering_label_id)
+ self.assertEmpty(labels['metering_labels'])
+
+ def _delete_metering_label_rule(self, metering_label_rule_id):
+ client = self.admin_metering_label_rules_client
+ # Deletes a rule and verifies if it is deleted or not
+ client.delete_metering_label_rule(metering_label_rule_id)
+ # Asserting that the rule is not found in list after deletion
+ rules = client.list_metering_label_rules(id=metering_label_rule_id)
+ self.assertEqual(len(rules['metering_label_rules']), 0)
+
+ @decorators.idempotent_id('e2fb2f8c-45bf-429a-9f17-171c70444612')
+ def test_list_metering_labels(self):
+ # Verify label filtering
+ body = self.admin_metering_labels_client.list_metering_labels(id=33)
+ metering_labels = body['metering_labels']
+ self.assertEmpty(metering_labels)
+
+ @decorators.idempotent_id('ec8e15ff-95d0-433b-b8a6-b466bddb1e50')
+ def test_create_delete_metering_label_with_filters(self):
+ # Creates a label
+ name = data_utils.rand_name('metering-label-')
+ description = "label created by tempest"
+ body = self.admin_metering_labels_client.create_metering_label(
+ name=name, description=description)
+ metering_label = body['metering_label']
+ self.addCleanup(self._delete_metering_label,
+ metering_label['id'])
+ # Assert whether created labels are found in labels list or fail
+ # if created labels are not found in labels list
+ labels = (self.admin_metering_labels_client.list_metering_labels(
+ id=metering_label['id']))
+ self.assertEqual(len(labels['metering_labels']), 1)
+
+ @decorators.idempotent_id('30abb445-0eea-472e-bd02-8649f54a5968')
+ def test_show_metering_label(self):
+ # Verifies the details of a label
+ body = self.admin_metering_labels_client.show_metering_label(
+ self.metering_label['id'])
+ metering_label = body['metering_label']
+ self.assertEqual(self.metering_label['id'], metering_label['id'])
+ self.assertEqual(self.metering_label['tenant_id'],
+ metering_label['tenant_id'])
+ self.assertEqual(self.metering_label['name'], metering_label['name'])
+ self.assertEqual(self.metering_label['description'],
+ metering_label['description'])
+
+ @decorators.idempotent_id('cc832399-6681-493b-9d79-0202831a1281')
+ def test_list_metering_label_rules(self):
+ client = self.admin_metering_label_rules_client
+ # Verify rule filtering
+ body = client.list_metering_label_rules(id=33)
+ metering_label_rules = body['metering_label_rules']
+ self.assertEmpty(metering_label_rules)
+
+ @decorators.idempotent_id('f4d547cd-3aee-408f-bf36-454f8825e045')
+ def test_create_delete_metering_label_rule_with_filters(self):
+ # Creates a rule
+ remote_ip_prefix = ("10.0.1.0/24" if self._ip_version == 4
+ else "fd03::/64")
+ client = self.admin_metering_label_rules_client
+ body = (client.create_metering_label_rule(
+ remote_ip_prefix=remote_ip_prefix,
+ direction="ingress",
+ metering_label_id=self.metering_label['id']))
+ metering_label_rule = body['metering_label_rule']
+ self.addCleanup(self._delete_metering_label_rule,
+ metering_label_rule['id'])
+ # Assert whether created rules are found in rules list or fail
+ # if created rules are not found in rules list
+ rules = client.list_metering_label_rules(id=metering_label_rule['id'])
+ self.assertEqual(len(rules['metering_label_rules']), 1)
+
+ @decorators.idempotent_id('b7354489-96ea-41f3-9452-bace120fb4a7')
+ def test_show_metering_label_rule(self):
+ # Verifies the details of a rule
+ client = self.admin_metering_label_rules_client
+ body = (client.show_metering_label_rule(
+ self.metering_label_rule['id']))
+ metering_label_rule = body['metering_label_rule']
+ self.assertEqual(self.metering_label_rule['id'],
+ metering_label_rule['id'])
+ self.assertEqual(self.metering_label_rule['remote_ip_prefix'],
+ metering_label_rule['remote_ip_prefix'])
+ self.assertEqual(self.metering_label_rule['direction'],
+ metering_label_rule['direction'])
+ self.assertEqual(self.metering_label_rule['metering_label_id'],
+ metering_label_rule['metering_label_id'])
+ self.assertFalse(metering_label_rule['excluded'])
+
+
+class MeteringIpV6TestJSON(MeteringTestJSON):
+ _ip_version = 6
diff --git a/tempest/api/network/admin/test_negative_quotas.py b/tempest/api/network/admin/test_negative_quotas.py
index 47da08c..6849653 100644
--- a/tempest/api/network/admin/test_negative_quotas.py
+++ b/tempest/api/network/admin/test_negative_quotas.py
@@ -14,8 +14,9 @@
# under the License.
from tempest.api.network import base
-from tempest import test
-from tempest_lib import exceptions as lib_exc
+from tempest.common import utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class QuotasNegativeTest(base.BaseAdminNetworkTest):
@@ -24,7 +25,7 @@
set network quota and exceed this quota
v2.0 of the API is assumed.
- It is also assumed that the per-tenant quota extension API is configured
+ It is also assumed that the per-project quota extension API is configured
in /etc/neutron/neutron.conf as follows:
quota_driver = neutron.db.quota_db.DbQuotaDriver
@@ -34,16 +35,12 @@
@classmethod
def skip_checks(cls):
super(QuotasNegativeTest, cls).skip_checks()
- if not test.is_extension_enabled('quotas', 'network'):
+ if not utils.is_extension_enabled('quotas', 'network'):
msg = "quotas extension not enabled."
raise cls.skipException(msg)
- @classmethod
- def setup_clients(cls):
- super(QuotasNegativeTest, cls).setup_clients()
- cls.identity_admin_client = cls.os_adm.identity_client
-
- @test.idempotent_id('644f4e1b-1bf9-4af0-9fd8-eb56ac0f51cf')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('644f4e1b-1bf9-4af0-9fd8-eb56ac0f51cf')
def test_network_quota_exceeding(self):
# Set the network quota to two
self.admin_quotas_client.update_quotas(self.networks_client.tenant_id,
@@ -60,10 +57,9 @@
n2['network']['id'])
# Try to create a third network while the quota is two
- with self.assertRaisesRegexp(
+ with self.assertRaisesRegex(
lib_exc.Conflict,
- "An object with that identifier already exists\\n" +
- "Details.*Quota exceeded for resources: \['network'\].*"):
+ "Quota exceeded for resources: \['network'\].*"):
n3 = self.networks_client.create_network()
self.addCleanup(self.networks_client.delete_network,
n3['network']['id'])
diff --git a/tempest/api/network/admin/test_ports.py b/tempest/api/network/admin/test_ports.py
new file mode 100644
index 0000000..483b405
--- /dev/null
+++ b/tempest/api/network/admin/test_ports.py
@@ -0,0 +1,103 @@
+# 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 tempest.api.network import base
+from tempest import config
+from tempest.lib import decorators
+
+CONF = config.CONF
+
+
+class PortsAdminExtendedAttrsTestJSON(base.BaseAdminNetworkTest):
+
+ @classmethod
+ def setup_clients(cls):
+ super(PortsAdminExtendedAttrsTestJSON, cls).setup_clients()
+ cls.hyper_client = cls.os_admin.hypervisor_client
+
+ @classmethod
+ def resource_setup(cls):
+ super(PortsAdminExtendedAttrsTestJSON, cls).resource_setup()
+ cls.network = cls.create_network()
+ hyper_list = cls.hyper_client.list_hypervisors()
+ cls.host_id = hyper_list['hypervisors'][0]['hypervisor_hostname']
+
+ @decorators.idempotent_id('8e8569c1-9ac7-44db-8bc1-f5fb2814f29b')
+ def test_create_port_binding_ext_attr(self):
+ post_body = {"network_id": self.network['id'],
+ "binding:host_id": self.host_id}
+ body = self.admin_ports_client.create_port(**post_body)
+ port = body['port']
+ self.addCleanup(self.admin_ports_client.delete_port, port['id'])
+ host_id = port['binding:host_id']
+ self.assertIsNotNone(host_id)
+ self.assertEqual(self.host_id, host_id)
+
+ @decorators.idempotent_id('6f6c412c-711f-444d-8502-0ac30fbf5dd5')
+ def test_update_port_binding_ext_attr(self):
+ post_body = {"network_id": self.network['id']}
+ body = self.admin_ports_client.create_port(**post_body)
+ port = body['port']
+ self.addCleanup(self.admin_ports_client.delete_port, port['id'])
+ update_body = {"binding:host_id": self.host_id}
+ body = self.admin_ports_client.update_port(port['id'], **update_body)
+ updated_port = body['port']
+ host_id = updated_port['binding:host_id']
+ self.assertIsNotNone(host_id)
+ self.assertEqual(self.host_id, host_id)
+
+ @decorators.idempotent_id('1c82a44a-6c6e-48ff-89e1-abe7eaf8f9f8')
+ def test_list_ports_binding_ext_attr(self):
+ # Create a new port
+ post_body = {"network_id": self.network['id']}
+ body = self.admin_ports_client.create_port(**post_body)
+ port = body['port']
+ self.addCleanup(self.admin_ports_client.delete_port, port['id'])
+
+ # Update the port's binding attributes so that is now 'bound'
+ # to a host
+ update_body = {"binding:host_id": self.host_id}
+ self.admin_ports_client.update_port(port['id'], **update_body)
+
+ # List all ports, ensure new port is part of list and its binding
+ # attributes are set and accurate
+ body = self.admin_ports_client.list_ports()
+ ports_list = body['ports']
+ pids_list = [p['id'] for p in ports_list]
+ self.assertIn(port['id'], pids_list)
+ listed_port = [p for p in ports_list if p['id'] == port['id']]
+ self.assertEqual(1, len(listed_port),
+ 'Multiple ports listed with id %s in ports listing: '
+ '%s' % (port['id'], ports_list))
+ self.assertEqual(self.host_id, listed_port[0]['binding:host_id'])
+
+ @decorators.idempotent_id('b54ac0ff-35fc-4c79-9ca3-c7dbd4ea4f13')
+ def test_show_port_binding_ext_attr(self):
+ body = self.admin_ports_client.create_port(
+ network_id=self.network['id'])
+ port = body['port']
+ self.addCleanup(self.admin_ports_client.delete_port, port['id'])
+ body = self.admin_ports_client.show_port(port['id'])
+ show_port = body['port']
+ self.assertEqual(port['binding:host_id'],
+ show_port['binding:host_id'])
+ self.assertEqual(port['binding:vif_type'],
+ show_port['binding:vif_type'])
+ self.assertEqual(port['binding:vif_details'],
+ show_port['binding:vif_details'])
+
+
+class PortsAdminExtendedAttrsIpV6TestJSON(PortsAdminExtendedAttrsTestJSON):
+ _ip_version = 6
diff --git a/tempest/api/network/admin/test_quotas.py b/tempest/api/network/admin/test_quotas.py
index 45d35cf..b1e4a58 100644
--- a/tempest/api/network/admin/test_quotas.py
+++ b/tempest/api/network/admin/test_quotas.py
@@ -13,23 +13,26 @@
# License for the specific language governing permissions and limitations
# under the License.
-import six
+import testtools
from tempest.api.network import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.common import identity
+from tempest.common import utils
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
class QuotasTest(base.BaseAdminNetworkTest):
"""Tests the following operations in the Neutron API:
- list quotas for tenants who have non-default quota values
- show quotas for a specified tenant
- update quotas for a specified tenant
- reset quotas to default values for a specified tenant
+ list quotas for projects who have non-default quota values
+ show quotas for a specified project
+ update quotas for a specified project
+ reset quotas to default values for a specified project
v2.0 of the API is assumed.
- It is also assumed that the per-tenant quota extension API is configured
+ It is also assumed that the per-project quota extension API is configured
in /etc/neutron/neutron.conf as follows:
quota_driver = neutron.db.quota_db.DbQuotaDriver
@@ -38,32 +41,29 @@
@classmethod
def skip_checks(cls):
super(QuotasTest, cls).skip_checks()
- if not test.is_extension_enabled('quotas', 'network'):
+ if not utils.is_extension_enabled('quotas', 'network'):
msg = "quotas extension not enabled."
raise cls.skipException(msg)
- @classmethod
- def setup_clients(cls):
- super(QuotasTest, cls).setup_clients()
- cls.identity_admin_client = cls.os_adm.identity_client
-
def _check_quotas(self, new_quotas):
- # Add a tenant to conduct the test
+ # Add a project to conduct the test
project = data_utils.rand_name('test_project_')
description = data_utils.rand_name('desc_')
- project = self.identity_utils.create_project(name=project,
- description=description)
+ project = identity.identity_utils(self.os_admin).create_project(
+ name=project, description=description)
project_id = project['id']
- self.addCleanup(self.identity_utils.delete_project, project_id)
+ self.addCleanup(identity.identity_utils(self.os_admin).delete_project,
+ project_id)
- # Change quotas for tenant
+ # Change quotas for project
quota_set = self.admin_quotas_client.update_quotas(
project_id, **new_quotas)['quota']
- self.addCleanup(self.admin_quotas_client.reset_quotas, project_id)
- for key, value in six.iteritems(new_quotas):
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.admin_quotas_client.reset_quotas, project_id)
+ for key, value in new_quotas.items():
self.assertEqual(value, quota_set[key])
- # Confirm our tenant is listed among tenants with non default quotas
+ # Confirm our project is listed among projects with non default quotas
non_default_quotas = self.admin_quotas_client.list_quotas()
found = False
for qs in non_default_quotas['quotas']:
@@ -71,10 +71,10 @@
found = True
self.assertTrue(found)
- # Confirm from API quotas were changed as requested for tenant
+ # Confirm from API quotas were changed as requested for project
quota_set = self.admin_quotas_client.show_quotas(project_id)
quota_set = quota_set['quota']
- for key, value in six.iteritems(new_quotas):
+ for key, value in new_quotas.items():
self.assertEqual(value, quota_set[key])
# Reset quotas to default and confirm
@@ -82,8 +82,24 @@
non_default_quotas = self.admin_quotas_client.list_quotas()
for q in non_default_quotas['quotas']:
self.assertNotEqual(project_id, q['tenant_id'])
+ quota_set = self.admin_quotas_client.show_quotas(project_id)['quota']
+ default_quotas = self.admin_quotas_client.show_default_quotas(
+ project_id)['quota']
+ self.assertEqual(default_quotas, quota_set)
- @test.idempotent_id('2390f766-836d-40ef-9aeb-e810d78207fb')
+ @decorators.idempotent_id('2390f766-836d-40ef-9aeb-e810d78207fb')
def test_quotas(self):
- new_quotas = {'network': 0, 'security_group': 0}
+ new_quotas = {'network': 0, 'port': 0}
self._check_quotas(new_quotas)
+
+ @testtools.skipUnless(utils.is_extension_enabled(
+ 'quota_details', 'network'), 'Quota details extension not enabled.')
+ @decorators.idempotent_id('7b05ec5f-bf44-43cb-b28f-ddd72a824288')
+ def test_show_quota_details(self):
+ # Show quota details for an existing project
+ quota_details = self.admin_quotas_client.show_quota_details(
+ self.admin_quotas_client.tenant_id)['quota']
+ expected_keys = ['used', 'limit', 'reserved']
+ for resource_type in quota_details:
+ for key in expected_keys:
+ self.assertIn(key, quota_details[resource_type])
diff --git a/tempest/api/network/admin/test_routers.py b/tempest/api/network/admin/test_routers.py
new file mode 100644
index 0000000..8cdb41e
--- /dev/null
+++ b/tempest/api/network/admin/test_routers.py
@@ -0,0 +1,213 @@
+# Copyright 2013 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.
+
+import testtools
+
+from tempest.api.network import base
+from tempest.common import identity
+from tempest.common import utils
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+
+CONF = config.CONF
+
+
+class RoutersAdminTest(base.BaseAdminNetworkTest):
+ # NOTE(salv-orlando): This class inherits from BaseAdminNetworkTest
+ # as some router operations, such as enabling or disabling SNAT
+ # require admin credentials by default
+
+ def _cleanup_router(self, router):
+ self.delete_router(router)
+ self.routers.remove(router)
+
+ def _create_router(self, name=None, admin_state_up=False,
+ external_network_id=None, enable_snat=None):
+ # associate a cleanup with created routers to avoid quota limits
+ router = self.create_router(name, admin_state_up,
+ external_network_id, enable_snat)
+ self.addCleanup(self._cleanup_router, router)
+ return router
+
+ @classmethod
+ def skip_checks(cls):
+ super(RoutersAdminTest, cls).skip_checks()
+ if not utils.is_extension_enabled('router', 'network'):
+ msg = "router extension not enabled."
+ raise cls.skipException(msg)
+
+ @decorators.idempotent_id('e54dd3a3-4352-4921-b09d-44369ae17397')
+ def test_create_router_setting_project_id(self):
+ # Test creating router from admin user setting project_id.
+ project = data_utils.rand_name('test_tenant_')
+ description = data_utils.rand_name('desc_')
+ project = identity.identity_utils(self.os_admin).create_project(
+ name=project, description=description)
+ project_id = project['id']
+ self.addCleanup(identity.identity_utils(self.os_admin).delete_project,
+ project_id)
+
+ name = data_utils.rand_name('router-')
+ create_body = self.admin_routers_client.create_router(
+ name=name, tenant_id=project_id)
+ self.addCleanup(self.admin_routers_client.delete_router,
+ create_body['router']['id'])
+ self.assertEqual(project_id, create_body['router']['tenant_id'])
+
+ @decorators.idempotent_id('847257cc-6afd-4154-b8fb-af49f5670ce8')
+ @utils.requires_ext(extension='ext-gw-mode', service='network')
+ @testtools.skipUnless(CONF.network.public_network_id,
+ 'The public_network_id option must be specified.')
+ def test_create_router_with_default_snat_value(self):
+ # Create a router with default snat rule
+ router = self._create_router(
+ external_network_id=CONF.network.public_network_id)
+ self._verify_router_gateway(
+ router['id'], {'network_id': CONF.network.public_network_id,
+ 'enable_snat': True})
+
+ @decorators.idempotent_id('ea74068d-09e9-4fd7-8995-9b6a1ace920f')
+ @utils.requires_ext(extension='ext-gw-mode', service='network')
+ @testtools.skipUnless(CONF.network.public_network_id,
+ 'The public_network_id option must be specified.')
+ def test_create_router_with_snat_explicit(self):
+ name = data_utils.rand_name('snat-router')
+ # Create a router enabling snat attributes
+ enable_snat_states = [False, True]
+ for enable_snat in enable_snat_states:
+ external_gateway_info = {
+ 'network_id': CONF.network.public_network_id,
+ 'enable_snat': enable_snat}
+ create_body = self.admin_routers_client.create_router(
+ name=name, external_gateway_info=external_gateway_info)
+ self.addCleanup(self.admin_routers_client.delete_router,
+ create_body['router']['id'])
+ # Verify snat attributes after router creation
+ self._verify_router_gateway(create_body['router']['id'],
+ exp_ext_gw_info=external_gateway_info)
+
+ def _verify_router_gateway(self, router_id, exp_ext_gw_info=None):
+ show_body = self.admin_routers_client.show_router(router_id)
+ actual_ext_gw_info = show_body['router']['external_gateway_info']
+ if exp_ext_gw_info is None:
+ self.assertIsNone(actual_ext_gw_info)
+ return
+ # Verify only keys passed in exp_ext_gw_info
+ for k, v in exp_ext_gw_info.items():
+ self.assertEqual(v, actual_ext_gw_info[k])
+
+ def _verify_gateway_port(self, router_id):
+ list_body = self.admin_ports_client.list_ports(
+ network_id=CONF.network.public_network_id,
+ device_id=router_id)
+ self.assertEqual(len(list_body['ports']), 1)
+ gw_port = list_body['ports'][0]
+ fixed_ips = gw_port['fixed_ips']
+ self.assertNotEmpty(fixed_ips)
+ # Assert that all of the IPs from the router gateway port
+ # are allocated from a valid public subnet.
+ public_net_body = self.admin_networks_client.show_network(
+ CONF.network.public_network_id)
+ public_subnet_ids = public_net_body['network']['subnets']
+ for fixed_ip in fixed_ips:
+ subnet_id = fixed_ip['subnet_id']
+ self.assertIn(subnet_id, public_subnet_ids)
+
+ @decorators.idempotent_id('6cc285d8-46bf-4f36-9b1a-783e3008ba79')
+ @testtools.skipUnless(CONF.network.public_network_id,
+ 'The public_network_id option must be specified.')
+ def test_update_router_set_gateway(self):
+ router = self._create_router()
+ self.routers_client.update_router(
+ router['id'],
+ external_gateway_info={
+ 'network_id': CONF.network.public_network_id})
+ # Verify operation - router
+ self._verify_router_gateway(
+ router['id'],
+ {'network_id': CONF.network.public_network_id})
+ self._verify_gateway_port(router['id'])
+
+ @decorators.idempotent_id('b386c111-3b21-466d-880c-5e72b01e1a33')
+ @utils.requires_ext(extension='ext-gw-mode', service='network')
+ @testtools.skipUnless(CONF.network.public_network_id,
+ 'The public_network_id option must be specified.')
+ def test_update_router_set_gateway_with_snat_explicit(self):
+ router = self._create_router()
+ self.admin_routers_client.update_router(
+ router['id'],
+ external_gateway_info={
+ 'network_id': CONF.network.public_network_id,
+ 'enable_snat': True})
+ self._verify_router_gateway(
+ router['id'],
+ {'network_id': CONF.network.public_network_id,
+ 'enable_snat': True})
+ self._verify_gateway_port(router['id'])
+
+ @decorators.idempotent_id('96536bc7-8262-4fb2-9967-5c46940fa279')
+ @utils.requires_ext(extension='ext-gw-mode', service='network')
+ @testtools.skipUnless(CONF.network.public_network_id,
+ 'The public_network_id option must be specified.')
+ def test_update_router_set_gateway_without_snat(self):
+ router = self._create_router()
+ self.admin_routers_client.update_router(
+ router['id'],
+ external_gateway_info={
+ 'network_id': CONF.network.public_network_id,
+ 'enable_snat': False})
+ self._verify_router_gateway(
+ router['id'],
+ {'network_id': CONF.network.public_network_id,
+ 'enable_snat': False})
+ self._verify_gateway_port(router['id'])
+
+ @decorators.idempotent_id('ad81b7ee-4f81-407b-a19c-17e623f763e8')
+ @testtools.skipUnless(CONF.network.public_network_id,
+ 'The public_network_id option must be specified.')
+ def test_update_router_unset_gateway(self):
+ router = self._create_router(
+ external_network_id=CONF.network.public_network_id)
+ self.routers_client.update_router(router['id'],
+ external_gateway_info={})
+ self._verify_router_gateway(router['id'])
+ # No gateway port expected
+ list_body = self.admin_ports_client.list_ports(
+ network_id=CONF.network.public_network_id,
+ device_id=router['id'])
+ self.assertFalse(list_body['ports'])
+
+ @decorators.idempotent_id('f2faf994-97f4-410b-a831-9bc977b64374')
+ @utils.requires_ext(extension='ext-gw-mode', service='network')
+ @testtools.skipUnless(CONF.network.public_network_id,
+ 'The public_network_id option must be specified.')
+ def test_update_router_reset_gateway_without_snat(self):
+ router = self._create_router(
+ external_network_id=CONF.network.public_network_id)
+ self.admin_routers_client.update_router(
+ router['id'],
+ external_gateway_info={
+ 'network_id': CONF.network.public_network_id,
+ 'enable_snat': False})
+ self._verify_router_gateway(
+ router['id'],
+ {'network_id': CONF.network.public_network_id,
+ 'enable_snat': False})
+ self._verify_gateway_port(router['id'])
+
+
+class RoutersIpV6AdminTest(RoutersAdminTest):
+ _ip_version = 6
diff --git a/tempest/api/network/admin/test_routers_dvr.py b/tempest/api/network/admin/test_routers_dvr.py
index 3e787af..93478e6 100644
--- a/tempest/api/network/admin/test_routers_dvr.py
+++ b/tempest/api/network/admin/test_routers_dvr.py
@@ -13,17 +13,21 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest.api.network import base_routers as base
-from tempest.common.utils import data_utils
-from tempest import test
+import testtools
+
+from tempest.api.network import base
+from tempest.common import utils
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
-class RoutersTestDVR(base.BaseRouterTest):
+class RoutersTestDVR(base.BaseAdminNetworkTest):
@classmethod
- def resource_setup(cls):
+ def skip_checks(cls):
+ super(RoutersTestDVR, cls).skip_checks()
for ext in ['router', 'dvr']:
- if not test.is_extension_enabled(ext, 'network'):
+ if not utils.is_extension_enabled(ext, 'network'):
msg = "%s extension not enabled." % ext
raise cls.skipException(msg)
# The check above will pass if api_extensions=all, which does
@@ -32,15 +36,18 @@
# admin credentials to create router with distributed=True attribute
# and checking for BadRequest exception and that the resulting router
# has a distributed attribute.
+
+ @classmethod
+ def resource_setup(cls):
super(RoutersTestDVR, cls).resource_setup()
name = data_utils.rand_name('pretest-check')
- router = cls.admin_client.create_router(name)
- cls.admin_client.delete_router(router['router']['id'])
+ router = cls.admin_routers_client.create_router(name=name)
+ cls.admin_routers_client.delete_router(router['router']['id'])
if 'distributed' not in router['router']:
msg = "'distributed' flag not found. DVR Possibly not enabled"
raise cls.skipException(msg)
- @test.idempotent_id('08a2a0a8-f1e4-4b34-8e30-e522e836c44e')
+ @decorators.idempotent_id('08a2a0a8-f1e4-4b34-8e30-e522e836c44e')
def test_distributed_router_creation(self):
"""Test distributed router creation
@@ -53,12 +60,13 @@
set to True
"""
name = data_utils.rand_name('router')
- router = self.admin_client.create_router(name, distributed=True)
- self.addCleanup(self.admin_client.delete_router,
+ router = self.admin_routers_client.create_router(name=name,
+ distributed=True)
+ self.addCleanup(self.admin_routers_client.delete_router,
router['router']['id'])
self.assertTrue(router['router']['distributed'])
- @test.idempotent_id('8a0a72b4-7290-4677-afeb-b4ffe37bc352')
+ @decorators.idempotent_id('8a0a72b4-7290-4677-afeb-b4ffe37bc352')
def test_centralized_router_creation(self):
"""Test centralized router creation
@@ -72,12 +80,15 @@
as opposed to a "Distributed Virtual Router"
"""
name = data_utils.rand_name('router')
- router = self.admin_client.create_router(name, distributed=False)
- self.addCleanup(self.admin_client.delete_router,
+ router = self.admin_routers_client.create_router(name=name,
+ distributed=False)
+ self.addCleanup(self.admin_routers_client.delete_router,
router['router']['id'])
self.assertFalse(router['router']['distributed'])
- @test.idempotent_id('acd43596-c1fb-439d-ada8-31ad48ae3c2e')
+ @decorators.idempotent_id('acd43596-c1fb-439d-ada8-31ad48ae3c2e')
+ @testtools.skipUnless(utils.is_extension_enabled('l3-ha', 'network'),
+ 'HA routers are not available.')
def test_centralized_router_update_to_dvr(self):
"""Test centralized router update
@@ -92,12 +103,22 @@
attribute will be set to True
"""
name = data_utils.rand_name('router')
+ tenant_id = self.routers_client.tenant_id
# router needs to be in admin state down in order to be upgraded to DVR
- router = self.admin_client.create_router(name, distributed=False,
- admin_state_up=False)
- self.addCleanup(self.admin_client.delete_router,
- router['router']['id'])
+ # l3ha routers are not upgradable to dvr, make it explicitly non ha
+ router = self.admin_routers_client.create_router(name=name,
+ distributed=False,
+ admin_state_up=False,
+ ha=False,
+ tenant_id=tenant_id)
+ router_id = router['router']['id']
+ self.addCleanup(self.admin_routers_client.delete_router,
+ router_id)
self.assertFalse(router['router']['distributed'])
- router = self.admin_client.update_router(router['router']['id'],
- distributed=True)
+ router = self.admin_routers_client.update_router(
+ router_id, distributed=True)
self.assertTrue(router['router']['distributed'])
+ show_body = self.admin_routers_client.show_router(router_id)
+ self.assertTrue(show_body['router']['distributed'])
+ show_body = self.routers_client.show_router(router_id)
+ self.assertNotIn('distributed', show_body['router'])
diff --git a/tempest/api/network/admin/test_routers_negative.py b/tempest/api/network/admin/test_routers_negative.py
new file mode 100644
index 0000000..9356bcc
--- /dev/null
+++ b/tempest/api/network/admin/test_routers_negative.py
@@ -0,0 +1,63 @@
+# Copyright 2013 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.
+
+import testtools
+
+from tempest.api.network import base
+from tempest.common import utils
+from tempest import config
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+CONF = config.CONF
+
+
+class RoutersAdminNegativeTest(base.BaseAdminNetworkTest):
+
+ @classmethod
+ def skip_checks(cls):
+ super(RoutersAdminNegativeTest, cls).skip_checks()
+ if not utils.is_extension_enabled('router', 'network'):
+ msg = "router extension not enabled."
+ raise cls.skipException(msg)
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('7101cc02-058a-11e7-93e1-fa163e4fa634')
+ @utils.requires_ext(extension='ext-gw-mode', service='network')
+ @testtools.skipUnless(CONF.network.public_network_id,
+ 'The public_network_id option must be specified.')
+ def test_router_set_gateway_used_ip_returns_409(self):
+ # At first create a address from public_network_id
+ port = self.admin_ports_client.create_port(
+ network_id=CONF.network.public_network_id)['port']
+ self.addCleanup(self.admin_ports_client.delete_port,
+ port_id=port['id'])
+ # Add used ip and subnet_id in external_fixed_ips
+ fixed_ip = {
+ 'subnet_id': port['fixed_ips'][0]['subnet_id'],
+ 'ip_address': port['fixed_ips'][0]['ip_address']
+ }
+ external_gateway_info = {
+ 'network_id': CONF.network.public_network_id,
+ 'external_fixed_ips': [fixed_ip]
+ }
+ # Create a router and set gateway to used ip
+ self.assertRaises(lib_exc.Conflict,
+ self.admin_routers_client.create_router,
+ external_gateway_info=external_gateway_info)
+
+
+class RoutersAdminNegativeIpV6Test(RoutersAdminNegativeTest):
+ _ip_version = 6
diff --git a/tempest/api/network/base.py b/tempest/api/network/base.py
index 2a1776a..8670165 100644
--- a/tempest/api/network/base.py
+++ b/tempest/api/network/base.py
@@ -14,32 +14,30 @@
# under the License.
import netaddr
-from oslo_log import log as logging
-from tempest_lib import exceptions as lib_exc
-from tempest.common.utils import data_utils
from tempest import config
from tempest import exceptions
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import exceptions as lib_exc
import tempest.test
CONF = config.CONF
-LOG = logging.getLogger(__name__)
-
class BaseNetworkTest(tempest.test.BaseTestCase):
- """Base class for the Neutron tests
+ """Base class for the Neutron tests.
Per the Neutron API Guide, API v1.x was removed from the source code tree
(docs.openstack.org/api/openstack-network/2.0/content/Overview-d1e71.html)
Therefore, v2.x of the Neutron API is assumed. It is also assumed that the
following options are defined in the [network] section of etc/tempest.conf:
- tenant_network_cidr with a block of cidr's from which smaller blocks
- can be allocated for tenant networks
+ project_network_cidr with a block of cidr's from which smaller blocks
+ can be allocated for project networks
- tenant_network_mask_bits with the mask bits to be used to partition the
- block defined by tenant-network_cidr
+ project_network_mask_bits with the mask bits to be used to partition
+ the block defined by project-network_cidr
Finally, it is assumed that the following option is defined in the
[service_available] section of etc/tempest.conf
@@ -70,103 +68,54 @@
@classmethod
def setup_clients(cls):
super(BaseNetworkTest, cls).setup_clients()
- cls.client = cls.os.network_client
- cls.agents_client = cls.os.network_agents_client
- cls.networks_client = cls.os.networks_client
- cls.subnetpools_client = cls.os.subnetpools_client
- cls.subnets_client = cls.os.subnets_client
- cls.ports_client = cls.os.ports_client
- cls.quotas_client = cls.os.network_quotas_client
- cls.floating_ips_client = cls.os.floating_ips_client
- cls.security_groups_client = cls.os.security_groups_client
+ cls.agents_client = cls.os_primary.network_agents_client
+ cls.network_extensions_client =\
+ cls.os_primary.network_extensions_client
+ cls.networks_client = cls.os_primary.networks_client
+ cls.routers_client = cls.os_primary.routers_client
+ cls.subnetpools_client = cls.os_primary.subnetpools_client
+ cls.subnets_client = cls.os_primary.subnets_client
+ cls.ports_client = cls.os_primary.ports_client
+ cls.quotas_client = cls.os_primary.network_quotas_client
+ cls.floating_ips_client = cls.os_primary.floating_ips_client
+ cls.security_groups_client = cls.os_primary.security_groups_client
+ cls.security_group_rules_client = (
+ cls.os_primary.security_group_rules_client)
+ cls.network_versions_client = cls.os_primary.network_versions_client
+ cls.service_providers_client = cls.os_primary.service_providers_client
+ cls.tags_client = cls.os_primary.tags_client
@classmethod
def resource_setup(cls):
super(BaseNetworkTest, cls).resource_setup()
- cls.networks = []
cls.subnets = []
cls.ports = []
cls.routers = []
- cls.floating_ips = []
- cls.metering_labels = []
- cls.metering_label_rules = []
cls.ethertype = "IPv" + str(cls._ip_version)
+ if cls._ip_version == 4:
+ cls.cidr = netaddr.IPNetwork(CONF.network.project_network_cidr)
+ cls.mask_bits = CONF.network.project_network_mask_bits
+ elif cls._ip_version == 6:
+ cls.cidr = netaddr.IPNetwork(CONF.network.project_network_v6_cidr)
+ cls.mask_bits = CONF.network.project_network_v6_mask_bits
@classmethod
- def resource_cleanup(cls):
- if CONF.service_available.neutron:
- # Clean up floating IPs
- for floating_ip in cls.floating_ips:
- cls._try_delete_resource(
- cls.floating_ips_client.delete_floatingip,
- floating_ip['id'])
-
- # Clean up metering label rules
- # Not all classes in the hierarchy have the client class variable
- if len(cls.metering_label_rules) > 0:
- label_rules_client = cls.admin_metering_label_rules_client
- for metering_label_rule in cls.metering_label_rules:
- cls._try_delete_resource(
- label_rules_client.delete_metering_label_rule,
- metering_label_rule['id'])
- # Clean up metering labels
- for metering_label in cls.metering_labels:
- cls._try_delete_resource(
- cls.admin_metering_labels_client.delete_metering_label,
- metering_label['id'])
- # Clean up ports
- for port in cls.ports:
- cls._try_delete_resource(cls.ports_client.delete_port,
- port['id'])
- # Clean up routers
- for router in cls.routers:
- cls._try_delete_resource(cls.delete_router,
- router)
- # Clean up subnets
- for subnet in cls.subnets:
- cls._try_delete_resource(cls.subnets_client.delete_subnet,
- subnet['id'])
- # Clean up networks
- for network in cls.networks:
- cls._try_delete_resource(cls.networks_client.delete_network,
- network['id'])
- super(BaseNetworkTest, cls).resource_cleanup()
-
- @classmethod
- def _try_delete_resource(self, delete_callable, *args, **kwargs):
- """Cleanup resources in case of test-failure
-
- Some resources are explicitly deleted by the test.
- If the test failed to delete a resource, this method will execute
- the appropriate delete methods. Otherwise, the method ignores NotFound
- exceptions thrown for resources that were correctly deleted by the
- test.
-
- :param delete_callable: delete method
- :param args: arguments for delete method
- :param kwargs: keyword arguments for delete method
- """
- try:
- delete_callable(*args, **kwargs)
- # if resource is not found, this means it was deleted in the test
- except lib_exc.NotFound:
- pass
-
- @classmethod
- def create_network(cls, network_name=None):
+ def create_network(cls, network_name=None, **kwargs):
"""Wrapper utility that returns a test network."""
- network_name = network_name or data_utils.rand_name('test-network-')
+ network_name = network_name or data_utils.rand_name(
+ cls.__name__ + '-test-network')
- body = cls.networks_client.create_network(name=network_name)
+ body = cls.networks_client.create_network(name=network_name, **kwargs)
network = body['network']
- cls.networks.append(network)
+ cls.addClassResourceCleanup(test_utils.call_and_ignore_notfound_exc,
+ cls.networks_client.delete_network,
+ network['id'])
return network
@classmethod
def create_subnet(cls, network, gateway='', cidr=None, mask_bits=None,
ip_version=None, client=None, **kwargs):
"""Wrapper utility that returns a test subnet."""
-
# allow tests to use admin client
if not client:
client = cls.subnets_client
@@ -175,12 +124,12 @@
ip_version = ip_version if ip_version is not None else cls._ip_version
gateway_not_set = gateway == ''
if ip_version == 4:
- cidr = cidr or netaddr.IPNetwork(CONF.network.tenant_network_cidr)
- mask_bits = mask_bits or CONF.network.tenant_network_mask_bits
+ cidr = cidr or netaddr.IPNetwork(CONF.network.project_network_cidr)
+ mask_bits = mask_bits or CONF.network.project_network_mask_bits
elif ip_version == 6:
- cidr = (
- cidr or netaddr.IPNetwork(CONF.network.tenant_network_v6_cidr))
- mask_bits = mask_bits or CONF.network.tenant_network_v6_mask_bits
+ cidr = (cidr or
+ netaddr.IPNetwork(CONF.network.project_network_v6_cidr))
+ mask_bits = mask_bits or CONF.network.project_network_v6_mask_bits
# Find a cidr that is not in use yet and create a subnet with it
for subnet_cidr in cidr.subnet(mask_bits):
if gateway_not_set:
@@ -203,6 +152,9 @@
message = 'Available CIDR for subnet creation could not be found'
raise exceptions.BuildErrorException(message)
subnet = body['subnet']
+ cls.addClassResourceCleanup(test_utils.call_and_ignore_notfound_exc,
+ cls.subnets_client.delete_subnet,
+ subnet['id'])
cls.subnets.append(subnet)
return subnet
@@ -212,6 +164,8 @@
body = cls.ports_client.create_port(network_id=network['id'],
**kwargs)
port = body['port']
+ cls.addClassResourceCleanup(test_utils.call_and_ignore_notfound_exc,
+ cls.ports_client.delete_port, port['id'])
cls.ports.append(port)
return port
@@ -226,15 +180,20 @@
def create_router(cls, router_name=None, admin_state_up=False,
external_network_id=None, enable_snat=None,
**kwargs):
+ router_name = router_name or data_utils.rand_name(
+ cls.__name__ + "-router")
+
ext_gw_info = {}
if external_network_id:
ext_gw_info['network_id'] = external_network_id
if enable_snat is not None:
ext_gw_info['enable_snat'] = enable_snat
- body = cls.client.create_router(
- router_name, external_gateway_info=ext_gw_info,
+ body = cls.routers_client.create_router(
+ name=router_name, external_gateway_info=ext_gw_info,
admin_state_up=admin_state_up, **kwargs)
router = body['router']
+ cls.addClassResourceCleanup(test_utils.call_and_ignore_notfound_exc,
+ cls.delete_router, router)
cls.routers.append(router)
return router
@@ -244,27 +203,27 @@
body = cls.floating_ips_client.create_floatingip(
floating_network_id=external_network_id)
fip = body['floatingip']
- cls.floating_ips.append(fip)
+ cls.addClassResourceCleanup(test_utils.call_and_ignore_notfound_exc,
+ cls.floating_ips_client.delete_floatingip,
+ fip['id'])
return fip
@classmethod
def create_router_interface(cls, router_id, subnet_id):
"""Wrapper utility that returns a router interface."""
- interface = cls.client.add_router_interface_with_subnet_id(
- router_id, subnet_id)
+ interface = cls.routers_client.add_router_interface(
+ router_id, subnet_id=subnet_id)
return interface
@classmethod
def delete_router(cls, router):
- body = cls.client.list_router_interfaces(router['id'])
+ body = cls.ports_client.list_ports(device_id=router['id'])
interfaces = body['ports']
for i in interfaces:
- try:
- cls.client.remove_router_interface_with_subnet_id(
- router['id'], i['fixed_ips'][0]['subnet_id'])
- except lib_exc.NotFound:
- pass
- cls.client.delete_router(router['id'])
+ test_utils.call_and_ignore_notfound_exc(
+ cls.routers_client.remove_router_interface, router['id'],
+ subnet_id=i['fixed_ips'][0]['subnet_id'])
+ cls.routers_client.delete_router(router['id'])
class BaseAdminNetworkTest(BaseNetworkTest):
@@ -274,35 +233,13 @@
@classmethod
def setup_clients(cls):
super(BaseAdminNetworkTest, cls).setup_clients()
- cls.admin_client = cls.os_adm.network_client
- cls.admin_agents_client = cls.os_adm.network_agents_client
- cls.admin_networks_client = cls.os_adm.networks_client
- cls.admin_subnets_client = cls.os_adm.subnets_client
- cls.admin_ports_client = cls.os_adm.ports_client
- cls.admin_quotas_client = cls.os_adm.network_quotas_client
- cls.admin_floating_ips_client = cls.os_adm.floating_ips_client
- cls.admin_metering_labels_client = cls.os_adm.metering_labels_client
+ cls.admin_agents_client = cls.os_admin.network_agents_client
+ cls.admin_networks_client = cls.os_admin.networks_client
+ cls.admin_routers_client = cls.os_admin.routers_client
+ cls.admin_subnets_client = cls.os_admin.subnets_client
+ cls.admin_ports_client = cls.os_admin.ports_client
+ cls.admin_quotas_client = cls.os_admin.network_quotas_client
+ cls.admin_floating_ips_client = cls.os_admin.floating_ips_client
+ cls.admin_metering_labels_client = cls.os_admin.metering_labels_client
cls.admin_metering_label_rules_client = (
- cls.os_adm.metering_label_rules_client)
-
- @classmethod
- def create_metering_label(cls, name, description):
- """Wrapper utility that returns a test metering label."""
- body = cls.admin_metering_labels_client.create_metering_label(
- description=description,
- name=data_utils.rand_name("metering-label"))
- metering_label = body['metering_label']
- cls.metering_labels.append(metering_label)
- return metering_label
-
- @classmethod
- def create_metering_label_rule(cls, remote_ip_prefix, direction,
- metering_label_id):
- """Wrapper utility that returns a test metering label rule."""
- client = cls.admin_metering_label_rules_client
- body = client.create_metering_label_rule(
- remote_ip_prefix=remote_ip_prefix, direction=direction,
- metering_label_id=metering_label_id)
- metering_label_rule = body['metering_label_rule']
- cls.metering_label_rules.append(metering_label_rule)
- return metering_label_rule
+ cls.os_admin.metering_label_rules_client)
diff --git a/tempest/api/network/base_routers.py b/tempest/api/network/base_routers.py
deleted file mode 100644
index 739e6f9..0000000
--- a/tempest/api/network/base_routers.py
+++ /dev/null
@@ -1,63 +0,0 @@
-# Copyright 2013 OpenStack Foundation
-# All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-from tempest.api.network import base
-
-
-class BaseRouterTest(base.BaseAdminNetworkTest):
- # NOTE(salv-orlando): This class inherits from BaseAdminNetworkTest
- # as some router operations, such as enabling or disabling SNAT
- # require admin credentials by default
-
- def _cleanup_router(self, router):
- self.delete_router(router)
- self.routers.remove(router)
-
- def _create_router(self, name, admin_state_up=False,
- external_network_id=None, enable_snat=None):
- # associate a cleanup with created routers to avoid quota limits
- router = self.create_router(name, admin_state_up,
- external_network_id, enable_snat)
- self.addCleanup(self._cleanup_router, router)
- return router
-
- def _delete_router(self, router_id, network_client=None):
- client = network_client or self.client
- client.delete_router(router_id)
- # Asserting that the router is not found in the list
- # after deletion
- list_body = self.client.list_routers()
- routers_list = list()
- for router in list_body['routers']:
- routers_list.append(router['id'])
- self.assertNotIn(router_id, routers_list)
-
- def _add_router_interface_with_subnet_id(self, router_id, subnet_id):
- interface = self.client.add_router_interface_with_subnet_id(
- router_id, subnet_id)
- self.addCleanup(self._remove_router_interface_with_subnet_id,
- router_id, subnet_id)
- self.assertEqual(subnet_id, interface['subnet_id'])
- return interface
-
- def _remove_router_interface_with_subnet_id(self, router_id, subnet_id):
- body = self.client.remove_router_interface_with_subnet_id(
- router_id, subnet_id)
- self.assertEqual(subnet_id, body['subnet_id'])
-
- def _remove_router_interface_with_port_id(self, router_id, port_id):
- body = self.client.remove_router_interface_with_port_id(router_id,
- port_id)
- self.assertEqual(port_id, body['port_id'])
diff --git a/tempest/api/network/base_security_groups.py b/tempest/api/network/base_security_groups.py
index 1525e19..b8d677a 100644
--- a/tempest/api/network/base_security_groups.py
+++ b/tempest/api/network/base_security_groups.py
@@ -14,7 +14,7 @@
# under the License.
from tempest.api.network import base
-from tempest.common.utils import data_utils
+from tempest.lib.common.utils import data_utils
class BaseSecGroupTest(base.BaseNetworkTest):
@@ -40,10 +40,11 @@
self.assertNotIn(secgroup_id, secgroup_list)
def _delete_security_group_rule(self, rule_id):
- self.client.delete_security_group_rule(rule_id)
+ self.security_group_rules_client.delete_security_group_rule(rule_id)
# Asserting that the security group is not found in the list
# after deletion
- list_body = self.client.list_security_group_rules()
+ list_body = (
+ self.security_group_rules_client.list_security_group_rules())
rules_list = list()
for rule in list_body['security_group_rules']:
rules_list.append(rule['id'])
diff --git a/tempest/api/network/test_allowed_address_pair.py b/tempest/api/network/test_allowed_address_pair.py
index 394aec1..3075047 100644
--- a/tempest/api/network/test_allowed_address_pair.py
+++ b/tempest/api/network/test_allowed_address_pair.py
@@ -13,11 +13,12 @@
# License for the specific language governing permissions and limitations
# under the License.
-import netaddr
+import six
from tempest.api.network import base
+from tempest.common import utils
from tempest import config
-from tempest import test
+from tempest.lib import decorators
CONF = config.CONF
@@ -42,7 +43,7 @@
@classmethod
def skip_checks(cls):
super(AllowedAddressPairTestJSON, cls).skip_checks()
- if not test.is_extension_enabled('allowed-address-pairs', 'network'):
+ if not utils.is_extension_enabled('allowed-address-pairs', 'network'):
msg = "Allowed Address Pairs extension not enabled."
raise cls.skipException(msg)
@@ -55,7 +56,7 @@
cls.ip_address = port['fixed_ips'][0]['ip_address']
cls.mac_address = port['mac_address']
- @test.idempotent_id('86c3529b-1231-40de-803c-00e40882f043')
+ @decorators.idempotent_id('86c3529b-1231-40de-803c-00e40882f043')
def test_create_list_port_with_address_pair(self):
# Create port with allowed address pair attribute
allowed_address_pairs = [{'ip_address': self.ip_address,
@@ -90,20 +91,20 @@
body = self.ports_client.update_port(
port_id, allowed_address_pairs=allowed_address_pairs)
allowed_address_pair = body['port']['allowed_address_pairs']
- self.assertEqual(allowed_address_pair, allowed_address_pairs)
+ six.assertCountEqual(self, allowed_address_pair,
+ allowed_address_pairs)
- @test.idempotent_id('9599b337-272c-47fd-b3cf-509414414ac4')
+ @decorators.idempotent_id('9599b337-272c-47fd-b3cf-509414414ac4')
def test_update_port_with_address_pair(self):
# Update port with allowed address pair
self._update_port_with_address(self.ip_address)
- @test.idempotent_id('4d6d178f-34f6-4bff-a01c-0a2f8fe909e4')
+ @decorators.idempotent_id('4d6d178f-34f6-4bff-a01c-0a2f8fe909e4')
def test_update_port_with_cidr_address_pair(self):
# Update allowed address pair with cidr
- cidr = str(netaddr.IPNetwork(CONF.network.tenant_network_cidr))
- self._update_port_with_address(cidr)
+ self._update_port_with_address(str(self.cidr))
- @test.idempotent_id('b3f20091-6cd5-472b-8487-3516137df933')
+ @decorators.idempotent_id('b3f20091-6cd5-472b-8487-3516137df933')
def test_update_port_with_multiple_ip_mac_address_pair(self):
# Create an ip _address and mac_address through port create
resp = self.ports_client.create_port(network_id=self.network['id'])
diff --git a/tempest/api/network/test_dhcp_ipv6.py b/tempest/api/network/test_dhcp_ipv6.py
index 74c1d51..9d6d700 100644
--- a/tempest/api/network/test_dhcp_ipv6.py
+++ b/tempest/api/network/test_dhcp_ipv6.py
@@ -13,16 +13,17 @@
# License for the specific language governing permissions and limitations
# under the License.
-import netaddr
import random
-import six
-from tempest_lib import exceptions as lib_exc
+import netaddr
+from oslo_utils import netutils
from tempest.api.network import base
-from tempest.common.utils import data_utils
+from tempest.common.utils import net_info
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
CONF = config.CONF
@@ -30,7 +31,7 @@
class NetworksTestDHCPv6(base.BaseNetworkTest):
_ip_version = 6
- """ Test DHCPv6 specific features using SLAAC, stateless and
+ """Test DHCPv6 specific features using SLAAC, stateless and
stateful settings for subnets. Also it shall check dual-stack
functionality (IPv4 + IPv6 together).
The tests include:
@@ -66,11 +67,10 @@
body = self.ports_client.list_ports()
ports = body['ports']
for port in ports:
- if (port['device_owner'].startswith('network:router_interface')
- and port['device_id'] in [r['id'] for r in self.routers]):
- self.client.remove_router_interface_with_port_id(
- port['device_id'], port['id']
- )
+ if (net_info.is_router_interface_port(port) and
+ port['device_id'] in [r['id'] for r in self.routers]):
+ self.routers_client.remove_router_interface(port['device_id'],
+ port_id=port['id'])
else:
if port['id'] in [p['id'] for p in self.ports]:
self.ports_client.delete_port(port['id'])
@@ -81,11 +81,11 @@
if subnet['id'] in [s['id'] for s in self.subnets]:
self.subnets_client.delete_subnet(subnet['id'])
self._remove_from_list_by_index(self.subnets, subnet)
- body = self.client.list_routers()
+ body = self.routers_client.list_routers()
routers = body['routers']
for router in routers:
if router['id'] in [r['id'] for r in self.routers]:
- self.client.delete_router(router['id'])
+ self.routers_client.delete_router(router['id'])
self._remove_from_list_by_index(self.routers, router)
def _get_ips_from_subnet(self, **kwargs):
@@ -93,11 +93,11 @@
port_mac = data_utils.rand_mac_address()
port = self.create_port(self.network, mac_address=port_mac)
real_ip = next(iter(port['fixed_ips']), None)['ip_address']
- eui_ip = data_utils.get_ipv6_addr_by_EUI64(subnet['cidr'],
- port_mac).format()
+ eui_ip = str(netutils.get_ipv6_addr_by_EUI64(
+ subnet['cidr'], port_mac))
return real_ip, eui_ip
- @test.idempotent_id('e5517e62-6f16-430d-a672-f80875493d4c')
+ @decorators.idempotent_id('e5517e62-6f16-430d-a672-f80875493d4c')
def test_dhcpv6_stateless_eui64(self):
# NOTE: When subnets configured with RAs SLAAC (AOM=100) and DHCP
# stateless (AOM=110) both for radvd and dnsmasq, port shall receive
@@ -115,7 +115,7 @@
'ipv6_ra_mode=%s and ipv6_address_mode=%s') % (
real_ip, eui_ip, ra_mode, add_mode))
- @test.idempotent_id('ae2f4a5d-03ff-4c42-a3b0-ce2fcb7ea832')
+ @decorators.idempotent_id('ae2f4a5d-03ff-4c42-a3b0-ce2fcb7ea832')
def test_dhcpv6_stateless_no_ra(self):
# NOTE: When subnets configured with dnsmasq SLAAC and DHCP stateless
# and there is no radvd, port shall receive IP address calculated
@@ -126,7 +126,7 @@
):
kwargs = {'ipv6_ra_mode': ra_mode,
'ipv6_address_mode': add_mode}
- kwargs = dict((k, v) for k, v in six.iteritems(kwargs) if v)
+ kwargs = dict((k, v) for k, v in kwargs.items() if v)
real_ip, eui_ip = self._get_ips_from_subnet(**kwargs)
self._clean_network()
self.assertEqual(eui_ip, real_ip,
@@ -136,7 +136,7 @@
ra_mode if ra_mode else "Off",
add_mode if add_mode else "Off"))
- @test.idempotent_id('81f18ef6-95b5-4584-9966-10d480b7496a')
+ @decorators.idempotent_id('81f18ef6-95b5-4584-9966-10d480b7496a')
def test_dhcpv6_invalid_options(self):
"""Different configurations for radvd and dnsmasq are not allowed"""
for ra_mode, add_mode in (
@@ -154,7 +154,7 @@
self.network,
**kwargs)
- @test.idempotent_id('21635b6f-165a-4d42-bf49-7d195e47342f')
+ @decorators.idempotent_id('21635b6f-165a-4d42-bf49-7d195e47342f')
def test_dhcpv6_stateless_no_ra_no_dhcp(self):
# NOTE: If no radvd option and no dnsmasq option is configured
# port shall receive IP from fixed IPs list of subnet.
@@ -166,7 +166,7 @@
'but shall be taken from fixed IPs') % (
real_ip, eui_ip))
- @test.idempotent_id('4544adf7-bb5f-4bdc-b769-b3e77026cef2')
+ @decorators.idempotent_id('4544adf7-bb5f-4bdc-b769-b3e77026cef2')
def test_dhcpv6_two_subnets(self):
# NOTE: When one IPv6 subnet configured with dnsmasq SLAAC or DHCP
# stateless and other IPv6 is with DHCP stateful, port shall receive
@@ -189,10 +189,8 @@
self.network, **kwargs_dhcp)
subnet_slaac = self.create_subnet(self.network, **kwargs)
port_mac = data_utils.rand_mac_address()
- eui_ip = data_utils.get_ipv6_addr_by_EUI64(
- subnet_slaac['cidr'],
- port_mac
- ).format()
+ eui_ip = str(netutils.get_ipv6_addr_by_EUI64(
+ subnet_slaac['cidr'], port_mac))
port = self.create_port(self.network, mac_address=port_mac)
real_ips = dict([(k['subnet_id'], k['ip_address'])
for k in port['fixed_ips']])
@@ -215,7 +213,7 @@
self.assertIn(netaddr.IPAddress(real_dhcp_ip),
netaddr.IPNetwork(subnet_dhcp['cidr']), msg)
- @test.idempotent_id('4256c61d-c538-41ea-9147-3c450c36669e')
+ @decorators.idempotent_id('4256c61d-c538-41ea-9147-3c450c36669e')
def test_dhcpv6_64_subnets(self):
# NOTE: When one IPv6 subnet configured with dnsmasq SLAAC or DHCP
# stateless and other IPv4 is with DHCP of IPv4, port shall receive
@@ -237,10 +235,8 @@
self.network, ip_version=4)
subnet_slaac = self.create_subnet(self.network, **kwargs)
port_mac = data_utils.rand_mac_address()
- eui_ip = data_utils.get_ipv6_addr_by_EUI64(
- subnet_slaac['cidr'],
- port_mac
- ).format()
+ eui_ip = str(netutils.get_ipv6_addr_by_EUI64(
+ subnet_slaac['cidr'], port_mac))
port = self.create_port(self.network, mac_address=port_mac)
real_ips = dict([(k['subnet_id'], k['ip_address'])
for k in port['fixed_ips']])
@@ -258,7 +254,7 @@
self.assertIn(netaddr.IPAddress(real_dhcp_ip),
netaddr.IPNetwork(subnet_dhcp['cidr']), msg)
- @test.idempotent_id('4ab211a0-276f-4552-9070-51e27f58fecf')
+ @decorators.idempotent_id('4ab211a0-276f-4552-9070-51e27f58fecf')
def test_dhcp_stateful(self):
# NOTE: With all options below, DHCPv6 shall allocate address from
# subnet pool to port.
@@ -269,7 +265,7 @@
):
kwargs = {'ipv6_ra_mode': ra_mode,
'ipv6_address_mode': add_mode}
- kwargs = dict((k, v) for k, v in six.iteritems(kwargs) if v)
+ kwargs = dict((k, v) for k, v in kwargs.items() if v)
subnet = self.create_subnet(self.network, **kwargs)
port = self.create_port(self.network)
port_ip = next(iter(port['fixed_ips']), None)['ip_address']
@@ -279,7 +275,7 @@
self.assertIn(netaddr.IPAddress(port_ip),
netaddr.IPNetwork(subnet['cidr']), msg)
- @test.idempotent_id('51a5e97f-f02e-4e4e-9a17-a69811d300e3')
+ @decorators.idempotent_id('51a5e97f-f02e-4e4e-9a17-a69811d300e3')
def test_dhcp_stateful_fixedips(self):
# NOTE: With all options below, port shall be able to get
# requested IP from fixed IP range not depending on
@@ -291,7 +287,7 @@
):
kwargs = {'ipv6_ra_mode': ra_mode,
'ipv6_address_mode': add_mode}
- kwargs = dict((k, v) for k, v in six.iteritems(kwargs) if v)
+ kwargs = dict((k, v) for k, v in kwargs.items() if v)
subnet = self.create_subnet(self.network, **kwargs)
ip_range = netaddr.IPRange(subnet["allocation_pools"][0]["start"],
subnet["allocation_pools"][0]["end"])
@@ -307,7 +303,7 @@
"port create request: %s") % (
port_ip, ip))
- @test.idempotent_id('98244d88-d990-4570-91d4-6b25d70d08af')
+ @decorators.idempotent_id('98244d88-d990-4570-91d4-6b25d70d08af')
def test_dhcp_stateful_fixedips_outrange(self):
# NOTE: When port gets IP address from fixed IP range it
# shall be checked if it's from subnets range.
@@ -324,7 +320,7 @@
fixed_ips=[{'subnet_id': subnet['id'],
'ip_address': ip}])
- @test.idempotent_id('57b8302b-cba9-4fbb-8835-9168df029051')
+ @decorators.idempotent_id('57b8302b-cba9-4fbb-8835-9168df029051')
def test_dhcp_stateful_fixedips_duplicate(self):
# NOTE: When port gets IP address from fixed IP range it
# shall be checked if it's not duplicate.
@@ -339,24 +335,22 @@
fixed_ips=[
{'subnet_id': subnet['id'],
'ip_address': ip}])
- self.assertRaisesRegexp(lib_exc.Conflict,
- "object with that identifier already exists",
- self.create_port,
- self.network,
- fixed_ips=[{'subnet_id': subnet['id'],
- 'ip_address': ip}])
+ self.assertRaisesRegex(lib_exc.Conflict,
+ "IpAddressAlreadyAllocated|IpAddressInUse",
+ self.create_port,
+ self.network,
+ fixed_ips=[{'subnet_id': subnet['id'],
+ 'ip_address': ip}])
def _create_subnet_router(self, kwargs):
subnet = self.create_subnet(self.network, **kwargs)
- router = self.create_router(
- router_name=data_utils.rand_name("routerv6-"),
- admin_state_up=True)
+ router = self.create_router(admin_state_up=True)
port = self.create_router_interface(router['id'],
subnet['id'])
body = self.ports_client.show_port(port['port_id'])
return subnet, body['port']
- @test.idempotent_id('e98f65db-68f4-4330-9fea-abd8c5192d4d')
+ @decorators.idempotent_id('e98f65db-68f4-4330-9fea-abd8c5192d4d')
def test_dhcp_stateful_router(self):
# NOTE: With all options below the router interface shall
# receive DHCPv6 IP address from allocation pool.
@@ -366,7 +360,7 @@
):
kwargs = {'ipv6_ra_mode': ra_mode,
'ipv6_address_mode': add_mode}
- kwargs = dict((k, v) for k, v in six.iteritems(kwargs) if v)
+ kwargs = dict((k, v) for k, v in kwargs.items() if v)
subnet, port = self._create_subnet_router(kwargs)
port_ip = next(iter(port['fixed_ips']), None)['ip_address']
self._clean_network()
diff --git a/tempest/api/network/test_extensions.py b/tempest/api/network/test_extensions.py
index b83d2b0..4804ada 100644
--- a/tempest/api/network/test_extensions.py
+++ b/tempest/api/network/test_extensions.py
@@ -15,7 +15,8 @@
from tempest.api.network import base
-from tempest import test
+from tempest.common import utils
+from tempest.lib import decorators
class ExtensionsTestJSON(base.BaseNetworkTest):
@@ -23,15 +24,15 @@
List all available extensions
- v2.0 of the Neutron API is assumed. It is also assumed that the following
- options are defined in the [network] section of etc/tempest.conf:
-
+ v2.0 of the Neutron API is assumed. It is also assumed that api-extensions
+ option is defined in the [network-feature-enabled] section of
+ etc/tempest.conf.
"""
- @test.attr(type='smoke')
- @test.idempotent_id('ef28c7e6-e646-4979-9d67-deb207bc5564')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('ef28c7e6-e646-4979-9d67-deb207bc5564')
def test_list_show_extensions(self):
- # List available extensions for the tenant
+ # List available extensions for the project
expected_alias = ['security-group', 'l3_agent_scheduler',
'ext-gw-mode', 'binding', 'quotas',
'agent', 'dhcp_agent_scheduler', 'provider',
@@ -39,16 +40,17 @@
'allowed-address-pairs', 'extra_dhcp_opt',
'metering', 'dvr']
expected_alias = [ext for ext in expected_alias if
- test.is_extension_enabled(ext, 'network')]
+ utils.is_extension_enabled(ext, 'network')]
actual_alias = list()
- extensions = self.client.list_extensions()
+ extensions = self.network_extensions_client.list_extensions()
list_extensions = extensions['extensions']
# Show and verify the details of the available extensions
for ext in list_extensions:
ext_name = ext['name']
ext_alias = ext['alias']
actual_alias.append(ext['alias'])
- ext_details = self.client.show_extension(ext_alias)
+ ext_details = self.network_extensions_client.show_extension(
+ ext_alias)
ext_details = ext_details['extension']
self.assertIsNotNone(ext_details)
@@ -64,5 +66,5 @@
# of extensions returned, but only for those that have been
# enabled via configuration
for e in expected_alias:
- if test.is_extension_enabled(e, 'network'):
+ if utils.is_extension_enabled(e, 'network'):
self.assertIn(e, actual_alias)
diff --git a/tempest/api/network/test_extra_dhcp_options.py b/tempest/api/network/test_extra_dhcp_options.py
index 062bc69..0d42033 100644
--- a/tempest/api/network/test_extra_dhcp_options.py
+++ b/tempest/api/network/test_extra_dhcp_options.py
@@ -14,8 +14,9 @@
# under the License.
from tempest.api.network import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.common import utils
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
class ExtraDHCPOptionsTestJSON(base.BaseNetworkTest):
@@ -34,7 +35,7 @@
@classmethod
def skip_checks(cls):
super(ExtraDHCPOptionsTestJSON, cls).skip_checks()
- if not test.is_extension_enabled('extra_dhcp_opt', 'network'):
+ if not utils.is_extension_enabled('extra_dhcp_opt', 'network'):
msg = "Extra DHCP Options extension not enabled."
raise cls.skipException(msg)
@@ -42,19 +43,19 @@
def resource_setup(cls):
super(ExtraDHCPOptionsTestJSON, cls).resource_setup()
cls.network = cls.create_network()
- cls.subnet = cls.create_subnet(cls.network)
+ cls.create_subnet(cls.network)
cls.port = cls.create_port(cls.network)
- cls.ip_tftp = ('123.123.123.123' if cls._ip_version == 4
- else '2015::dead')
- cls.ip_server = ('123.123.123.45' if cls._ip_version == 4
- else '2015::badd')
+ ip_tftp = ('123.123.123.123' if cls._ip_version == 4
+ else '2015::dead')
+ ip_server = ('123.123.123.45' if cls._ip_version == 4
+ else '2015::badd')
cls.extra_dhcp_opts = [
{'opt_value': 'pxelinux.0', 'opt_name': 'bootfile-name'},
- {'opt_value': cls.ip_tftp, 'opt_name': 'tftp-server'},
- {'opt_value': cls.ip_server, 'opt_name': 'server-ip-address'}
+ {'opt_value': ip_tftp, 'opt_name': 'tftp-server'},
+ {'opt_value': ip_server, 'opt_name': 'server-ip-address'}
]
- @test.idempotent_id('d2c17063-3767-4a24-be4f-a23dbfa133c9')
+ @decorators.idempotent_id('d2c17063-3767-4a24-be4f-a23dbfa133c9')
def test_create_list_port_with_extra_dhcp_options(self):
# Create a port with Extra DHCP Options
body = self.ports_client.create_port(
@@ -70,11 +71,11 @@
self.assertTrue(port)
self._confirm_extra_dhcp_options(port[0], self.extra_dhcp_opts)
- @test.idempotent_id('9a6aebf4-86ee-4f47-b07a-7f7232c55607')
+ @decorators.idempotent_id('9a6aebf4-86ee-4f47-b07a-7f7232c55607')
def test_update_show_port_with_extra_dhcp_options(self):
# Update port with extra dhcp options
name = data_utils.rand_name('new-port-name')
- body = self.ports_client.update_port(
+ self.ports_client.update_port(
self.port['id'],
name=name,
extra_dhcp_opts=self.extra_dhcp_opts)
diff --git a/tempest/api/network/test_floating_ips.py b/tempest/api/network/test_floating_ips.py
index ce9c4be..b4bb88e 100644
--- a/tempest/api/network/test_floating_ips.py
+++ b/tempest/api/network/test_floating_ips.py
@@ -13,12 +13,12 @@
# License for the specific language governing permissions and limitations
# under the License.
-import netaddr
-
from tempest.api.network import base
+from tempest.common import utils
from tempest.common.utils import data_utils
+from tempest.common.utils import net_utils
from tempest import config
-from tempest import test
+from tempest.lib import decorators
CONF = config.CONF
@@ -44,9 +44,14 @@
@classmethod
def skip_checks(cls):
super(FloatingIPTestJSON, cls).skip_checks()
- if not test.is_extension_enabled('router', 'network'):
+ if not utils.is_extension_enabled('router', 'network'):
msg = "router extension not enabled."
raise cls.skipException(msg)
+ if not CONF.network.public_network_id:
+ msg = "The public_network_id option must be specified."
+ raise cls.skipException(msg)
+ if not CONF.network_feature_enabled.floating_ips:
+ raise cls.skipException("Floating ips are not available")
@classmethod
def resource_setup(cls):
@@ -55,17 +60,15 @@
# Create network, subnet, router and add interface
cls.network = cls.create_network()
- cls.subnet = cls.create_subnet(cls.network)
- cls.router = cls.create_router(data_utils.rand_name('router-'),
- external_network_id=cls.ext_net_id)
+ cls.subnet = cls.create_subnet(cls.network, enable_dhcp=False)
+ cls.router = cls.create_router(external_network_id=cls.ext_net_id)
cls.create_router_interface(cls.router['id'], cls.subnet['id'])
- cls.port = list()
# Create two ports one each for Creation and Updating of floatingIP
for i in range(2):
cls.create_port(cls.network)
- @test.attr(type='smoke')
- @test.idempotent_id('62595970-ab1c-4b7f-8fcc-fddfe55e8718')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('62595970-ab1c-4b7f-8fcc-fddfe55e8718')
def test_create_list_show_update_delete_floating_ip(self):
# Creates a floating IP
body = self.floating_ips_client.create_floatingip(
@@ -120,7 +123,7 @@
self.assertIsNone(updated_floating_ip['fixed_ip_address'])
self.assertIsNone(updated_floating_ip['router_id'])
- @test.idempotent_id('e1f6bffd-442f-4668-b30e-df13f2705e77')
+ @decorators.idempotent_id('e1f6bffd-442f-4668-b30e-df13f2705e77')
def test_floating_ip_delete_port(self):
# Create a floating IP
body = self.floating_ips_client.create_floatingip(
@@ -146,7 +149,7 @@
self.assertIsNone(shown_floating_ip['fixed_ip_address'])
self.assertIsNone(shown_floating_ip['router_id'])
- @test.idempotent_id('1bb2f731-fe5a-4b8c-8409-799ade1bed4d')
+ @decorators.idempotent_id('1bb2f731-fe5a-4b8c-8409-799ade1bed4d')
def test_floating_ip_update_different_router(self):
# Associate a floating IP to a port on a router
body = self.floating_ips_client.create_floatingip(
@@ -156,12 +159,21 @@
self.addCleanup(self.floating_ips_client.delete_floatingip,
created_floating_ip['id'])
self.assertEqual(created_floating_ip['router_id'], self.router['id'])
- network2 = self.create_network()
+ network_name = data_utils.rand_name(self.__class__.__name__)
+ network2 = self.networks_client.create_network(
+ name=network_name)['network']
+ self.addCleanup(self.networks_client.delete_network,
+ network2['id'])
subnet2 = self.create_subnet(network2)
- router2 = self.create_router(data_utils.rand_name('router-'),
- external_network_id=self.ext_net_id)
+ self.addCleanup(self.subnets_client.delete_subnet, subnet2['id'])
+ router2 = self.create_router(external_network_id=self.ext_net_id)
+ self.addCleanup(self.routers_client.delete_router, router2['id'])
self.create_router_interface(router2['id'], subnet2['id'])
+ self.addCleanup(self.routers_client.remove_router_interface,
+ router2['id'], subnet_id=subnet2['id'])
port_other_router = self.create_port(network2)
+ self.addCleanup(self.ports_client.delete_port,
+ port_other_router['id'])
# Associate floating IP to the other port on another router
floating_ip = self.floating_ips_client.update_floatingip(
created_floating_ip['id'],
@@ -172,8 +184,8 @@
port_other_router['id'])
self.assertIsNotNone(updated_floating_ip['fixed_ip_address'])
- @test.attr(type='smoke')
- @test.idempotent_id('36de4bd0-f09c-43e3-a8e1-1decc1ffd3a5')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('36de4bd0-f09c-43e3-a8e1-1decc1ffd3a5')
def test_create_floating_ip_specifying_a_fixed_ip_address(self):
body = self.floating_ips_client.create_floatingip(
floating_network_id=self.ext_net_id,
@@ -190,11 +202,15 @@
port_id=None)
self.assertIsNone(floating_ip['floatingip']['port_id'])
- @test.idempotent_id('45c4c683-ea97-41ef-9c51-5e9802f2f3d7')
+ @decorators.idempotent_id('45c4c683-ea97-41ef-9c51-5e9802f2f3d7')
def test_create_update_floatingip_with_port_multiple_ip_address(self):
# Find out ips that can be used for tests
- ips = list(netaddr.IPNetwork(self.subnet['cidr']))
- list_ips = [str(ip) for ip in ips[-3:-1]]
+ list_ips = net_utils.get_unused_ip_addresses(
+ self.ports_client,
+ self.subnets_client,
+ self.subnet['network_id'],
+ self.subnet['id'],
+ 2)
fixed_ips = [{'ip_address': list_ips[0]}, {'ip_address': list_ips[1]}]
# Create port
body = self.ports_client.create_port(network_id=self.network['id'],
diff --git a/tempest/api/network/test_floating_ips_negative.py b/tempest/api/network/test_floating_ips_negative.py
index f915615..e904a81 100644
--- a/tempest/api/network/test_floating_ips_negative.py
+++ b/tempest/api/network/test_floating_ips_negative.py
@@ -14,12 +14,11 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.network import base
-from tempest.common.utils import data_utils
+from tempest.common import utils
from tempest import config
-from tempest import test
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
CONF = config.CONF
@@ -35,9 +34,14 @@
@classmethod
def skip_checks(cls):
super(FloatingIPNegativeTestJSON, cls).skip_checks()
- if not test.is_extension_enabled('router', 'network'):
+ if not utils.is_extension_enabled('router', 'network'):
msg = "router extension not enabled."
raise cls.skipException(msg)
+ if not CONF.network.public_network_id:
+ msg = "The public_network_id option must be specified."
+ raise cls.skipException(msg)
+ if not CONF.network_feature_enabled.floating_ips:
+ raise cls.skipException("Floating ips are not available")
@classmethod
def resource_setup(cls):
@@ -45,13 +49,13 @@
cls.ext_net_id = CONF.network.public_network_id
# Create a network with a subnet connected to a router.
cls.network = cls.create_network()
- cls.subnet = cls.create_subnet(cls.network)
- cls.router = cls.create_router(data_utils.rand_name('router'))
- cls.create_router_interface(cls.router['id'], cls.subnet['id'])
+ subnet = cls.create_subnet(cls.network)
+ router = cls.create_router()
+ cls.create_router_interface(router['id'], subnet['id'])
cls.port = cls.create_port(cls.network)
- @test.attr(type=['negative'])
- @test.idempotent_id('22996ea8-4a81-4b27-b6e1-fa5df92fa5e8')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('22996ea8-4a81-4b27-b6e1-fa5df92fa5e8')
def test_create_floatingip_with_port_ext_net_unreachable(self):
self.assertRaises(
lib_exc.NotFound, self.floating_ips_client.create_floatingip,
@@ -59,8 +63,8 @@
fixed_ip_address=self.port['fixed_ips'][0]
['ip_address'])
- @test.attr(type=['negative'])
- @test.idempotent_id('50b9aeb4-9f0b-48ee-aa31-fa955a48ff54')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('50b9aeb4-9f0b-48ee-aa31-fa955a48ff54')
def test_create_floatingip_in_private_network(self):
self.assertRaises(lib_exc.BadRequest,
self.floating_ips_client.create_floatingip,
@@ -69,8 +73,8 @@
fixed_ip_address=self.port['fixed_ips'][0]
['ip_address'])
- @test.attr(type=['negative'])
- @test.idempotent_id('6b3b8797-6d43-4191-985c-c48b773eb429')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('6b3b8797-6d43-4191-985c-c48b773eb429')
def test_associate_floatingip_port_ext_net_unreachable(self):
# Create floating ip
body = self.floating_ips_client.create_floatingip(
diff --git a/tempest/api/network/test_metering_extensions.py b/tempest/api/network/test_metering_extensions.py
deleted file mode 100644
index 007ba3b..0000000
--- a/tempest/api/network/test_metering_extensions.py
+++ /dev/null
@@ -1,150 +0,0 @@
-# Copyright (C) 2014 eNovance SAS <licensing@enovance.com>
-#
-# 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_log import log as logging
-
-from tempest.api.network import base
-from tempest.common.utils import data_utils
-from tempest import test
-
-
-LOG = logging.getLogger(__name__)
-
-
-class MeteringTestJSON(base.BaseAdminNetworkTest):
- """Tests the following operations in the Neutron API:
-
- List, Show, Create, Delete Metering labels
- List, Show, Create, Delete Metering labels rules
- """
-
- @classmethod
- def skip_checks(cls):
- super(MeteringTestJSON, cls).skip_checks()
- if not test.is_extension_enabled('metering', 'network'):
- msg = "metering extension not enabled."
- raise cls.skipException(msg)
-
- @classmethod
- def resource_setup(cls):
- super(MeteringTestJSON, cls).resource_setup()
- description = "metering label created by tempest"
- name = data_utils.rand_name("metering-label")
- cls.metering_label = cls.create_metering_label(name, description)
- remote_ip_prefix = ("10.0.0.0/24" if cls._ip_version == 4
- else "fd02::/64")
- direction = "ingress"
- cls.metering_label_rule = cls.create_metering_label_rule(
- remote_ip_prefix, direction,
- metering_label_id=cls.metering_label['id'])
-
- def _delete_metering_label(self, metering_label_id):
- # Deletes a label and verifies if it is deleted or not
- self.admin_metering_labels_client.delete_metering_label(
- metering_label_id)
- # Asserting that the label is not found in list after deletion
- labels = self.admin_metering_labels_client.list_metering_labels(
- id=metering_label_id)
- self.assertEqual(len(labels['metering_labels']), 0)
-
- def _delete_metering_label_rule(self, metering_label_rule_id):
- client = self.admin_metering_label_rules_client
- # Deletes a rule and verifies if it is deleted or not
- client.delete_metering_label_rule(metering_label_rule_id)
- # Asserting that the rule is not found in list after deletion
- rules = client.list_metering_label_rules(id=metering_label_rule_id)
- self.assertEqual(len(rules['metering_label_rules']), 0)
-
- @test.idempotent_id('e2fb2f8c-45bf-429a-9f17-171c70444612')
- def test_list_metering_labels(self):
- # Verify label filtering
- body = self.admin_metering_labels_client.list_metering_labels(id=33)
- metering_labels = body['metering_labels']
- self.assertEqual(0, len(metering_labels))
-
- @test.idempotent_id('ec8e15ff-95d0-433b-b8a6-b466bddb1e50')
- def test_create_delete_metering_label_with_filters(self):
- # Creates a label
- name = data_utils.rand_name('metering-label-')
- description = "label created by tempest"
- body = self.admin_metering_labels_client.create_metering_label(
- name=name, description=description)
- metering_label = body['metering_label']
- self.addCleanup(self._delete_metering_label,
- metering_label['id'])
- # Assert whether created labels are found in labels list or fail
- # if created labels are not found in labels list
- labels = (self.admin_metering_labels_client.list_metering_labels(
- id=metering_label['id']))
- self.assertEqual(len(labels['metering_labels']), 1)
-
- @test.idempotent_id('30abb445-0eea-472e-bd02-8649f54a5968')
- def test_show_metering_label(self):
- # Verifies the details of a label
- body = self.admin_metering_labels_client.show_metering_label(
- self.metering_label['id'])
- metering_label = body['metering_label']
- self.assertEqual(self.metering_label['id'], metering_label['id'])
- self.assertEqual(self.metering_label['tenant_id'],
- metering_label['tenant_id'])
- self.assertEqual(self.metering_label['name'], metering_label['name'])
- self.assertEqual(self.metering_label['description'],
- metering_label['description'])
-
- @test.idempotent_id('cc832399-6681-493b-9d79-0202831a1281')
- def test_list_metering_label_rules(self):
- client = self.admin_metering_label_rules_client
- # Verify rule filtering
- body = client.list_metering_label_rules(id=33)
- metering_label_rules = body['metering_label_rules']
- self.assertEqual(0, len(metering_label_rules))
-
- @test.idempotent_id('f4d547cd-3aee-408f-bf36-454f8825e045')
- def test_create_delete_metering_label_rule_with_filters(self):
- # Creates a rule
- remote_ip_prefix = ("10.0.1.0/24" if self._ip_version == 4
- else "fd03::/64")
- client = self.admin_metering_label_rules_client
- body = (client.create_metering_label_rule(
- remote_ip_prefix=remote_ip_prefix,
- direction="ingress",
- metering_label_id=self.metering_label['id']))
- metering_label_rule = body['metering_label_rule']
- self.addCleanup(self._delete_metering_label_rule,
- metering_label_rule['id'])
- # Assert whether created rules are found in rules list or fail
- # if created rules are not found in rules list
- rules = client.list_metering_label_rules(id=metering_label_rule['id'])
- self.assertEqual(len(rules['metering_label_rules']), 1)
-
- @test.idempotent_id('b7354489-96ea-41f3-9452-bace120fb4a7')
- def test_show_metering_label_rule(self):
- # Verifies the details of a rule
- client = self.admin_metering_label_rules_client
- body = (client.show_metering_label_rule(
- self.metering_label_rule['id']))
- metering_label_rule = body['metering_label_rule']
- self.assertEqual(self.metering_label_rule['id'],
- metering_label_rule['id'])
- self.assertEqual(self.metering_label_rule['remote_ip_prefix'],
- metering_label_rule['remote_ip_prefix'])
- self.assertEqual(self.metering_label_rule['direction'],
- metering_label_rule['direction'])
- self.assertEqual(self.metering_label_rule['metering_label_id'],
- metering_label_rule['metering_label_id'])
- self.assertFalse(metering_label_rule['excluded'])
-
-
-class MeteringIpV6TestJSON(MeteringTestJSON):
- _ip_version = 6
diff --git a/tempest/api/network/test_networks.py b/tempest/api/network/test_networks.py
index 1c446ef..7345fd1 100644
--- a/tempest/api/network/test_networks.py
+++ b/tempest/api/network/test_networks.py
@@ -12,58 +12,29 @@
# 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 itertools
-
import netaddr
import six
-from tempest_lib import exceptions as lib_exc
+import testtools
from tempest.api.network import base
from tempest.common import custom_matchers
-from tempest.common.utils import data_utils
+from tempest.common import utils
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
CONF = config.CONF
-class NetworksTest(base.BaseNetworkTest):
- """Tests the following operations in the Neutron API:
-
- create a network for a tenant
- list tenant's networks
- show a tenant network details
- create a subnet for a tenant
- list tenant's subnets
- show a tenant subnet details
- network update
- subnet update
- delete a network also deletes its subnets
- list external networks
-
- All subnet tests are run once with ipv4 and once with ipv6.
-
- v2.0 of the Neutron API is assumed. It is also assumed that the following
- options are defined in the [network] section of etc/tempest.conf:
-
- tenant_network_cidr with a block of cidr's from which smaller blocks
- can be allocated for tenant ipv4 subnets
-
- tenant_network_v6_cidr is the equivalent for ipv6 subnets
-
- tenant_network_mask_bits with the mask bits to be used to partition the
- block defined by tenant_network_cidr
-
- tenant_network_v6_mask_bits is the equivalent for ipv6 subnets
- """
+class BaseNetworkTestResources(base.BaseNetworkTest):
@classmethod
def resource_setup(cls):
- super(NetworksTest, cls).resource_setup()
+ super(BaseNetworkTestResources, cls).resource_setup()
cls.network = cls.create_network()
- cls.name = cls.network['name']
- cls.subnet = cls._create_subnet_with_last_subnet_block(cls.network,
- cls._ip_version)
+ cls.subnet = cls._create_subnet_with_last_subnet_block(cls.network)
cls._subnet_data = {6: {'gateway':
str(cls._get_gateway_from_tempest_conf(6)),
'allocation_pools':
@@ -92,30 +63,23 @@
'new_dns_nameservers': ['7.8.8.8', '7.8.4.4']}}
@classmethod
- def _create_subnet_with_last_subnet_block(cls, network, ip_version):
- # Derive last subnet CIDR block from tenant CIDR and
+ def _create_subnet_with_last_subnet_block(cls, network):
+ # Derive last subnet CIDR block from project CIDR and
# create the subnet with that derived CIDR
- if ip_version == 4:
- cidr = netaddr.IPNetwork(CONF.network.tenant_network_cidr)
- mask_bits = CONF.network.tenant_network_mask_bits
- elif ip_version == 6:
- cidr = netaddr.IPNetwork(CONF.network.tenant_network_v6_cidr)
- mask_bits = CONF.network.tenant_network_v6_mask_bits
-
- subnet_cidr = list(cidr.subnet(mask_bits))[-1]
+ subnet_cidr = list(cls.cidr.subnet(cls.mask_bits))[-1]
gateway_ip = str(netaddr.IPAddress(subnet_cidr) + 1)
return cls.create_subnet(network, gateway=gateway_ip,
- cidr=subnet_cidr, mask_bits=mask_bits)
+ cidr=subnet_cidr, mask_bits=cls.mask_bits)
@classmethod
def _get_gateway_from_tempest_conf(cls, ip_version):
"""Return first subnet gateway for configured CIDR """
if ip_version == 4:
- cidr = netaddr.IPNetwork(CONF.network.tenant_network_cidr)
- mask_bits = CONF.network.tenant_network_mask_bits
+ cidr = netaddr.IPNetwork(CONF.network.project_network_cidr)
+ mask_bits = CONF.network.project_network_mask_bits
elif ip_version == 6:
- cidr = netaddr.IPNetwork(CONF.network.tenant_network_v6_cidr)
- mask_bits = CONF.network.tenant_network_v6_mask_bits
+ cidr = netaddr.IPNetwork(CONF.network.project_network_v6_cidr)
+ mask_bits = CONF.network.project_network_v6_mask_bits
if mask_bits >= cidr.prefixlen:
return netaddr.IPAddress(cidr) + 1
@@ -127,7 +91,7 @@
def _get_allocation_pools_from_gateway(cls, ip_version):
"""Return allocation range for subnet of given gateway"""
gateway = cls._get_gateway_from_tempest_conf(ip_version)
- return [{'start': str(gateway + 2), 'end': str(gateway + 3)}]
+ return [{'start': str(gateway + 2), 'end': str(gateway + 6)}]
def subnet_dict(self, include_keys):
# Return a subnet dict which has include_keys and their corresponding
@@ -140,15 +104,6 @@
self.assertThat(actual, custom_matchers.MatchesDictExceptForKeys(
expected, exclude_keys))
- def _delete_network(self, network):
- # Deleting network also deletes its subnets if exists
- self.networks_client.delete_network(network['id'])
- if network in self.networks:
- self.networks.remove(network)
- for subnet in self.subnets:
- if subnet['network_id'] == network['id']:
- self.subnets.remove(subnet)
-
def _create_verify_delete_subnet(self, cidr=None, mask_bits=None,
**kwargs):
network = self.create_network()
@@ -158,7 +113,7 @@
**kwargs)
compare_args_full = dict(gateway_ip=gateway, cidr=cidr,
mask_bits=mask_bits, **kwargs)
- compare_args = dict((k, v) for k, v in six.iteritems(compare_args_full)
+ compare_args = dict((k, v) for k, v in compare_args_full.items()
if v is not None)
if 'dns_nameservers' in set(subnet).intersection(compare_args):
@@ -168,16 +123,44 @@
self._compare_resource_attrs(subnet, compare_args)
self.networks_client.delete_network(net_id)
- self.networks.pop()
- self.subnets.pop()
- @test.attr(type='smoke')
- @test.idempotent_id('0e269138-0da6-4efc-a46d-578161e7b221')
+
+class NetworksTest(BaseNetworkTestResources):
+ """Tests the following operations in the Neutron API:
+
+ create a network for a project
+ list project's networks
+ show a project network details
+ create a subnet for a project
+ list project's subnets
+ show a project subnet details
+ network update
+ subnet update
+ delete a network also deletes its subnets
+ list external networks
+
+ All subnet tests are run once with ipv4 and once with ipv6.
+
+ v2.0 of the Neutron API is assumed. It is also assumed that the following
+ options are defined in the [network] section of etc/tempest.conf:
+
+ project_network_cidr with a block of cidr's from which smaller blocks
+ can be allocated for project ipv4 subnets
+
+ project_network_v6_cidr is the equivalent for ipv6 subnets
+
+ project_network_mask_bits with the mask bits to be used to partition
+ the block defined by project_network_cidr
+
+ project_network_v6_mask_bits is the equivalent for ipv6 subnets
+ """
+
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('0e269138-0da6-4efc-a46d-578161e7b221')
def test_create_update_delete_network_subnet(self):
# Create a network
- name = data_utils.rand_name('network-')
- network = self.create_network(network_name=name)
- self.addCleanup(self._delete_network, network)
+ network = self.create_network()
+ self.addCleanup(self.networks_client.delete_network, network['id'])
net_id = network['id']
self.assertEqual('ACTIVE', network['status'])
# Verify network update
@@ -194,8 +177,8 @@
updated_subnet = body['subnet']
self.assertEqual(updated_subnet['name'], new_name)
- @test.attr(type='smoke')
- @test.idempotent_id('2bf13842-c93f-4a69-83ed-717d2ec3b44e')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('2bf13842-c93f-4a69-83ed-717d2ec3b44e')
def test_show_network(self):
# Verify the details of a network
body = self.networks_client.show_network(self.network['id'])
@@ -203,19 +186,23 @@
for key in ['id', 'name']:
self.assertEqual(network[key], self.network[key])
- @test.idempotent_id('867819bb-c4b6-45f7-acf9-90edcf70aa5e')
+ @decorators.idempotent_id('867819bb-c4b6-45f7-acf9-90edcf70aa5e')
def test_show_network_fields(self):
# Verify specific fields of a network
fields = ['id', 'name']
+ if utils.is_extension_enabled('net-mtu', 'network'):
+ fields.append('mtu')
body = self.networks_client.show_network(self.network['id'],
fields=fields)
network = body['network']
self.assertEqual(sorted(network.keys()), sorted(fields))
for field_name in fields:
self.assertEqual(network[field_name], self.network[field_name])
+ self.assertNotIn('tenant_id', network)
+ self.assertNotIn('project_id', network)
- @test.attr(type='smoke')
- @test.idempotent_id('f7ffdeda-e200-4a7a-bcbe-05716e86bf43')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('f7ffdeda-e200-4a7a-bcbe-05716e86bf43')
def test_list_networks(self):
# Verify the network exists in the list of all networks
body = self.networks_client.list_networks()
@@ -223,18 +210,20 @@
if network['id'] == self.network['id']]
self.assertNotEmpty(networks, "Created network not found in the list")
- @test.idempotent_id('6ae6d24f-9194-4869-9c85-c313cb20e080')
+ @decorators.idempotent_id('6ae6d24f-9194-4869-9c85-c313cb20e080')
def test_list_networks_fields(self):
# Verify specific fields of the networks
fields = ['id', 'name']
+ if utils.is_extension_enabled('net-mtu', 'network'):
+ fields.append('mtu')
body = self.networks_client.list_networks(fields=fields)
networks = body['networks']
self.assertNotEmpty(networks, "Network list returned is empty")
for network in networks:
self.assertEqual(sorted(network.keys()), sorted(fields))
- @test.attr(type='smoke')
- @test.idempotent_id('bd635d81-6030-4dd1-b3b9-31ba0cfdf6cc')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('bd635d81-6030-4dd1-b3b9-31ba0cfdf6cc')
def test_show_subnet(self):
# Verify the details of a subnet
body = self.subnets_client.show_subnet(self.subnet['id'])
@@ -244,7 +233,7 @@
self.assertIn(key, subnet)
self.assertEqual(subnet[key], self.subnet[key])
- @test.idempotent_id('270fff0b-8bfc-411f-a184-1e8fd35286f0')
+ @decorators.idempotent_id('270fff0b-8bfc-411f-a184-1e8fd35286f0')
def test_show_subnet_fields(self):
# Verify specific fields of a subnet
fields = ['id', 'network_id']
@@ -255,8 +244,8 @@
for field_name in fields:
self.assertEqual(subnet[field_name], self.subnet[field_name])
- @test.attr(type='smoke')
- @test.idempotent_id('db68ba48-f4ea-49e9-81d1-e367f6d0b20a')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('db68ba48-f4ea-49e9-81d1-e367f6d0b20a')
def test_list_subnets(self):
# Verify the subnet exists in the list of all subnets
body = self.subnets_client.list_subnets()
@@ -264,7 +253,7 @@
if subnet['id'] == self.subnet['id']]
self.assertNotEmpty(subnets, "Created subnet not found in the list")
- @test.idempotent_id('842589e3-9663-46b0-85e4-7f01273b0412')
+ @decorators.idempotent_id('842589e3-9663-46b0-85e4-7f01273b0412')
def test_list_subnets_fields(self):
# Verify specific fields of subnets
fields = ['id', 'network_id']
@@ -274,71 +263,57 @@
for subnet in subnets:
self.assertEqual(sorted(subnet.keys()), sorted(fields))
- def _try_delete_network(self, net_id):
- # delete network, if it exists
- try:
- self.networks_client.delete_network(net_id)
- # if network is not found, this means it was deleted in the test
- except lib_exc.NotFound:
- pass
-
- @test.idempotent_id('f04f61a9-b7f3-4194-90b2-9bcf660d1bfe')
+ @decorators.idempotent_id('f04f61a9-b7f3-4194-90b2-9bcf660d1bfe')
def test_delete_network_with_subnet(self):
# Creates a network
- name = data_utils.rand_name('network-')
- body = self.networks_client.create_network(name=name)
- network = body['network']
+ network = self.create_network()
net_id = network['id']
- self.addCleanup(self._try_delete_network, net_id)
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.networks_client.delete_network, network['id'])
# Find a cidr that is not in use yet and create a subnet with it
subnet = self.create_subnet(network)
subnet_id = subnet['id']
# Delete network while the subnet still exists
- body = self.networks_client.delete_network(net_id)
+ self.networks_client.delete_network(net_id)
# Verify that the subnet got automatically deleted.
self.assertRaises(lib_exc.NotFound, self.subnets_client.show_subnet,
subnet_id)
- # Since create_subnet adds the subnet to the delete list, and it is
- # is actually deleted here - this will create and issue, hence remove
- # it from the list.
- self.subnets.pop()
-
- @test.idempotent_id('d2d596e2-8e76-47a9-ac51-d4648009f4d3')
+ @decorators.idempotent_id('d2d596e2-8e76-47a9-ac51-d4648009f4d3')
def test_create_delete_subnet_without_gateway(self):
self._create_verify_delete_subnet()
- @test.idempotent_id('9393b468-186d-496d-aa36-732348cd76e7')
+ @decorators.idempotent_id('9393b468-186d-496d-aa36-732348cd76e7')
def test_create_delete_subnet_with_gw(self):
self._create_verify_delete_subnet(
**self.subnet_dict(['gateway']))
- @test.idempotent_id('bec949c4-3147-4ba6-af5f-cd2306118404')
+ @decorators.idempotent_id('bec949c4-3147-4ba6-af5f-cd2306118404')
def test_create_delete_subnet_with_allocation_pools(self):
self._create_verify_delete_subnet(
**self.subnet_dict(['allocation_pools']))
- @test.idempotent_id('8217a149-0c6c-4cfb-93db-0486f707d13f')
+ @decorators.idempotent_id('8217a149-0c6c-4cfb-93db-0486f707d13f')
def test_create_delete_subnet_with_gw_and_allocation_pools(self):
self._create_verify_delete_subnet(**self.subnet_dict(
['gateway', 'allocation_pools']))
- @test.idempotent_id('d830de0a-be47-468f-8f02-1fd996118289')
+ @decorators.idempotent_id('d830de0a-be47-468f-8f02-1fd996118289')
def test_create_delete_subnet_with_host_routes_and_dns_nameservers(self):
self._create_verify_delete_subnet(
**self.subnet_dict(['host_routes', 'dns_nameservers']))
- @test.idempotent_id('94ce038d-ff0a-4a4c-a56b-09da3ca0b55d')
+ @decorators.idempotent_id('94ce038d-ff0a-4a4c-a56b-09da3ca0b55d')
def test_create_delete_subnet_with_dhcp_enabled(self):
self._create_verify_delete_subnet(enable_dhcp=True)
- @test.idempotent_id('3d3852eb-3009-49ec-97ac-5ce83b73010a')
+ @decorators.idempotent_id('3d3852eb-3009-49ec-97ac-5ce83b73010a')
def test_update_subnet_gw_dns_host_routes_dhcp(self):
network = self.create_network()
- self.addCleanup(self._delete_network, network)
+ self.addCleanup(self.networks_client.delete_network, network['id'])
subnet = self.create_subnet(
network, **self.subnet_dict(['gateway', 'host_routes',
@@ -368,54 +343,81 @@
self._compare_resource_attrs(updated_subnet, kwargs)
- @test.idempotent_id('a4d9ec4c-0306-4111-a75c-db01a709030b')
+ @decorators.idempotent_id('a4d9ec4c-0306-4111-a75c-db01a709030b')
def test_create_delete_subnet_all_attributes(self):
self._create_verify_delete_subnet(
enable_dhcp=True,
**self.subnet_dict(['gateway', 'host_routes', 'dns_nameservers']))
- @test.attr(type='smoke')
- @test.idempotent_id('af774677-42a9-4e4b-bb58-16fe6a5bc1ec')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('af774677-42a9-4e4b-bb58-16fe6a5bc1ec')
+ @utils.requires_ext(extension='external-net', service='network')
+ @testtools.skipUnless(CONF.network.public_network_id,
+ 'The public_network_id option must be specified.')
def test_external_network_visibility(self):
- """Verifies user can see external networks but not subnets."""
+ public_network_id = CONF.network.public_network_id
+
+ # find external network matching public_network_id
body = self.networks_client.list_networks(**{'router:external': True})
- networks = [network['id'] for network in body['networks']]
- self.assertNotEmpty(networks, "No external networks found")
+ external_network = next((network for network in body['networks']
+ if network['id'] == public_network_id), None)
+ self.assertIsNotNone(external_network, "Public network %s not found "
+ "in external network list"
+ % public_network_id)
nonexternal = [net for net in body['networks'] if
not net['router:external']]
self.assertEmpty(nonexternal, "Found non-external networks"
" in filtered list (%s)." % nonexternal)
- self.assertIn(CONF.network.public_network_id, networks)
- subnets_iter = (network['subnets']
- for network in body['networks']
- if not network['shared'])
- # subnets_iter is a list (iterator) of lists. This flattens it to a
- # list of UUIDs
- public_subnets_iter = itertools.chain(*subnets_iter)
- body = self.subnets_client.list_subnets()
- subnets = [sub['id'] for sub in body['subnets']
- if sub['id'] in public_subnets_iter]
- self.assertEmpty(subnets, "Public subnets visible")
+ # only check the public network ID because the other networks may
+ # belong to other tests and their state may have changed during this
+ # test
+ body = self.subnets_client.list_subnets(network_id=public_network_id)
+
+ # check subnet visibility of external_network
+ if external_network['shared']:
+ self.assertNotEmpty(body['subnets'], "Subnets should be visible "
+ "for shared public network %s"
+ % public_network_id)
+ else:
+ self.assertEmpty(body['subnets'], "Subnets should not be visible "
+ "for non-shared public "
+ "network %s"
+ % public_network_id)
+
+ @decorators.idempotent_id('c72c1c0c-2193-4aca-ccc4-b1442640bbbb')
+ @utils.requires_ext(extension="standard-attr-description",
+ service="network")
+ def test_create_update_network_description(self):
+ body = self.create_network(description='d1')
+ self.assertEqual('d1', body['description'])
+ net_id = body['id']
+ body = self.networks_client.list_networks(id=net_id)['networks'][0]
+ self.assertEqual('d1', body['description'])
+ body = self.networks_client.update_network(body['id'],
+ description='d2')
+ self.assertEqual('d2', body['network']['description'])
+ body = self.networks_client.list_networks(id=net_id)['networks'][0]
+ self.assertEqual('d2', body['description'])
-class BulkNetworkOpsTestJSON(base.BaseNetworkTest):
+class BulkNetworkOpsTest(base.BaseNetworkTest):
"""Tests the following operations in the Neutron API:
bulk network creation
bulk subnet creation
bulk port creation
- list tenant's networks
+ list project's networks
v2.0 of the Neutron API is assumed. It is also assumed that the following
options are defined in the [network] section of etc/tempest.conf:
- tenant_network_cidr with a block of cidr's from which smaller blocks
- can be allocated for tenant networks
+ project_network_cidr with a block of cidr's from which smaller blocks
+ can be allocated for project networks
- tenant_network_mask_bits with the mask bits to be used to partition the
- block defined by tenant-network_cidr
+ project_network_mask_bits with the mask bits to be used to partition
+ the block defined by project-network_cidr
"""
def _delete_networks(self, created_networks):
@@ -445,13 +447,13 @@
for n in created_ports:
self.assertNotIn(n['id'], ports_list)
- @test.attr(type='smoke')
- @test.idempotent_id('d4f9024d-1e28-4fc1-a6b1-25dbc6fa11e2')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('d4f9024d-1e28-4fc1-a6b1-25dbc6fa11e2')
def test_bulk_create_delete_network(self):
# Creates 2 networks in one request
network_list = [{'name': data_utils.rand_name('network-')},
{'name': data_utils.rand_name('network-')}]
- body = self.client.create_bulk_network(networks=network_list)
+ body = self.networks_client.create_bulk_networks(networks=network_list)
created_networks = body['networks']
self.addCleanup(self._delete_networks, created_networks)
# Asserting that the networks are found in the list after creation
@@ -461,19 +463,13 @@
self.assertIsNotNone(n['id'])
self.assertIn(n['id'], networks_list)
- @test.attr(type='smoke')
- @test.idempotent_id('8936533b-c0aa-4f29-8e53-6cc873aec489')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('8936533b-c0aa-4f29-8e53-6cc873aec489')
def test_bulk_create_delete_subnet(self):
networks = [self.create_network(), self.create_network()]
# Creates 2 subnets in one request
- if self._ip_version == 4:
- cidr = netaddr.IPNetwork(CONF.network.tenant_network_cidr)
- mask_bits = CONF.network.tenant_network_mask_bits
- else:
- cidr = netaddr.IPNetwork(CONF.network.tenant_network_v6_cidr)
- mask_bits = CONF.network.tenant_network_v6_mask_bits
-
- cidrs = [subnet_cidr for subnet_cidr in cidr.subnet(mask_bits)]
+ cidrs = [subnet_cidr
+ for subnet_cidr in self.cidr.subnet(self.mask_bits)]
names = [data_utils.rand_name('subnet-') for i in range(len(networks))]
subnets_list = []
@@ -486,7 +482,7 @@
}
subnets_list.append(p1)
del subnets_list[1]['name']
- body = self.client.create_bulk_subnet(subnets=subnets_list)
+ body = self.subnets_client.create_bulk_subnets(subnets=subnets_list)
created_subnets = body['subnets']
self.addCleanup(self._delete_subnets, created_subnets)
# Asserting that the subnets are found in the list after creation
@@ -496,8 +492,8 @@
self.assertIsNotNone(n['id'])
self.assertIn(n['id'], subnets_list)
- @test.attr(type='smoke')
- @test.idempotent_id('48037ff2-e889-4c3b-b86a-8e3f34d2d060')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('48037ff2-e889-4c3b-b86a-8e3f34d2d060')
def test_bulk_create_delete_port(self):
networks = [self.create_network(), self.create_network()]
# Creates 2 ports in one request
@@ -512,7 +508,7 @@
}
port_list.append(p1)
del port_list[1]['name']
- body = self.client.create_bulk_port(ports=port_list)
+ body = self.ports_client.create_bulk_ports(ports=port_list)
created_ports = body['ports']
self.addCleanup(self._delete_ports, created_ports)
# Asserting that the ports are found in the list after creation
@@ -523,37 +519,34 @@
self.assertIn(n['id'], ports_list)
-class BulkNetworkOpsIpV6TestJSON(BulkNetworkOpsTestJSON):
+class BulkNetworkOpsIpV6Test(BulkNetworkOpsTest):
_ip_version = 6
-class NetworksIpV6TestJSON(NetworksTest):
+class NetworksIpV6Test(NetworksTest):
_ip_version = 6
- @test.idempotent_id('e41a4888-65a6-418c-a095-f7c2ef4ad59a')
+ @decorators.idempotent_id('e41a4888-65a6-418c-a095-f7c2ef4ad59a')
def test_create_delete_subnet_with_gw(self):
- net = netaddr.IPNetwork(CONF.network.tenant_network_v6_cidr)
+ net = netaddr.IPNetwork(CONF.network.project_network_v6_cidr)
gateway = str(netaddr.IPAddress(net.first + 2))
- name = data_utils.rand_name('network-')
- network = self.create_network(network_name=name)
+ network = self.create_network()
subnet = self.create_subnet(network, gateway)
# Verifies Subnet GW in IPv6
self.assertEqual(subnet['gateway_ip'], gateway)
- @test.idempotent_id('ebb4fd95-524f-46af-83c1-0305b239338f')
+ @decorators.idempotent_id('ebb4fd95-524f-46af-83c1-0305b239338f')
def test_create_delete_subnet_with_default_gw(self):
- net = netaddr.IPNetwork(CONF.network.tenant_network_v6_cidr)
+ net = netaddr.IPNetwork(CONF.network.project_network_v6_cidr)
gateway_ip = str(netaddr.IPAddress(net.first + 1))
- name = data_utils.rand_name('network-')
- network = self.create_network(network_name=name)
+ network = self.create_network()
subnet = self.create_subnet(network)
# Verifies Subnet GW in IPv6
self.assertEqual(subnet['gateway_ip'], gateway_ip)
- @test.idempotent_id('a9653883-b2a4-469b-8c3c-4518430a7e55')
+ @decorators.idempotent_id('a9653883-b2a4-469b-8c3c-4518430a7e55')
def test_create_list_subnet_with_no_gw64_one_network(self):
- name = data_utils.rand_name('network-')
- network = self.create_network(name)
+ network = self.create_network()
ipv6_gateway = self.subnet_dict(['gateway'])['gateway']
subnet1 = self.create_subnet(network,
ip_version=6,
@@ -568,18 +561,20 @@
# Verifies Subnet GW is set in IPv6
self.assertEqual(subnet1['gateway_ip'], ipv6_gateway)
# Verifies Subnet GW is None in IPv4
- self.assertEqual(subnet2['gateway_ip'], None)
+ self.assertIsNone(subnet2['gateway_ip'])
# Verifies all 2 subnets in the same network
body = self.subnets_client.list_subnets()
subnets = [sub['id'] for sub in body['subnets']
if sub['network_id'] == network['id']]
test_subnet_ids = [sub['id'] for sub in (subnet1, subnet2)]
- self.assertItemsEqual(subnets,
- test_subnet_ids,
- 'Subnet are not in the same network')
+ six.assertCountEqual(self, subnets,
+ test_subnet_ids,
+ 'Subnet are not in the same network')
-class NetworksIpV6TestAttrs(NetworksIpV6TestJSON):
+class NetworksIpV6TestAttrs(BaseNetworkTestResources):
+
+ _ip_version = 6
@classmethod
def skip_checks(cls):
@@ -588,20 +583,20 @@
raise cls.skipException("IPv6 extended attributes for "
"subnets not available")
- @test.idempotent_id('da40cd1b-a833-4354-9a85-cd9b8a3b74ca')
+ @decorators.idempotent_id('da40cd1b-a833-4354-9a85-cd9b8a3b74ca')
def test_create_delete_subnet_with_v6_attributes_stateful(self):
self._create_verify_delete_subnet(
gateway=self._subnet_data[self._ip_version]['gateway'],
ipv6_ra_mode='dhcpv6-stateful',
ipv6_address_mode='dhcpv6-stateful')
- @test.idempotent_id('176b030f-a923-4040-a755-9dc94329e60c')
+ @decorators.idempotent_id('176b030f-a923-4040-a755-9dc94329e60c')
def test_create_delete_subnet_with_v6_attributes_slaac(self):
self._create_verify_delete_subnet(
ipv6_ra_mode='slaac',
ipv6_address_mode='slaac')
- @test.idempotent_id('7d410310-8c86-4902-adf9-865d08e31adb')
+ @decorators.idempotent_id('7d410310-8c86-4902-adf9-865d08e31adb')
def test_create_delete_subnet_with_v6_attributes_stateless(self):
self._create_verify_delete_subnet(
ipv6_ra_mode='dhcpv6-stateless',
@@ -616,18 +611,17 @@
port = self.create_port(slaac_network)
self.assertIsNotNone(port['fixed_ips'][0]['ip_address'])
self.subnets_client.delete_subnet(subnet_slaac['id'])
- self.subnets.pop()
subnets = self.subnets_client.list_subnets()
subnet_ids = [subnet['id'] for subnet in subnets['subnets']]
self.assertNotIn(subnet_slaac['id'], subnet_ids,
"Subnet wasn't deleted")
- self.assertRaisesRegexp(
+ self.assertRaisesRegex(
lib_exc.Conflict,
"There are one or more ports still in use on the network",
self.networks_client.delete_network,
slaac_network['id'])
- @test.idempotent_id('88554555-ebf8-41ef-9300-4926d45e06e9')
+ @decorators.idempotent_id('88554555-ebf8-41ef-9300-4926d45e06e9')
def test_create_delete_slaac_subnet_with_ports(self):
"""Test deleting subnet with SLAAC ports
@@ -637,7 +631,7 @@
"""
self._test_delete_subnet_with_ports("slaac")
- @test.idempotent_id('2de6ab5a-fcf0-4144-9813-f91a940291f1')
+ @decorators.idempotent_id('2de6ab5a-fcf0-4144-9813-f91a940291f1')
def test_create_delete_stateless_subnet_with_ports(self):
"""Test deleting subnet with DHCPv6 stateless ports
diff --git a/tempest/api/network/test_networks_negative.py b/tempest/api/network/test_networks_negative.py
index 0ef96a6..bc4f41f 100644
--- a/tempest/api/network/test_networks_negative.py
+++ b/tempest/api/network/test_networks_negative.py
@@ -14,83 +14,82 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.network import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class NetworksNegativeTestJSON(base.BaseNetworkTest):
- @test.attr(type=['negative'])
- @test.idempotent_id('9293e937-824d-42d2-8d5b-e985ea67002a')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('9293e937-824d-42d2-8d5b-e985ea67002a')
def test_show_non_existent_network(self):
non_exist_id = data_utils.rand_uuid()
self.assertRaises(lib_exc.NotFound, self.networks_client.show_network,
non_exist_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('d746b40c-5e09-4043-99f7-cba1be8b70df')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('d746b40c-5e09-4043-99f7-cba1be8b70df')
def test_show_non_existent_subnet(self):
non_exist_id = data_utils.rand_uuid()
self.assertRaises(lib_exc.NotFound, self.subnets_client.show_subnet,
non_exist_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('a954861d-cbfd-44e8-b0a9-7fab111f235d')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('a954861d-cbfd-44e8-b0a9-7fab111f235d')
def test_show_non_existent_port(self):
non_exist_id = data_utils.rand_uuid()
self.assertRaises(lib_exc.NotFound, self.ports_client.show_port,
non_exist_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('98bfe4e3-574e-4012-8b17-b2647063de87')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('98bfe4e3-574e-4012-8b17-b2647063de87')
def test_update_non_existent_network(self):
non_exist_id = data_utils.rand_uuid()
self.assertRaises(
lib_exc.NotFound, self.networks_client.update_network,
non_exist_id, name="new_name")
- @test.attr(type=['negative'])
- @test.idempotent_id('03795047-4a94-4120-a0a1-bd376e36fd4e')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('03795047-4a94-4120-a0a1-bd376e36fd4e')
def test_delete_non_existent_network(self):
non_exist_id = data_utils.rand_uuid()
self.assertRaises(lib_exc.NotFound,
self.networks_client.delete_network,
non_exist_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('1cc47884-ac52-4415-a31c-e7ce5474a868')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('1cc47884-ac52-4415-a31c-e7ce5474a868')
def test_update_non_existent_subnet(self):
non_exist_id = data_utils.rand_uuid()
self.assertRaises(lib_exc.NotFound, self.subnets_client.update_subnet,
non_exist_id, name='new_name')
- @test.attr(type=['negative'])
- @test.idempotent_id('a176c859-99fb-42ec-a208-8a85b552a239')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('a176c859-99fb-42ec-a208-8a85b552a239')
def test_delete_non_existent_subnet(self):
non_exist_id = data_utils.rand_uuid()
self.assertRaises(lib_exc.NotFound,
self.subnets_client.delete_subnet, non_exist_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('13d3b106-47e6-4b9b-8d53-dae947f092fe')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('13d3b106-47e6-4b9b-8d53-dae947f092fe')
def test_create_port_on_non_existent_network(self):
non_exist_net_id = data_utils.rand_uuid()
self.assertRaises(lib_exc.NotFound,
self.ports_client.create_port,
network_id=non_exist_net_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('cf8eef21-4351-4f53-adcd-cc5cb1e76b92')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('cf8eef21-4351-4f53-adcd-cc5cb1e76b92')
def test_update_non_existent_port(self):
non_exist_port_id = data_utils.rand_uuid()
self.assertRaises(lib_exc.NotFound, self.ports_client.update_port,
non_exist_port_id, name='new_name')
- @test.attr(type=['negative'])
- @test.idempotent_id('49ec2bbd-ac2e-46fd-8054-798e679ff894')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('49ec2bbd-ac2e-46fd-8054-798e679ff894')
def test_delete_non_existent_port(self):
non_exist_port_id = data_utils.rand_uuid()
self.assertRaises(lib_exc.NotFound,
diff --git a/tempest/api/network/test_ports.py b/tempest/api/network/test_ports.py
index 67f2c83..246a5c3 100644
--- a/tempest/api/network/test_ports.py
+++ b/tempest/api/network/test_ports.py
@@ -13,16 +13,19 @@
# License for the specific language governing permissions and limitations
# under the License.
-import socket
+import ipaddress
import netaddr
+import six
+import testtools
-from tempest.api.network import base
from tempest.api.network import base_security_groups as sec_base
from tempest.common import custom_matchers
-from tempest.common.utils import data_utils
+from tempest.common import utils
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions
CONF = config.CONF
@@ -49,8 +52,23 @@
ports_list = body['ports']
self.assertFalse(port_id in [n['id'] for n in ports_list])
- @test.attr(type='smoke')
- @test.idempotent_id('c72c1c0c-2193-4aca-aaa4-b1442640f51c')
+ def _create_subnet(self, network, gateway='',
+ cidr=None, mask_bits=None, **kwargs):
+ subnet = self.create_subnet(network, gateway, cidr, mask_bits)
+ self.addCleanup(self.subnets_client.delete_subnet, subnet['id'])
+ return subnet
+
+ def _create_network(self, network_name=None, **kwargs):
+ network_name = network_name or data_utils.rand_name(
+ self.__class__.__name__)
+ network = self.networks_client.create_network(
+ name=network_name, **kwargs)['network']
+ self.addCleanup(self.networks_client.delete_network,
+ network['id'])
+ return network
+
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('c72c1c0c-2193-4aca-aaa4-b1442640f51c')
def test_create_update_delete_port(self):
# Verify port creation
body = self.ports_client.create_port(network_id=self.network['id'])
@@ -67,14 +85,13 @@
self.assertEqual(updated_port['name'], new_name)
self.assertFalse(updated_port['admin_state_up'])
- @test.idempotent_id('67f1b811-f8db-43e2-86bd-72c074d4a42c')
+ @decorators.idempotent_id('67f1b811-f8db-43e2-86bd-72c074d4a42c')
def test_create_bulk_port(self):
network1 = self.network
- name = data_utils.rand_name('network-')
- network2 = self.create_network(network_name=name)
+ network2 = self._create_network()
network_list = [network1['id'], network2['id']]
port_list = [{'network_id': net_id} for net_id in network_list]
- body = self.client.create_bulk_port(ports=port_list)
+ body = self.ports_client.create_bulk_ports(ports=port_list)
created_ports = body['ports']
port1 = created_ports[0]
port2 = created_ports[1]
@@ -85,27 +102,22 @@
self.assertTrue(port1['admin_state_up'])
self.assertTrue(port2['admin_state_up'])
- @classmethod
- def _get_ipaddress_from_tempest_conf(cls):
- """Return first subnet gateway for configured CIDR """
- if cls._ip_version == 4:
- cidr = netaddr.IPNetwork(CONF.network.tenant_network_cidr)
-
- elif cls._ip_version == 6:
- cidr = netaddr.IPNetwork(CONF.network.tenant_network_v6_cidr)
-
- return netaddr.IPAddress(cidr)
-
- @test.attr(type='smoke')
- @test.idempotent_id('0435f278-40ae-48cb-a404-b8a087bc09b1')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('0435f278-40ae-48cb-a404-b8a087bc09b1')
def test_create_port_in_allowed_allocation_pools(self):
- network = self.create_network()
+ network = self._create_network()
net_id = network['id']
- address = self._get_ipaddress_from_tempest_conf()
- allocation_pools = {'allocation_pools': [{'start': str(address + 4),
- 'end': str(address + 6)}]}
- subnet = self.create_subnet(network, **allocation_pools)
- self.addCleanup(self.subnets_client.delete_subnet, subnet['id'])
+ address = self.cidr
+ address.prefixlen = self.mask_bits
+ if ((address.version == 4 and address.prefixlen >= 30) or
+ (address.version == 6 and address.prefixlen >= 126)):
+ msg = ("Subnet %s isn't large enough for the test" % address.cidr)
+ raise exceptions.InvalidConfiguration(msg)
+ allocation_pools = {'allocation_pools': [{'start': str(address[2]),
+ 'end': str(address[-2])}]}
+ self._create_subnet(network, cidr=address,
+ mask_bits=address.prefixlen,
+ **allocation_pools)
body = self.ports_client.create_port(network_id=net_id)
self.addCleanup(self.ports_client.delete_port, body['port']['id'])
port = body['port']
@@ -115,21 +127,24 @@
ip_range = netaddr.IPRange(start_ip_address, end_ip_address)
self.assertIn(ip_address, ip_range)
- @test.attr(type='smoke')
- @test.idempotent_id('c9a685bd-e83f-499c-939f-9f7863ca259f')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('c9a685bd-e83f-499c-939f-9f7863ca259f')
def test_show_port(self):
# Verify the details of port
body = self.ports_client.show_port(self.port['id'])
port = body['port']
self.assertIn('id', port)
- # TODO(Santosh)- This is a temporary workaround to compare create_port
- # and show_port dict elements.Remove this once extra_dhcp_opts issue
- # gets fixed in neutron.( bug - 1365341.)
+ # NOTE(rfolco): created_at and updated_at may get inconsistent values
+ # due to possible delay between POST request and resource creation.
+ # TODO(rfolco): Neutron Bug #1365341 is fixed, can remove the key
+ # extra_dhcp_opts in the O release (K/L gate jobs still need it).
self.assertThat(self.port,
custom_matchers.MatchesDictExceptForKeys
- (port, excluded_keys=['extra_dhcp_opts']))
+ (port, excluded_keys=['extra_dhcp_opts',
+ 'created_at',
+ 'updated_at']))
- @test.idempotent_id('45fcdaf2-dab0-4c13-ac6c-fcddfb579dbd')
+ @decorators.idempotent_id('45fcdaf2-dab0-4c13-ac6c-fcddfb579dbd')
def test_show_port_fields(self):
# Verify specific fields of a port
fields = ['id', 'mac_address']
@@ -140,8 +155,8 @@
for field_name in fields:
self.assertEqual(port[field_name], self.port[field_name])
- @test.attr(type='smoke')
- @test.idempotent_id('cf95b358-3e92-4a29-a148-52445e1ac50e')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('cf95b358-3e92-4a29-a148-52445e1ac50e')
def test_list_ports(self):
# Verify the port exists in the list of all ports
body = self.ports_client.list_ports()
@@ -149,12 +164,11 @@
if port['id'] == self.port['id']]
self.assertNotEmpty(ports, "Created port not found in the list")
- @test.idempotent_id('e7fe260b-1e79-4dd3-86d9-bec6a7959fc5')
+ @decorators.idempotent_id('e7fe260b-1e79-4dd3-86d9-bec6a7959fc5')
def test_port_list_filter_by_ip(self):
# Create network and subnet
- network = self.create_network()
- subnet = self.create_subnet(network)
- self.addCleanup(self.subnets_client.delete_subnet, subnet['id'])
+ network = self._create_network()
+ self._create_subnet(network)
# Create two ports
port_1 = self.ports_client.create_port(network_id=network['id'])
self.addCleanup(self.ports_client.delete_port, port_1['port']['id'])
@@ -180,21 +194,94 @@
self.assertIn(port_1_fixed_ip, port_ips)
self.assertIn(network['id'], port_net_ids)
- @test.idempotent_id('5ad01ed0-0e6e-4c5d-8194-232801b15c72')
+ @decorators.idempotent_id('79895408-85d5-460d-94e7-9531c5fd9123')
+ @testtools.skipUnless(
+ utils.is_extension_enabled('ip-substring-filtering', 'network'),
+ 'ip-substring-filtering extension not enabled.')
+ def test_port_list_filter_by_ip_substr(self):
+ # Create network and subnet
+ network = self._create_network()
+ subnet = self._create_subnet(network)
+ # Get two IP addresses
+ ip_address_1 = None
+ ip_address_2 = None
+ ip_network = ipaddress.ip_network(six.text_type(subnet['cidr']))
+ for ip in ip_network:
+ if ip == ip_network.network_address:
+ continue
+ if ip_address_1 is None:
+ ip_address_1 = six.text_type(ip)
+ else:
+ ip_address_2 = ip_address_1
+ ip_address_1 = six.text_type(ip)
+ # Make sure these two IP addresses have different substring
+ if ip_address_1[:-1] != ip_address_2[:-1]:
+ break
+
+ # Create two ports
+ fixed_ips = [{'subnet_id': subnet['id'], 'ip_address': ip_address_1}]
+ port_1 = self.ports_client.create_port(network_id=network['id'],
+ fixed_ips=fixed_ips)
+ self.addCleanup(self.ports_client.delete_port, port_1['port']['id'])
+ fixed_ips = [{'subnet_id': subnet['id'], 'ip_address': ip_address_2}]
+ port_2 = self.ports_client.create_port(network_id=network['id'],
+ fixed_ips=fixed_ips)
+ self.addCleanup(self.ports_client.delete_port, port_2['port']['id'])
+
+ # Scenario 1: List port1 (port2 is filtered out)
+ if ip_address_1[:-1] != ip_address_2[:-1]:
+ ips_filter = 'ip_address_substr=' + ip_address_1[:-1]
+ else:
+ ips_filter = 'ip_address_substr=' + ip_address_1
+ ports = self.ports_client.list_ports(fixed_ips=ips_filter)['ports']
+ # Check that we got the desired port
+ port_ids = [port['id'] for port in ports]
+ fixed_ips = [port['fixed_ips'] for port in ports]
+ port_ips = []
+ for addr in fixed_ips:
+ port_ips.extend([a['ip_address'] for a in addr])
+
+ port_net_ids = [port['network_id'] for port in ports]
+ self.assertIn(network['id'], port_net_ids)
+ self.assertIn(port_1['port']['id'], port_ids)
+ self.assertIn(port_1['port']['fixed_ips'][0]['ip_address'], port_ips)
+ self.assertNotIn(port_2['port']['id'], port_ids)
+ self.assertNotIn(
+ port_2['port']['fixed_ips'][0]['ip_address'], port_ips)
+
+ # Scenario 2: List both port1 and port2
+ substr = ip_address_1
+ while substr not in ip_address_2:
+ substr = substr[:-1]
+ ips_filter = 'ip_address_substr=' + substr
+ ports = self.ports_client.list_ports(fixed_ips=ips_filter)['ports']
+ # Check that we got both port
+ port_ids = [port['id'] for port in ports]
+ fixed_ips = [port['fixed_ips'] for port in ports]
+ port_ips = []
+ for addr in fixed_ips:
+ port_ips.extend([a['ip_address'] for a in addr])
+
+ port_net_ids = [port['network_id'] for port in ports]
+ self.assertIn(network['id'], port_net_ids)
+ self.assertIn(port_1['port']['id'], port_ids)
+ self.assertIn(port_1['port']['fixed_ips'][0]['ip_address'], port_ips)
+ self.assertIn(port_2['port']['id'], port_ids)
+ self.assertIn(port_2['port']['fixed_ips'][0]['ip_address'], port_ips)
+
+ @decorators.idempotent_id('5ad01ed0-0e6e-4c5d-8194-232801b15c72')
def test_port_list_filter_by_router_id(self):
# Create a router
- network = self.create_network()
- self.addCleanup(self.networks_client.delete_network, network['id'])
- subnet = self.create_subnet(network)
- self.addCleanup(self.subnets_client.delete_subnet, subnet['id'])
- router = self.create_router(data_utils.rand_name('router-'))
- self.addCleanup(self.client.delete_router, router['id'])
+ network = self._create_network()
+ self._create_subnet(network)
+ router = self.create_router()
+ self.addCleanup(self.routers_client.delete_router, router['id'])
port = self.ports_client.create_port(network_id=network['id'])
# Add router interface to port created above
- self.client.add_router_interface_with_port_id(
- router['id'], port['port']['id'])
- self.addCleanup(self.client.remove_router_interface_with_port_id,
- router['id'], port['port']['id'])
+ self.routers_client.add_router_interface(router['id'],
+ port_id=port['port']['id'])
+ self.addCleanup(self.routers_client.remove_router_interface,
+ router['id'], port_id=port['port']['id'])
# List ports filtered by router_id
port_list = self.ports_client.list_ports(device_id=router['id'])
ports = port_list['ports']
@@ -202,7 +289,7 @@
self.assertEqual(ports[0]['id'], port['port']['id'])
self.assertEqual(ports[0]['device_id'], router['id'])
- @test.idempotent_id('ff7f117f-f034-4e0e-abff-ccef05c454b4')
+ @decorators.idempotent_id('ff7f117f-f034-4e0e-abff-ccef05c454b4')
def test_list_ports_fields(self):
# Verify specific fields of ports
fields = ['id', 'mac_address']
@@ -213,15 +300,12 @@
for port in ports:
self.assertEqual(sorted(fields), sorted(port.keys()))
- @test.idempotent_id('63aeadd4-3b49-427f-a3b1-19ca81f06270')
+ @decorators.idempotent_id('63aeadd4-3b49-427f-a3b1-19ca81f06270')
def test_create_update_port_with_second_ip(self):
# Create a network with two subnets
- network = self.create_network()
- self.addCleanup(self.networks_client.delete_network, network['id'])
- subnet_1 = self.create_subnet(network)
- self.addCleanup(self.subnets_client.delete_subnet, subnet_1['id'])
- subnet_2 = self.create_subnet(network)
- self.addCleanup(self.subnets_client.delete_subnet, subnet_2['id'])
+ network = self._create_network()
+ subnet_1 = self._create_subnet(network)
+ subnet_2 = self._create_subnet(network)
fixed_ip_1 = [{'subnet_id': subnet_1['id']}]
fixed_ip_2 = [{'subnet_id': subnet_2['id']}]
@@ -245,8 +329,7 @@
self.assertEqual(2, len(port['fixed_ips']))
def _update_port_with_security_groups(self, security_groups_names):
- subnet_1 = self.create_subnet(self.network)
- self.addCleanup(self.subnets_client.delete_subnet, subnet_1['id'])
+ subnet_1 = self._create_subnet(self.network)
fixed_ip_1 = [{'subnet_id': subnet_1['id']}]
security_groups_list = list()
@@ -295,18 +378,24 @@
for security_group in security_groups_list:
self.assertIn(security_group, port_show['security_groups'])
- @test.idempotent_id('58091b66-4ff4-4cc1-a549-05d60c7acd1a')
+ @decorators.idempotent_id('58091b66-4ff4-4cc1-a549-05d60c7acd1a')
+ @testtools.skipUnless(
+ utils.is_extension_enabled('security-group', 'network'),
+ 'security-group extension not enabled.')
def test_update_port_with_security_group_and_extra_attributes(self):
self._update_port_with_security_groups(
[data_utils.rand_name('secgroup')])
- @test.idempotent_id('edf6766d-3d40-4621-bc6e-2521a44c257d')
+ @decorators.idempotent_id('edf6766d-3d40-4621-bc6e-2521a44c257d')
+ @testtools.skipUnless(
+ utils.is_extension_enabled('security-group', 'network'),
+ 'security-group extension not enabled.')
def test_update_port_with_two_security_groups_and_extra_attributes(self):
self._update_port_with_security_groups(
[data_utils.rand_name('secgroup'),
data_utils.rand_name('secgroup')])
- @test.idempotent_id('13e95171-6cbd-489c-9d7c-3f9c58215c18')
+ @decorators.idempotent_id('13e95171-6cbd-489c-9d7c-3f9c58215c18')
def test_create_show_delete_port_user_defined_mac(self):
# Create a port for a legal mac
body = self.ports_client.create_port(network_id=self.network['id'])
@@ -323,104 +412,19 @@
self.assertEqual(free_mac_address,
show_port['mac_address'])
- @test.attr(type='smoke')
- @test.idempotent_id('4179dcb9-1382-4ced-84fe-1b91c54f5735')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('4179dcb9-1382-4ced-84fe-1b91c54f5735')
+ @testtools.skipUnless(
+ utils.is_extension_enabled('security-group', 'network'),
+ 'security-group extension not enabled.')
def test_create_port_with_no_securitygroups(self):
- network = self.create_network()
- self.addCleanup(self.networks_client.delete_network, network['id'])
- subnet = self.create_subnet(network)
- self.addCleanup(self.subnets_client.delete_subnet, subnet['id'])
+ network = self._create_network()
+ self._create_subnet(network)
port = self.create_port(network, security_groups=[])
self.addCleanup(self.ports_client.delete_port, port['id'])
self.assertIsNotNone(port['security_groups'])
self.assertEmpty(port['security_groups'])
-class PortsAdminExtendedAttrsTestJSON(base.BaseAdminNetworkTest):
-
- @classmethod
- def setup_clients(cls):
- super(PortsAdminExtendedAttrsTestJSON, cls).setup_clients()
- cls.identity_client = cls.os_adm.identity_client
-
- @classmethod
- def resource_setup(cls):
- super(PortsAdminExtendedAttrsTestJSON, cls).resource_setup()
- cls.network = cls.create_network()
- cls.host_id = socket.gethostname()
-
- @test.idempotent_id('8e8569c1-9ac7-44db-8bc1-f5fb2814f29b')
- def test_create_port_binding_ext_attr(self):
- post_body = {"network_id": self.network['id'],
- "binding:host_id": self.host_id}
- body = self.admin_ports_client.create_port(**post_body)
- port = body['port']
- self.addCleanup(self.admin_ports_client.delete_port, port['id'])
- host_id = port['binding:host_id']
- self.assertIsNotNone(host_id)
- self.assertEqual(self.host_id, host_id)
-
- @test.idempotent_id('6f6c412c-711f-444d-8502-0ac30fbf5dd5')
- def test_update_port_binding_ext_attr(self):
- post_body = {"network_id": self.network['id']}
- body = self.admin_ports_client.create_port(**post_body)
- port = body['port']
- self.addCleanup(self.admin_ports_client.delete_port, port['id'])
- update_body = {"binding:host_id": self.host_id}
- body = self.admin_ports_client.update_port(port['id'], **update_body)
- updated_port = body['port']
- host_id = updated_port['binding:host_id']
- self.assertIsNotNone(host_id)
- self.assertEqual(self.host_id, host_id)
-
- @test.idempotent_id('1c82a44a-6c6e-48ff-89e1-abe7eaf8f9f8')
- def test_list_ports_binding_ext_attr(self):
- # Create a new port
- post_body = {"network_id": self.network['id']}
- body = self.admin_ports_client.create_port(**post_body)
- port = body['port']
- self.addCleanup(self.admin_ports_client.delete_port, port['id'])
-
- # Update the port's binding attributes so that is now 'bound'
- # to a host
- update_body = {"binding:host_id": self.host_id}
- self.admin_ports_client.update_port(port['id'], **update_body)
-
- # List all ports, ensure new port is part of list and its binding
- # attributes are set and accurate
- body = self.admin_ports_client.list_ports()
- ports_list = body['ports']
- pids_list = [p['id'] for p in ports_list]
- self.assertIn(port['id'], pids_list)
- listed_port = [p for p in ports_list if p['id'] == port['id']]
- self.assertEqual(1, len(listed_port),
- 'Multiple ports listed with id %s in ports listing: '
- '%s' % (port['id'], ports_list))
- self.assertEqual(self.host_id, listed_port[0]['binding:host_id'])
-
- @test.idempotent_id('b54ac0ff-35fc-4c79-9ca3-c7dbd4ea4f13')
- def test_show_port_binding_ext_attr(self):
- body = self.admin_ports_client.create_port(
- network_id=self.network['id'])
- port = body['port']
- self.addCleanup(self.admin_ports_client.delete_port, port['id'])
- body = self.admin_ports_client.show_port(port['id'])
- show_port = body['port']
- self.assertEqual(port['binding:host_id'],
- show_port['binding:host_id'])
- self.assertEqual(port['binding:vif_type'],
- show_port['binding:vif_type'])
- self.assertEqual(port['binding:vif_details'],
- show_port['binding:vif_details'])
-
-
class PortsIpV6TestJSON(PortsTestJSON):
_ip_version = 6
- _tenant_network_cidr = CONF.network.tenant_network_v6_cidr
- _tenant_network_mask_bits = CONF.network.tenant_network_v6_mask_bits
-
-
-class PortsAdminExtendedAttrsIpV6TestJSON(PortsAdminExtendedAttrsTestJSON):
- _ip_version = 6
- _tenant_network_cidr = CONF.network.tenant_network_v6_cidr
- _tenant_network_mask_bits = CONF.network.tenant_network_v6_mask_bits
diff --git a/tempest/api/network/test_routers.py b/tempest/api/network/test_routers.py
index 406ad44..3ff12e4 100644
--- a/tempest/api/network/test_routers.py
+++ b/tempest/api/network/test_routers.py
@@ -14,131 +14,104 @@
# under the License.
import netaddr
-import six
+import testtools
-from tempest.api.network import base_routers as base
-from tempest.common.utils import data_utils
+from tempest.api.network import base
+from tempest.common import utils
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
CONF = config.CONF
-class RoutersTest(base.BaseRouterTest):
+class RoutersTest(base.BaseNetworkTest):
+
+ def _cleanup_router(self, router):
+ self.delete_router(router)
+ self.routers.remove(router)
+
+ def _create_router(self, name=None, admin_state_up=False,
+ external_network_id=None, enable_snat=None):
+ # associate a cleanup with created routers to avoid quota limits
+ router = self.create_router(name, admin_state_up,
+ external_network_id, enable_snat)
+ self.addCleanup(self._cleanup_router, router)
+ return router
+
+ def _create_subnet(self, network, gateway='', cidr=None):
+ subnet = self.create_subnet(network, gateway, cidr)
+ self.addCleanup(self.subnets_client.delete_subnet, subnet['id'])
+ return subnet
+
+ def _add_router_interface_with_subnet_id(self, router_id, subnet_id):
+ interface = self.routers_client.add_router_interface(
+ router_id, subnet_id=subnet_id)
+ self.addCleanup(self._remove_router_interface_with_subnet_id,
+ router_id, subnet_id)
+ self.assertEqual(subnet_id, interface['subnet_id'])
+ return interface
+
+ def _remove_router_interface_with_subnet_id(self, router_id, subnet_id):
+ body = self.routers_client.remove_router_interface(router_id,
+ subnet_id=subnet_id)
+ self.assertEqual(subnet_id, body['subnet_id'])
@classmethod
def skip_checks(cls):
super(RoutersTest, cls).skip_checks()
- if not test.is_extension_enabled('router', 'network'):
+ if not utils.is_extension_enabled('router', 'network'):
msg = "router extension not enabled."
raise cls.skipException(msg)
- @classmethod
- def setup_clients(cls):
- super(RoutersTest, cls).setup_clients()
- cls.identity_admin_client = cls.os_adm.identity_client
-
- @classmethod
- def resource_setup(cls):
- super(RoutersTest, cls).resource_setup()
- cls.tenant_cidr = (CONF.network.tenant_network_cidr
- if cls._ip_version == 4 else
- CONF.network.tenant_network_v6_cidr)
-
- @test.attr(type='smoke')
- @test.idempotent_id('f64403e2-8483-4b34-8ccd-b09a87bcc68c')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('f64403e2-8483-4b34-8ccd-b09a87bcc68c')
+ @testtools.skipUnless(CONF.network.public_network_id,
+ 'The public_network_id option must be specified.')
def test_create_show_list_update_delete_router(self):
# Create a router
- # NOTE(salv-orlando): Do not invoke self.create_router
- # as we need to check the response code
- name = data_utils.rand_name('router-')
- create_body = self.client.create_router(
- name, external_gateway_info={
- "network_id": CONF.network.public_network_id},
- admin_state_up=False)
- self.addCleanup(self._delete_router, create_body['router']['id'])
- self.assertEqual(create_body['router']['name'], name)
- self.assertEqual(
- create_body['router']['external_gateway_info']['network_id'],
- CONF.network.public_network_id)
- self.assertEqual(create_body['router']['admin_state_up'], False)
- # Show details of the created router
- show_body = self.client.show_router(create_body['router']['id'])
- self.assertEqual(show_body['router']['name'], name)
- self.assertEqual(
- show_body['router']['external_gateway_info']['network_id'],
- CONF.network.public_network_id)
- self.assertEqual(show_body['router']['admin_state_up'], False)
- # List routers and verify if created router is there in response
- list_body = self.client.list_routers()
- routers_list = list()
- for router in list_body['routers']:
- routers_list.append(router['id'])
- self.assertIn(create_body['router']['id'], routers_list)
- # Update the name of router and verify if it is updated
- updated_name = 'updated ' + name
- update_body = self.client.update_router(create_body['router']['id'],
- name=updated_name)
- self.assertEqual(update_body['router']['name'], updated_name)
- show_body = self.client.show_router(
- create_body['router']['id'])
- self.assertEqual(show_body['router']['name'], updated_name)
-
- @test.idempotent_id('e54dd3a3-4352-4921-b09d-44369ae17397')
- def test_create_router_setting_project_id(self):
- # Test creating router from admin user setting project_id.
- project = data_utils.rand_name('test_tenant_')
- description = data_utils.rand_name('desc_')
- project = self.identity_utils.create_project(name=project,
- description=description)
- project_id = project['id']
- self.addCleanup(self.identity_utils.delete_project, project_id)
-
- name = data_utils.rand_name('router-')
- create_body = self.admin_client.create_router(name,
- tenant_id=project_id)
- self.addCleanup(self.admin_client.delete_router,
- create_body['router']['id'])
- self.assertEqual(project_id, create_body['router']['tenant_id'])
-
- @test.idempotent_id('847257cc-6afd-4154-b8fb-af49f5670ce8')
- @test.requires_ext(extension='ext-gw-mode', service='network')
- def test_create_router_with_default_snat_value(self):
- # Create a router with default snat rule
- name = data_utils.rand_name('router')
+ router_name = data_utils.rand_name(self.__class__.__name__ + '-router')
router = self._create_router(
- name, external_network_id=CONF.network.public_network_id)
- self._verify_router_gateway(
- router['id'], {'network_id': CONF.network.public_network_id,
- 'enable_snat': True})
+ name=router_name,
+ admin_state_up=False,
+ external_network_id=CONF.network.public_network_id)
+ self.assertEqual(router['name'], router_name)
+ self.assertEqual(router['admin_state_up'], False)
+ self.assertEqual(
+ router['external_gateway_info']['network_id'],
+ CONF.network.public_network_id)
+ # Show details of the created router
+ router_show = self.routers_client.show_router(
+ router['id'])['router']
+ self.assertEqual(router_show['name'], router['name'])
+ self.assertEqual(
+ router_show['external_gateway_info']['network_id'],
+ CONF.network.public_network_id)
+ # List routers and verify if created router is there in response
+ routers = self.routers_client.list_routers()['routers']
+ self.assertIn(router['id'], map(lambda x: x['id'], routers))
+ # Update the name of router and verify if it is updated
+ updated_name = 'updated' + router['name']
+ router_update = self.routers_client.update_router(
+ router['id'], name=updated_name)['router']
+ self.assertEqual(router_update['name'], updated_name)
+ router_show = self.routers_client.show_router(
+ router['id'])['router']
+ self.assertEqual(router_show['name'], updated_name)
- @test.idempotent_id('ea74068d-09e9-4fd7-8995-9b6a1ace920f')
- @test.requires_ext(extension='ext-gw-mode', service='network')
- def test_create_router_with_snat_explicit(self):
- name = data_utils.rand_name('snat-router')
- # Create a router enabling snat attributes
- enable_snat_states = [False, True]
- for enable_snat in enable_snat_states:
- external_gateway_info = {
- 'network_id': CONF.network.public_network_id,
- 'enable_snat': enable_snat}
- create_body = self.admin_client.create_router(
- name, external_gateway_info=external_gateway_info)
- self.addCleanup(self.admin_client.delete_router,
- create_body['router']['id'])
- # Verify snat attributes after router creation
- self._verify_router_gateway(create_body['router']['id'],
- exp_ext_gw_info=external_gateway_info)
-
- @test.attr(type='smoke')
- @test.idempotent_id('b42e6e39-2e37-49cc-a6f4-8467e940900a')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('b42e6e39-2e37-49cc-a6f4-8467e940900a')
def test_add_remove_router_interface_with_subnet_id(self):
- network = self.create_network()
- subnet = self.create_subnet(network)
- router = self._create_router(data_utils.rand_name('router-'))
+ network_name = data_utils.rand_name(self.__class__.__name__)
+ network = self.networks_client.create_network(
+ name=network_name)['network']
+ self.addCleanup(self.networks_client.delete_network,
+ network['id'])
+ subnet = self._create_subnet(network)
+ router = self._create_router()
# Add router interface with subnet id
- interface = self.client.add_router_interface_with_subnet_id(
- router['id'], subnet['id'])
+ interface = self.routers_client.add_router_interface(
+ router['id'], subnet_id=subnet['id'])
self.addCleanup(self._remove_router_interface_with_subnet_id,
router['id'], subnet['id'])
self.assertIn('subnet_id', interface.keys())
@@ -149,19 +122,24 @@
self.assertEqual(show_port_body['port']['device_id'],
router['id'])
- @test.attr(type='smoke')
- @test.idempotent_id('2b7d2f37-6748-4d78-92e5-1d590234f0d5')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('2b7d2f37-6748-4d78-92e5-1d590234f0d5')
def test_add_remove_router_interface_with_port_id(self):
- network = self.create_network()
- self.create_subnet(network)
- router = self._create_router(data_utils.rand_name('router-'))
+ network_name = data_utils.rand_name(self.__class__.__name__)
+ network = self.networks_client.create_network(
+ name=network_name)['network']
+ self.addCleanup(self.networks_client.delete_network,
+ network['id'])
+ self._create_subnet(network)
+ router = self._create_router()
port_body = self.ports_client.create_port(
network_id=network['id'])
# add router interface to port created above
- interface = self.client.add_router_interface_with_port_id(
- router['id'], port_body['port']['id'])
- self.addCleanup(self._remove_router_interface_with_port_id,
- router['id'], port_body['port']['id'])
+ interface = self.routers_client.add_router_interface(
+ router['id'],
+ port_id=port_body['port']['id'])
+ self.addCleanup(self.routers_client.remove_router_interface,
+ router['id'], port_id=port_body['port']['id'])
self.assertIn('subnet_id', interface.keys())
self.assertIn('port_id', interface.keys())
# Verify router id is equal to device id in port details
@@ -170,128 +148,66 @@
self.assertEqual(show_port_body['port']['device_id'],
router['id'])
- def _verify_router_gateway(self, router_id, exp_ext_gw_info=None):
- show_body = self.admin_client.show_router(router_id)
- actual_ext_gw_info = show_body['router']['external_gateway_info']
- if exp_ext_gw_info is None:
- self.assertIsNone(actual_ext_gw_info)
- return
- # Verify only keys passed in exp_ext_gw_info
- for k, v in six.iteritems(exp_ext_gw_info):
- self.assertEqual(v, actual_ext_gw_info[k])
+ @decorators.idempotent_id('cbe42f84-04c2-11e7-8adb-fa163e4fa634')
+ @utils.requires_ext(extension='ext-gw-mode', service='network')
+ @testtools.skipUnless(CONF.network.public_network_id,
+ 'The public_network_id option must be specified.')
+ @decorators.skip_because(bug='1676207')
+ def test_create_router_set_gateway_with_fixed_ip(self):
+ # Don't know public_network_address, so at first create address
+ # from public_network and delete
+ port = self.admin_ports_client.create_port(
+ network_id=CONF.network.public_network_id)['port']
+ self.admin_ports_client.delete_port(port_id=port['id'])
- def _verify_gateway_port(self, router_id):
- list_body = self.admin_ports_client.list_ports(
- network_id=CONF.network.public_network_id,
- device_id=router_id)
- self.assertEqual(len(list_body['ports']), 1)
- gw_port = list_body['ports'][0]
- fixed_ips = gw_port['fixed_ips']
- self.assertGreaterEqual(len(fixed_ips), 1)
- public_net_body = self.admin_networks_client.show_network(
- CONF.network.public_network_id)
- public_subnet_id = public_net_body['network']['subnets'][0]
- self.assertIn(public_subnet_id,
- map(lambda x: x['subnet_id'], fixed_ips))
+ fixed_ip = {
+ 'subnet_id': port['fixed_ips'][0]['subnet_id'],
+ 'ip_address': port['fixed_ips'][0]['ip_address']
+ }
+ external_gateway_info = {
+ 'network_id': CONF.network.public_network_id,
+ 'external_fixed_ips': [fixed_ip]
+ }
- @test.idempotent_id('6cc285d8-46bf-4f36-9b1a-783e3008ba79')
- def test_update_router_set_gateway(self):
- router = self._create_router(data_utils.rand_name('router-'))
- self.client.update_router(
- router['id'],
- external_gateway_info={
- 'network_id': CONF.network.public_network_id})
- # Verify operation - router
- self._verify_router_gateway(
- router['id'],
- {'network_id': CONF.network.public_network_id})
- self._verify_gateway_port(router['id'])
+ # Create a router and set gateway to fixed_ip
+ router = self.admin_routers_client.create_router(
+ external_gateway_info=external_gateway_info)['router']
+ self.addCleanup(self.admin_routers_client.delete_router,
+ router_id=router['id'])
+ # Examine router's gateway is equal to fixed_ip
+ self.assertEqual(router['external_gateway_info'][
+ 'external_fixed_ips'][0]['ip_address'],
+ fixed_ip['ip_address'])
- @test.idempotent_id('b386c111-3b21-466d-880c-5e72b01e1a33')
- @test.requires_ext(extension='ext-gw-mode', service='network')
- def test_update_router_set_gateway_with_snat_explicit(self):
- router = self._create_router(data_utils.rand_name('router-'))
- self.admin_client.update_router_with_snat_gw_info(
- router['id'],
- external_gateway_info={
- 'network_id': CONF.network.public_network_id,
- 'enable_snat': True})
- self._verify_router_gateway(
- router['id'],
- {'network_id': CONF.network.public_network_id,
- 'enable_snat': True})
- self._verify_gateway_port(router['id'])
-
- @test.idempotent_id('96536bc7-8262-4fb2-9967-5c46940fa279')
- @test.requires_ext(extension='ext-gw-mode', service='network')
- def test_update_router_set_gateway_without_snat(self):
- router = self._create_router(data_utils.rand_name('router-'))
- self.admin_client.update_router_with_snat_gw_info(
- router['id'],
- external_gateway_info={
- 'network_id': CONF.network.public_network_id,
- 'enable_snat': False})
- self._verify_router_gateway(
- router['id'],
- {'network_id': CONF.network.public_network_id,
- 'enable_snat': False})
- self._verify_gateway_port(router['id'])
-
- @test.idempotent_id('ad81b7ee-4f81-407b-a19c-17e623f763e8')
- def test_update_router_unset_gateway(self):
- router = self._create_router(
- data_utils.rand_name('router-'),
- external_network_id=CONF.network.public_network_id)
- self.client.update_router(router['id'], external_gateway_info={})
- self._verify_router_gateway(router['id'])
- # No gateway port expected
- list_body = self.admin_ports_client.list_ports(
- network_id=CONF.network.public_network_id,
- device_id=router['id'])
- self.assertFalse(list_body['ports'])
-
- @test.idempotent_id('f2faf994-97f4-410b-a831-9bc977b64374')
- @test.requires_ext(extension='ext-gw-mode', service='network')
- def test_update_router_reset_gateway_without_snat(self):
- router = self._create_router(
- data_utils.rand_name('router-'),
- external_network_id=CONF.network.public_network_id)
- self.admin_client.update_router_with_snat_gw_info(
- router['id'],
- external_gateway_info={
- 'network_id': CONF.network.public_network_id,
- 'enable_snat': False})
- self._verify_router_gateway(
- router['id'],
- {'network_id': CONF.network.public_network_id,
- 'enable_snat': False})
- self._verify_gateway_port(router['id'])
-
- @test.idempotent_id('c86ac3a8-50bd-4b00-a6b8-62af84a0765c')
- @test.requires_ext(extension='extraroute', service='network')
- def test_update_extra_route(self):
+ @decorators.idempotent_id('c86ac3a8-50bd-4b00-a6b8-62af84a0765c')
+ @utils.requires_ext(extension='extraroute', service='network')
+ def test_update_delete_extra_route(self):
# Create different cidr for each subnet to avoid cidr duplicate
- # The cidr starts from tenant_cidr
- next_cidr = netaddr.IPNetwork(self.tenant_cidr)
+ # The cidr starts from project_cidr
+ next_cidr = self.cidr
# Prepare to build several routes
test_routes = []
routes_num = 4
# Create a router
- router = self._create_router(
- data_utils.rand_name('router-'), True)
+ router = self._create_router(admin_state_up=True)
self.addCleanup(
self._delete_extra_routes,
router['id'])
# Update router extra route, second ip of the range is
# used as next hop
for i in range(routes_num):
- network = self.create_network()
+ network_name = data_utils.rand_name(self.__class__.__name__)
+ network = self.networks_client.create_network(
+ name=network_name)['network']
+ self.addCleanup(self.networks_client.delete_network,
+ network['id'])
subnet = self.create_subnet(network, cidr=next_cidr)
next_cidr = next_cidr.next()
# Add router interface with subnet id
self.create_router_interface(router['id'], subnet['id'])
-
+ self.addCleanup(self._remove_router_interface_with_subnet_id,
+ router['id'], subnet['id'])
cidr = netaddr.IPNetwork(subnet['cidr'])
next_hop = str(cidr[2])
destination = str(subnet['cidr'])
@@ -300,9 +216,9 @@
)
test_routes.sort(key=lambda x: x['destination'])
- extra_route = self.client.update_extra_routes(router['id'],
- routes=test_routes)
- show_body = self.client.show_router(router['id'])
+ extra_route = self.routers_client.update_router(
+ router['id'], routes=test_routes)
+ show_body = self.routers_client.show_router(router['id'])
# Assert the number of routes
self.assertEqual(routes_num, len(extra_route['router']['routes']))
self.assertEqual(routes_num, len(show_body['router']['routes']))
@@ -322,31 +238,41 @@
routes[i]['destination'])
self.assertEqual(test_routes[i]['nexthop'], routes[i]['nexthop'])
- def _delete_extra_routes(self, router_id):
- self.client.delete_extra_routes(router_id)
+ self._delete_extra_routes(router['id'])
+ show_body_after_deletion = self.routers_client.show_router(
+ router['id'])
+ self.assertEmpty(show_body_after_deletion['router']['routes'])
- @test.idempotent_id('a8902683-c788-4246-95c7-ad9c6d63a4d9')
+ def _delete_extra_routes(self, router_id):
+ self.routers_client.update_router(router_id, routes=None)
+
+ @decorators.idempotent_id('a8902683-c788-4246-95c7-ad9c6d63a4d9')
def test_update_router_admin_state(self):
- router = self._create_router(data_utils.rand_name('router-'))
+ router = self._create_router()
self.assertFalse(router['admin_state_up'])
# Update router admin state
- update_body = self.client.update_router(router['id'],
- admin_state_up=True)
+ update_body = self.routers_client.update_router(router['id'],
+ admin_state_up=True)
self.assertTrue(update_body['router']['admin_state_up'])
- show_body = self.client.show_router(router['id'])
+ show_body = self.routers_client.show_router(router['id'])
self.assertTrue(show_body['router']['admin_state_up'])
- @test.attr(type='smoke')
- @test.idempotent_id('802c73c9-c937-4cef-824b-2191e24a6aab')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('802c73c9-c937-4cef-824b-2191e24a6aab')
def test_add_multiple_router_interfaces(self):
- network01 = self.create_network(
- network_name=data_utils.rand_name('router-network01-'))
- network02 = self.create_network(
- network_name=data_utils.rand_name('router-network02-'))
- subnet01 = self.create_subnet(network01)
- sub02_cidr = netaddr.IPNetwork(self.tenant_cidr).next()
- subnet02 = self.create_subnet(network02, cidr=sub02_cidr)
- router = self._create_router(data_utils.rand_name('router-'))
+ network_name = data_utils.rand_name(self.__class__.__name__)
+ network01 = self.networks_client.create_network(
+ name=network_name)['network']
+ self.addCleanup(self.networks_client.delete_network,
+ network01['id'])
+ network02 = self.networks_client.create_network(
+ name=data_utils.rand_name(self.__class__.__name__))['network']
+ self.addCleanup(self.networks_client.delete_network,
+ network02['id'])
+ subnet01 = self._create_subnet(network01)
+ sub02_cidr = self.cidr.next()
+ subnet02 = self._create_subnet(network02, cidr=sub02_cidr)
+ router = self._create_router()
interface01 = self._add_router_interface_with_subnet_id(router['id'],
subnet01['id'])
self._verify_router_interface(router['id'], subnet01['id'],
@@ -356,6 +282,27 @@
self._verify_router_interface(router['id'], subnet02['id'],
interface02['port_id'])
+ @decorators.idempotent_id('96522edf-b4b5-45d9-8443-fa11c26e6eff')
+ def test_router_interface_port_update_with_fixed_ip(self):
+ network_name = data_utils.rand_name(self.__class__.__name__)
+ network = self.networks_client.create_network(
+ name=network_name)['network']
+ self.addCleanup(self.networks_client.delete_network,
+ network['id'])
+ subnet = self._create_subnet(network)
+ router = self._create_router()
+ fixed_ip = [{'subnet_id': subnet['id']}]
+ interface = self._add_router_interface_with_subnet_id(router['id'],
+ subnet['id'])
+ self.assertIn('port_id', interface)
+ self.assertIn('subnet_id', interface)
+ port = self.ports_client.show_port(interface['port_id'])
+ self.assertEqual(port['port']['id'], interface['port_id'])
+ router_port = self.ports_client.update_port(port['port']['id'],
+ fixed_ips=fixed_ip)
+ self.assertEqual(subnet['id'],
+ router_port['port']['fixed_ips'][0]['subnet_id'])
+
def _verify_router_interface(self, router_id, subnet_id, port_id):
show_port_body = self.ports_client.show_port(port_id)
interface_port = show_port_body['port']
@@ -366,35 +313,3 @@
class RoutersIpV6Test(RoutersTest):
_ip_version = 6
-
-
-class DvrRoutersTest(base.BaseRouterTest):
-
- @classmethod
- def skip_checks(cls):
- super(DvrRoutersTest, cls).skip_checks()
- if not test.is_extension_enabled('dvr', 'network'):
- msg = "DVR extension not enabled."
- raise cls.skipException(msg)
-
- @test.idempotent_id('141297aa-3424-455d-aa8d-f2d95731e00a')
- def test_create_distributed_router(self):
- name = data_utils.rand_name('router')
- create_body = self.admin_client.create_router(
- name, distributed=True)
- self.addCleanup(self._delete_router,
- create_body['router']['id'],
- self.admin_client)
- self.assertTrue(create_body['router']['distributed'])
-
- @test.idempotent_id('644d7a4a-01a1-4b68-bb8d-0c0042cb1729')
- def test_convert_centralized_router(self):
- router = self._create_router(data_utils.rand_name('router'))
- self.assertNotIn('distributed', router)
- update_body = self.admin_client.update_router(router['id'],
- distributed=True)
- self.assertTrue(update_body['router']['distributed'])
- show_body = self.admin_client.show_router(router['id'])
- self.assertTrue(show_body['router']['distributed'])
- show_body = self.client.show_router(router['id'])
- self.assertNotIn('distributed', show_body['router'])
diff --git a/tempest/api/network/test_routers_negative.py b/tempest/api/network/test_routers_negative.py
index 90da6fd..ddd7d3a 100644
--- a/tempest/api/network/test_routers_negative.py
+++ b/tempest/api/network/test_routers_negative.py
@@ -13,60 +13,55 @@
# License for the specific language governing permissions and limitations
# under the License.
-import netaddr
-from tempest_lib import exceptions as lib_exc
-
-from tempest.api.network import base_routers as base
-from tempest.common.utils import data_utils
+from tempest.api.network import base
+from tempest.common import utils
from tempest import config
-from tempest import test
+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 RoutersNegativeTest(base.BaseRouterTest):
+class RoutersNegativeTest(base.BaseNetworkTest):
@classmethod
def skip_checks(cls):
super(RoutersNegativeTest, cls).skip_checks()
- if not test.is_extension_enabled('router', 'network'):
+ if not utils.is_extension_enabled('router', 'network'):
msg = "router extension not enabled."
raise cls.skipException(msg)
@classmethod
def resource_setup(cls):
super(RoutersNegativeTest, cls).resource_setup()
- cls.router = cls.create_router(data_utils.rand_name('router-'))
+ cls.router = cls.create_router()
cls.network = cls.create_network()
cls.subnet = cls.create_subnet(cls.network)
- cls.tenant_cidr = (CONF.network.tenant_network_cidr
- if cls._ip_version == 4 else
- CONF.network.tenant_network_v6_cidr)
- @test.attr(type=['negative'])
- @test.idempotent_id('37a94fc0-a834-45b9-bd23-9a81d2fd1e22')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('37a94fc0-a834-45b9-bd23-9a81d2fd1e22')
def test_router_add_gateway_invalid_network_returns_404(self):
self.assertRaises(lib_exc.NotFound,
- self.client.update_router,
+ self.routers_client.update_router,
self.router['id'],
external_gateway_info={
'network_id': self.router['id']})
- @test.attr(type=['negative'])
- @test.idempotent_id('11836a18-0b15-4327-a50b-f0d9dc66bddd')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('11836a18-0b15-4327-a50b-f0d9dc66bddd')
def test_router_add_gateway_net_not_external_returns_400(self):
- alt_network = self.create_network(
- network_name=data_utils.rand_name('router-negative-'))
- sub_cidr = netaddr.IPNetwork(self.tenant_cidr).next()
+ alt_network = self.create_network()
+ sub_cidr = self.cidr.next()
self.create_subnet(alt_network, cidr=sub_cidr)
self.assertRaises(lib_exc.BadRequest,
- self.client.update_router,
+ self.routers_client.update_router,
self.router['id'],
external_gateway_info={
'network_id': alt_network['id']})
- @test.attr(type=['negative'])
- @test.idempotent_id('957751a3-3c68-4fa2-93b6-eb52ea10db6e')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('957751a3-3c68-4fa2-93b6-eb52ea10db6e')
def test_add_router_interfaces_on_overlapping_subnets_returns_400(self):
network01 = self.create_network(
network_name=data_utils.rand_name('router-network01-'))
@@ -74,41 +69,46 @@
network_name=data_utils.rand_name('router-network02-'))
subnet01 = self.create_subnet(network01)
subnet02 = self.create_subnet(network02)
- self._add_router_interface_with_subnet_id(self.router['id'],
- subnet01['id'])
+ interface = self.routers_client.add_router_interface(
+ self.router['id'], subnet_id=subnet01['id'])
+ self.addCleanup(self.routers_client.remove_router_interface,
+ self.router['id'], subnet_id=subnet01['id'])
+ self.assertEqual(subnet01['id'], interface['subnet_id'])
self.assertRaises(lib_exc.BadRequest,
- self._add_router_interface_with_subnet_id,
+ self.routers_client.add_router_interface,
self.router['id'],
- subnet02['id'])
+ subnet_id=subnet02['id'])
- @test.attr(type=['negative'])
- @test.idempotent_id('04df80f9-224d-47f5-837a-bf23e33d1c20')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('04df80f9-224d-47f5-837a-bf23e33d1c20')
def test_router_remove_interface_in_use_returns_409(self):
- self.client.add_router_interface_with_subnet_id(
- self.router['id'], self.subnet['id'])
+ self.routers_client.add_router_interface(self.router['id'],
+ subnet_id=self.subnet['id'])
+ self.addCleanup(self.routers_client.remove_router_interface,
+ self.router['id'], subnet_id=self.subnet['id'])
self.assertRaises(lib_exc.Conflict,
- self.client.delete_router,
+ self.routers_client.delete_router,
self.router['id'])
- @test.attr(type=['negative'])
- @test.idempotent_id('c2a70d72-8826-43a7-8208-0209e6360c47')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('c2a70d72-8826-43a7-8208-0209e6360c47')
def test_show_non_existent_router_returns_404(self):
router = data_utils.rand_name('non_exist_router')
- self.assertRaises(lib_exc.NotFound, self.client.show_router,
+ self.assertRaises(lib_exc.NotFound, self.routers_client.show_router,
router)
- @test.attr(type=['negative'])
- @test.idempotent_id('b23d1569-8b0c-4169-8d4b-6abd34fad5c7')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('b23d1569-8b0c-4169-8d4b-6abd34fad5c7')
def test_update_non_existent_router_returns_404(self):
router = data_utils.rand_name('non_exist_router')
- self.assertRaises(lib_exc.NotFound, self.client.update_router,
+ self.assertRaises(lib_exc.NotFound, self.routers_client.update_router,
router, name="new_name")
- @test.attr(type=['negative'])
- @test.idempotent_id('c7edc5ad-d09d-41e6-a344-5c0c31e2e3e4')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('c7edc5ad-d09d-41e6-a344-5c0c31e2e3e4')
def test_delete_non_existent_router_returns_404(self):
router = data_utils.rand_name('non_exist_router')
- self.assertRaises(lib_exc.NotFound, self.client.delete_router,
+ self.assertRaises(lib_exc.NotFound, self.routers_client.delete_router,
router)
@@ -116,26 +116,17 @@
_ip_version = 6
-class DvrRoutersNegativeTest(base.BaseRouterTest):
+class DvrRoutersNegativeTest(base.BaseNetworkTest):
@classmethod
def skip_checks(cls):
super(DvrRoutersNegativeTest, cls).skip_checks()
- if not test.is_extension_enabled('dvr', 'network'):
+ if not utils.is_extension_enabled('dvr', 'network'):
msg = "DVR extension not enabled."
raise cls.skipException(msg)
- @classmethod
- def resource_setup(cls):
- super(DvrRoutersNegativeTest, cls).resource_setup()
- cls.router = cls.create_router(data_utils.rand_name('router'))
- cls.network = cls.create_network()
- cls.subnet = cls.create_subnet(cls.network)
-
- @test.attr(type=['negative'])
- @test.idempotent_id('4990b055-8fc7-48ab-bba7-aa28beaad0b9')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('4990b055-8fc7-48ab-bba7-aa28beaad0b9')
def test_router_create_tenant_distributed_returns_forbidden(self):
- self.assertRaises(lib_exc.Forbidden,
- self.create_router,
- data_utils.rand_name('router'),
+ self.assertRaises(lib_exc.Forbidden, self.create_router,
distributed=True)
diff --git a/tempest/api/network/test_security_groups.py b/tempest/api/network/test_security_groups.py
index cf45328..24bd8ea 100644
--- a/tempest/api/network/test_security_groups.py
+++ b/tempest/api/network/test_security_groups.py
@@ -13,23 +13,21 @@
# License for the specific language governing permissions and limitations
# under the License.
-import six
-
from tempest.api.network import base_security_groups as base
-from tempest.common.utils import data_utils
+from tempest.common import utils
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
CONF = config.CONF
class SecGroupTest(base.BaseSecGroupTest):
- _tenant_network_cidr = CONF.network.tenant_network_cidr
@classmethod
def skip_checks(cls):
super(SecGroupTest, cls).skip_checks()
- if not test.is_extension_enabled('security-group', 'network'):
+ if not utils.is_extension_enabled('security-group', 'network'):
msg = "security-group extension not enabled."
raise cls.skipException(msg)
@@ -41,7 +39,8 @@
remote_ip_prefix=None):
# Create Security Group rule with the input params and validate
# that SG rule is created with the same parameters.
- rule_create_body = self.client.create_security_group_rule(
+ sec_group_rules_client = self.security_group_rules_client
+ rule_create_body = sec_group_rules_client.create_security_group_rule(
security_group_id=sg_id,
direction=direction,
ethertype=ethertype,
@@ -61,16 +60,16 @@
'port_range_max': port_range_max,
'remote_group_id': remote_group_id,
'remote_ip_prefix': remote_ip_prefix}
- for key, value in six.iteritems(expected):
+ for key, value in expected.items():
self.assertEqual(value, sec_group_rule[key],
"Field %s of the created security group "
"rule does not match with %s." %
(key, value))
- @test.attr(type='smoke')
- @test.idempotent_id('e30abd17-fef9-4739-8617-dc26da88e686')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('e30abd17-fef9-4739-8617-dc26da88e686')
def test_list_security_groups(self):
- # Verify the that security group belonging to tenant exist in list
+ # Verify the security group belonging to project exist in list
body = self.security_groups_client.list_security_groups()
security_groups = body['security_groups']
found = None
@@ -80,10 +79,10 @@
msg = "Security-group list doesn't contain default security-group"
self.assertIsNotNone(found, msg)
- @test.attr(type='smoke')
- @test.idempotent_id('bfd128e5-3c92-44b6-9d66-7fe29d22c802')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('bfd128e5-3c92-44b6-9d66-7fe29d22c802')
def test_create_list_update_show_delete_security_group(self):
- group_create_body, name = self._create_security_group()
+ group_create_body, _ = self._create_security_group()
# List security groups and verify if created group is there in response
list_body = self.security_groups_client.list_security_groups()
@@ -109,15 +108,16 @@
self.assertEqual(show_body['security_group']['description'],
new_description)
- @test.attr(type='smoke')
- @test.idempotent_id('cfb99e0e-7410-4a3d-8a0c-959a63ee77e9')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('cfb99e0e-7410-4a3d-8a0c-959a63ee77e9')
def test_create_show_delete_security_group_rule(self):
group_create_body, _ = self._create_security_group()
# Create rules for each protocol
protocols = ['tcp', 'udp', 'icmp']
+ client = self.security_group_rules_client
for protocol in protocols:
- rule_create_body = self.client.create_security_group_rule(
+ rule_create_body = client.create_security_group_rule(
security_group_id=group_create_body['security_group']['id'],
protocol=protocol,
direction='ingress',
@@ -125,23 +125,24 @@
)
# Show details of the created security rule
- show_rule_body = self.client.show_security_group_rule(
+ show_rule_body = client.show_security_group_rule(
rule_create_body['security_group_rule']['id']
)
create_dict = rule_create_body['security_group_rule']
- for key, value in six.iteritems(create_dict):
+ for key, value in create_dict.items():
self.assertEqual(value,
show_rule_body['security_group_rule'][key],
"%s does not match." % key)
# List rules and verify created rule is in response
- rule_list_body = self.client.list_security_group_rules()
+ rule_list_body = (
+ self.security_group_rules_client.list_security_group_rules())
rule_list = [rule['id']
for rule in rule_list_body['security_group_rules']]
self.assertIn(rule_create_body['security_group_rule']['id'],
rule_list)
- @test.idempotent_id('87dfbcf9-1849-43ea-b1e4-efa3eeae9f71')
+ @decorators.idempotent_id('87dfbcf9-1849-43ea-b1e4-efa3eeae9f71')
def test_create_security_group_rule_with_additional_args(self):
"""Verify security group rule with additional arguments works.
@@ -159,7 +160,7 @@
port_range_min,
port_range_max)
- @test.idempotent_id('c9463db8-b44d-4f52-b6c0-8dbda99f26ce')
+ @decorators.idempotent_id('c9463db8-b44d-4f52-b6c0-8dbda99f26ce')
def test_create_security_group_rule_with_icmp_type_code(self):
"""Verify security group rule for icmp protocol works.
@@ -179,7 +180,7 @@
self.ethertype, protocol,
icmp_type, icmp_code)
- @test.idempotent_id('c2ed2deb-7a0c-44d8-8b4c-a5825b5c310b')
+ @decorators.idempotent_id('c2ed2deb-7a0c-44d8-8b4c-a5825b5c310b')
def test_create_security_group_rule_with_remote_group_id(self):
# Verify creating security group rule with remote_group_id works
sg1_body, _ = self._create_security_group()
@@ -197,7 +198,7 @@
port_range_max,
remote_group_id=remote_id)
- @test.idempotent_id('16459776-5da2-4634-bce4-4b55ee3ec188')
+ @decorators.idempotent_id('16459776-5da2-4634-bce4-4b55ee3ec188')
def test_create_security_group_rule_with_remote_ip_prefix(self):
# Verify creating security group rule with remote_ip_prefix works
sg1_body, _ = self._create_security_group()
@@ -207,14 +208,14 @@
protocol = 'tcp'
port_range_min = 76
port_range_max = 77
- ip_prefix = self._tenant_network_cidr
+ ip_prefix = str(self.cidr)
self._create_verify_security_group_rule(sg_id, direction,
self.ethertype, protocol,
port_range_min,
port_range_max,
remote_ip_prefix=ip_prefix)
- @test.idempotent_id('0a307599-6655-4220-bebc-fd70c64f2290')
+ @decorators.idempotent_id('0a307599-6655-4220-bebc-fd70c64f2290')
def test_create_security_group_rule_with_protocol_integer_value(self):
# Verify creating security group rule with the
# protocol as integer value
@@ -223,7 +224,8 @@
direction = 'ingress'
protocol = 17
security_group_id = group_create_body['security_group']['id']
- rule_create_body = self.client.create_security_group_rule(
+ client = self.security_group_rules_client
+ rule_create_body = client.create_security_group_rule(
security_group_id=security_group_id,
direction=direction,
protocol=protocol
@@ -235,4 +237,3 @@
class SecGroupIPv6Test(SecGroupTest):
_ip_version = 6
- _tenant_network_cidr = CONF.network.tenant_network_v6_cidr
diff --git a/tempest/api/network/test_security_groups_negative.py b/tempest/api/network/test_security_groups_negative.py
index 58e39e9..d054865 100644
--- a/tempest/api/network/test_security_groups_negative.py
+++ b/tempest/api/network/test_security_groups_negative.py
@@ -13,66 +13,66 @@
# License for the specific language governing permissions and limitations
# under the License.
-import uuid
-
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.network import base_security_groups as base
+from tempest.common import utils
from tempest import config
-from tempest import test
+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 NegativeSecGroupTest(base.BaseSecGroupTest):
- _tenant_network_cidr = CONF.network.tenant_network_cidr
@classmethod
def skip_checks(cls):
super(NegativeSecGroupTest, cls).skip_checks()
- if not test.is_extension_enabled('security-group', 'network'):
+ if not utils.is_extension_enabled('security-group', 'network'):
msg = "security-group extension not enabled."
raise cls.skipException(msg)
- @test.attr(type=['negative'])
- @test.idempotent_id('424fd5c3-9ddc-486a-b45f-39bf0c820fc6')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('424fd5c3-9ddc-486a-b45f-39bf0c820fc6')
def test_show_non_existent_security_group(self):
- non_exist_id = str(uuid.uuid4())
+ non_exist_id = data_utils.rand_uuid()
self.assertRaises(
lib_exc.NotFound, self.security_groups_client.show_security_group,
non_exist_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('4c094c09-000b-4e41-8100-9617600c02a6')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('4c094c09-000b-4e41-8100-9617600c02a6')
def test_show_non_existent_security_group_rule(self):
- non_exist_id = str(uuid.uuid4())
- self.assertRaises(lib_exc.NotFound,
- self.client.show_security_group_rule,
- non_exist_id)
+ non_exist_id = data_utils.rand_uuid()
+ self.assertRaises(
+ lib_exc.NotFound,
+ self.security_group_rules_client.show_security_group_rule,
+ non_exist_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('1f1bb89d-5664-4956-9fcd-83ee0fa603df')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('1f1bb89d-5664-4956-9fcd-83ee0fa603df')
def test_delete_non_existent_security_group(self):
- non_exist_id = str(uuid.uuid4())
+ non_exist_id = data_utils.rand_uuid()
self.assertRaises(lib_exc.NotFound,
self.security_groups_client.delete_security_group,
non_exist_id
)
- @test.attr(type=['negative'])
- @test.idempotent_id('981bdc22-ce48-41ed-900a-73148b583958')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('981bdc22-ce48-41ed-900a-73148b583958')
def test_create_security_group_rule_with_bad_protocol(self):
group_create_body, _ = self._create_security_group()
# Create rule with bad protocol name
pname = 'bad_protocol_name'
self.assertRaises(
- lib_exc.BadRequest, self.client.create_security_group_rule,
+ lib_exc.BadRequest,
+ self.security_group_rules_client.create_security_group_rule,
security_group_id=group_create_body['security_group']['id'],
protocol=pname, direction='ingress', ethertype=self.ethertype)
- @test.attr(type=['negative'])
- @test.idempotent_id('5f8daf69-3c5f-4aaa-88c9-db1d66f68679')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('5f8daf69-3c5f-4aaa-88c9-db1d66f68679')
def test_create_security_group_rule_with_bad_remote_ip_prefix(self):
group_create_body, _ = self._create_security_group()
@@ -80,55 +80,59 @@
prefix = ['192.168.1./24', '192.168.1.1/33', 'bad_prefix', '256']
for remote_ip_prefix in prefix:
self.assertRaises(
- lib_exc.BadRequest, self.client.create_security_group_rule,
+ lib_exc.BadRequest,
+ self.security_group_rules_client.create_security_group_rule,
security_group_id=group_create_body['security_group']['id'],
protocol='tcp', direction='ingress', ethertype=self.ethertype,
remote_ip_prefix=remote_ip_prefix)
- @test.attr(type=['negative'])
- @test.idempotent_id('4bf786fd-2f02-443c-9716-5b98e159a49a')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('4bf786fd-2f02-443c-9716-5b98e159a49a')
def test_create_security_group_rule_with_non_existent_remote_groupid(self):
group_create_body, _ = self._create_security_group()
- non_exist_id = str(uuid.uuid4())
+ non_exist_id = data_utils.rand_uuid()
# Create rule with non existent remote_group_id
group_ids = ['bad_group_id', non_exist_id]
for remote_group_id in group_ids:
self.assertRaises(
- lib_exc.NotFound, self.client.create_security_group_rule,
+ lib_exc.NotFound,
+ self.security_group_rules_client.create_security_group_rule,
security_group_id=group_create_body['security_group']['id'],
protocol='tcp', direction='ingress', ethertype=self.ethertype,
remote_group_id=remote_group_id)
- @test.attr(type=['negative'])
- @test.idempotent_id('b5c4b247-6b02-435b-b088-d10d45650881')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('b5c4b247-6b02-435b-b088-d10d45650881')
def test_create_security_group_rule_with_remote_ip_and_group(self):
sg1_body, _ = self._create_security_group()
sg2_body, _ = self._create_security_group()
# Create rule specifying both remote_ip_prefix and remote_group_id
- prefix = self._tenant_network_cidr
+ prefix = str(self.cidr)
self.assertRaises(
- lib_exc.BadRequest, self.client.create_security_group_rule,
+ lib_exc.BadRequest,
+ self.security_group_rules_client.create_security_group_rule,
security_group_id=sg1_body['security_group']['id'],
protocol='tcp', direction='ingress',
ethertype=self.ethertype, remote_ip_prefix=prefix,
remote_group_id=sg2_body['security_group']['id'])
- @test.attr(type=['negative'])
- @test.idempotent_id('5666968c-fff3-40d6-9efc-df1c8bd01abb')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('5666968c-fff3-40d6-9efc-df1c8bd01abb')
def test_create_security_group_rule_with_bad_ethertype(self):
group_create_body, _ = self._create_security_group()
# Create rule with bad ethertype
ethertype = 'bad_ethertype'
self.assertRaises(
- lib_exc.BadRequest, self.client.create_security_group_rule,
+ lib_exc.BadRequest,
+ self.security_group_rules_client.create_security_group_rule,
security_group_id=group_create_body['security_group']['id'],
protocol='udp', direction='ingress', ethertype=ethertype)
- @test.attr(type=['negative'])
- @test.idempotent_id('0d9c7791-f2ad-4e2f-ac73-abf2373b0d2d')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('0d9c7791-f2ad-4e2f-ac73-abf2373b0d2d')
def test_create_security_group_rule_with_invalid_ports(self):
group_create_body, _ = self._create_security_group()
@@ -140,7 +144,8 @@
(-16, 65536, 'Invalid value for port')]
for pmin, pmax, msg in states:
ex = self.assertRaises(
- lib_exc.BadRequest, self.client.create_security_group_rule,
+ lib_exc.BadRequest,
+ self.security_group_rules_client.create_security_group_rule,
security_group_id=group_create_body['security_group']['id'],
protocol='tcp', port_range_min=pmin, port_range_max=pmax,
direction='ingress', ethertype=self.ethertype)
@@ -148,18 +153,20 @@
# Create rule for icmp protocol with invalid ports
states = [(1, 256, 'Invalid value for ICMP code'),
+ (-1, 25, 'Invalid value'),
(None, 6, 'ICMP type (port-range-min) is missing'),
(300, 1, 'Invalid value for ICMP type')]
for pmin, pmax, msg in states:
ex = self.assertRaises(
- lib_exc.BadRequest, self.client.create_security_group_rule,
+ lib_exc.BadRequest,
+ self.security_group_rules_client.create_security_group_rule,
security_group_id=group_create_body['security_group']['id'],
protocol='icmp', port_range_min=pmin, port_range_max=pmax,
direction='ingress', ethertype=self.ethertype)
self.assertIn(msg, str(ex))
- @test.attr(type=['negative'])
- @test.idempotent_id('2323061e-9fbf-4eb0-b547-7e8fafc90849')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('2323061e-9fbf-4eb0-b547-7e8fafc90849')
def test_create_additional_default_security_group_fails(self):
# Create security group named 'default', it should be failed.
name = 'default'
@@ -167,8 +174,18 @@
self.security_groups_client.create_security_group,
name=name)
- @test.attr(type=['negative'])
- @test.idempotent_id('8fde898f-ce88-493b-adc9-4e4692879fc5')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('966e2b96-023a-11e7-a9e4-fa163e4fa634')
+ def test_create_security_group_update_name_default(self):
+ # Update security group name to 'default', it should be failed.
+ group_create_body, _ = self._create_security_group()
+ self.assertRaises(lib_exc.Conflict,
+ self.security_groups_client.update_security_group,
+ group_create_body['security_group']['id'],
+ name="default")
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('8fde898f-ce88-493b-adc9-4e4692879fc5')
def test_create_duplicate_security_group_rule_fails(self):
# Create duplicate security group rule, it should fail.
body, _ = self._create_security_group()
@@ -176,7 +193,7 @@
min_port = 66
max_port = 67
# Create a rule with valid params
- self.client.create_security_group_rule(
+ self.security_group_rules_client.create_security_group_rule(
security_group_id=body['security_group']['id'],
direction='ingress',
ethertype=self.ethertype,
@@ -187,41 +204,42 @@
# Try creating the same security group rule, it should fail
self.assertRaises(
- lib_exc.Conflict, self.client.create_security_group_rule,
+ lib_exc.Conflict,
+ self.security_group_rules_client.create_security_group_rule,
security_group_id=body['security_group']['id'],
protocol='tcp', direction='ingress', ethertype=self.ethertype,
port_range_min=min_port, port_range_max=max_port)
- @test.attr(type=['negative'])
- @test.idempotent_id('be308db6-a7cf-4d5c-9baf-71bafd73f35e')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('be308db6-a7cf-4d5c-9baf-71bafd73f35e')
def test_create_security_group_rule_with_non_existent_security_group(self):
# Create security group rules with not existing security group.
- non_existent_sg = str(uuid.uuid4())
- self.assertRaises(lib_exc.NotFound,
- self.client.create_security_group_rule,
- security_group_id=non_existent_sg,
- direction='ingress', ethertype=self.ethertype)
+ non_existent_sg = data_utils.rand_uuid()
+ self.assertRaises(
+ lib_exc.NotFound,
+ self.security_group_rules_client.create_security_group_rule,
+ security_group_id=non_existent_sg,
+ direction='ingress', ethertype=self.ethertype)
class NegativeSecGroupIPv6Test(NegativeSecGroupTest):
_ip_version = 6
- _tenant_network_cidr = CONF.network.tenant_network_v6_cidr
- @test.attr(type=['negative'])
- @test.idempotent_id('7607439c-af73-499e-bf64-f687fd12a842')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('7607439c-af73-499e-bf64-f687fd12a842')
def test_create_security_group_rule_wrong_ip_prefix_version(self):
group_create_body, _ = self._create_security_group()
# Create rule with bad remote_ip_prefix
pairs = ({'ethertype': 'IPv6',
- 'ip_prefix': CONF.network.tenant_network_cidr},
+ 'ip_prefix': CONF.network.project_network_cidr},
{'ethertype': 'IPv4',
- 'ip_prefix': CONF.network.tenant_network_v6_cidr})
+ 'ip_prefix': CONF.network.project_network_v6_cidr})
for pair in pairs:
- self.assertRaisesRegexp(
+ self.assertRaisesRegex(
lib_exc.BadRequest,
"Conflicting value ethertype",
- self.client.create_security_group_rule,
+ self.security_group_rules_client.create_security_group_rule,
security_group_id=group_create_body['security_group']['id'],
protocol='tcp', direction='ingress',
ethertype=pair['ethertype'],
diff --git a/tempest/api/network/test_service_providers.py b/tempest/api/network/test_service_providers.py
new file mode 100644
index 0000000..9ebcd89
--- /dev/null
+++ b/tempest/api/network/test_service_providers.py
@@ -0,0 +1,29 @@
+# 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.network import base
+from tempest.common import utils
+from tempest.lib import decorators
+
+
+class ServiceProvidersTest(base.BaseNetworkTest):
+
+ @decorators.idempotent_id('2cbbeea9-f010-40f6-8df5-4eaa0c918ea6')
+ @testtools.skipUnless(
+ utils.is_extension_enabled('service-type', 'network'),
+ 'service-type extension not enabled.')
+ def test_service_providers_list(self):
+ body = self.service_providers_client.list_service_providers()
+ self.assertIn('service_providers', body)
+ self.assertIsInstance(body['service_providers'], list)
diff --git a/tempest/api/network/test_service_type_management.py b/tempest/api/network/test_service_type_management.py
deleted file mode 100644
index ad1ecc4..0000000
--- a/tempest/api/network/test_service_type_management.py
+++ /dev/null
@@ -1,32 +0,0 @@
-# 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 import decorators
-
-from tempest.api.network import base
-from tempest import test
-
-
-class ServiceTypeManagementTestJSON(base.BaseNetworkTest):
-
- @classmethod
- def skip_checks(cls):
- super(ServiceTypeManagementTestJSON, cls).skip_checks()
- if not test.is_extension_enabled('service-type', 'network'):
- msg = "Neutron Service Type Management not enabled."
- raise cls.skipException(msg)
-
- @decorators.skip_because(bug="1400370")
- @test.idempotent_id('2cbbeea9-f010-40f6-8df5-4eaa0c918ea6')
- def test_service_provider_list(self):
- body = self.client.list_service_providers()
- self.assertIsInstance(body['service_providers'], list)
diff --git a/tempest/api/network/test_subnetpools_extensions.py b/tempest/api/network/test_subnetpools_extensions.py
index 29d1d89..bfc2609 100644
--- a/tempest/api/network/test_subnetpools_extensions.py
+++ b/tempest/api/network/test_subnetpools_extensions.py
@@ -13,10 +13,12 @@
# under the License.
from tempest.api.network import base
-from tempest.common.utils import data_utils
+from tempest.common import utils
from tempest import config
-from tempest import test
-from tempest_lib import exceptions as lib_exc
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
CONF = config.CONF
@@ -30,7 +32,7 @@
Lists subnet pool.
Show subnet pool details.
- v2.0 of the Neutron API is assumed. It is assumed that subnetpools
+ v2.0 of the Neutron API is assumed. It is assumed that subnet_allocation
options mentioned in the [network-feature-enabled] section and
default_network option mentioned in the [network] section of
etc/tempest.conf:
@@ -40,38 +42,33 @@
@classmethod
def skip_checks(cls):
super(SubnetPoolsTestJSON, cls).skip_checks()
- if not test.is_extension_enabled('subnetpools', 'network'):
- msg = "subnet pools extension not enabled."
+ if not utils.is_extension_enabled('subnet_allocation', 'network'):
+ msg = "subnet_allocation extension not enabled."
raise cls.skipException(msg)
- @test.attr(type='smoke')
- @test.idempotent_id('62595970-ab1c-4b7f-8fcc-fddfe55e9811')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('62595970-ab1c-4b7f-8fcc-fddfe55e9811')
def test_create_list_show_update_delete_subnetpools(self):
subnetpool_name = data_utils.rand_name('subnetpools')
# create subnet pool
prefix = CONF.network.default_network
- body = self.subnetpools_client.create_subnetpools(name=subnetpool_name,
- prefixes=prefix)
+ body = self.subnetpools_client.create_subnetpool(name=subnetpool_name,
+ prefixes=prefix)
subnetpool_id = body["subnetpool"]["id"]
- self.addCleanup(self._cleanup_subnetpools, subnetpool_id)
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.subnetpools_client.delete_subnetpool,
+ subnetpool_id)
self.assertEqual(subnetpool_name, body["subnetpool"]["name"])
# get detail about subnet pool
- body = self.subnetpools_client.show_subnetpools(subnetpool_id)
+ body = self.subnetpools_client.show_subnetpool(subnetpool_id)
self.assertEqual(subnetpool_name, body["subnetpool"]["name"])
# update the subnet pool
subnetpool_name = data_utils.rand_name('subnetpools_update')
- body = self.subnetpools_client.update_subnetpools(subnetpool_id,
- name=subnetpool_name)
+ body = self.subnetpools_client.update_subnetpool(subnetpool_id,
+ name=subnetpool_name)
self.assertEqual(subnetpool_name, body["subnetpool"]["name"])
# delete subnet pool
- body = self.subnetpools_client.delete_subnetpools(subnetpool_id)
+ body = self.subnetpools_client.delete_subnetpool(subnetpool_id)
self.assertRaises(lib_exc.NotFound,
- self.subnetpools_client.show_subnetpools,
+ self.subnetpools_client.show_subnetpool,
subnetpool_id)
-
- def _cleanup_subnetpools(self, subnetpool_id):
- # this is used to cleanup the resources
- try:
- self.subnetpools_client.delete_subnetpools(subnetpool_id)
- except lib_exc.NotFound:
- pass
diff --git a/tempest/api/network/test_tags.py b/tempest/api/network/test_tags.py
new file mode 100644
index 0000000..85f6896
--- /dev/null
+++ b/tempest/api/network/test_tags.py
@@ -0,0 +1,199 @@
+# Copyright 2017 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 tempest.api.network import base
+from tempest.common import utils
+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 TagsTest(base.BaseNetworkTest):
+ """Tests the following operations in the tags API:
+
+ Update all tags.
+ Delete all tags.
+ Check tag existence.
+ Create a tag.
+ List tags.
+ Remove a tag.
+
+ v2.0 of the Neutron API is assumed. The tag extension allows users to set
+ tags on their networks. The extension supports networks only.
+ """
+
+ @classmethod
+ def skip_checks(cls):
+ super(TagsTest, cls).skip_checks()
+ if not utils.is_extension_enabled('tag', 'network'):
+ msg = "tag extension not enabled."
+ raise cls.skipException(msg)
+
+ @classmethod
+ def resource_setup(cls):
+ super(TagsTest, cls).resource_setup()
+ cls.network = cls.create_network()
+
+ @decorators.idempotent_id('ee76bfaf-ac94-4d74-9ecc-4bbd4c583cb1')
+ def test_create_list_show_update_delete_tags(self):
+ # Validate that creating a tag on a network resource works.
+ tag_name = data_utils.rand_name(self.__class__.__name__ + '-Tag')
+ self.tags_client.create_tag('networks', self.network['id'], tag_name)
+ self.addCleanup(self.tags_client.delete_all_tags, 'networks',
+ self.network['id'])
+ self.tags_client.check_tag_existence('networks', self.network['id'],
+ tag_name)
+
+ # Validate that listing tags on a network resource works.
+ retrieved_tags = self.tags_client.list_tags(
+ 'networks', self.network['id'])['tags']
+ self.assertEqual([tag_name], retrieved_tags)
+
+ # Generate 3 new tag names.
+ replace_tags = [data_utils.rand_name(
+ self.__class__.__name__ + '-Tag') for _ in range(3)]
+
+ # Replace the current tag with the 3 new tags and validate that the
+ # network resource has the 3 new tags.
+ updated_tags = self.tags_client.update_all_tags(
+ 'networks', self.network['id'], replace_tags)['tags']
+ self.assertEqual(3, len(updated_tags))
+ self.assertEqual(set(replace_tags), set(updated_tags))
+
+ # Delete the first tag and check that it has been removed.
+ self.tags_client.delete_tag(
+ 'networks', self.network['id'], replace_tags[0])
+ self.assertRaises(lib_exc.NotFound,
+ self.tags_client.check_tag_existence, 'networks',
+ self.network['id'], replace_tags[0])
+ for i in range(1, 3):
+ self.tags_client.check_tag_existence(
+ 'networks', self.network['id'], replace_tags[i])
+
+ # Delete all the remaining tags and check that they have been removed.
+ self.tags_client.delete_all_tags('networks', self.network['id'])
+ for i in range(1, 3):
+ self.assertRaises(lib_exc.NotFound,
+ self.tags_client.check_tag_existence, 'networks',
+ self.network['id'], replace_tags[i])
+
+
+class TagsExtTest(base.BaseNetworkTest):
+ """Tests the following operations in the tags API:
+
+ Update all tags.
+ Delete all tags.
+ Check tag existence.
+ Create a tag.
+ List tags.
+ Remove a tag.
+
+ v2.0 of the Neutron API is assumed. The tag-ext extension allows users to
+ set tags on the following resources: subnets, ports, routers and
+ subnetpools.
+ """
+
+ # NOTE(felipemonteiro): The supported resource names are plural. Use
+ # the singular case for the corresponding class resource object.
+ SUPPORTED_RESOURCES = ['subnets', 'ports', 'routers', 'subnetpools']
+
+ @classmethod
+ def skip_checks(cls):
+ super(TagsExtTest, cls).skip_checks()
+ if not utils.is_extension_enabled('tag-ext', 'network'):
+ msg = "tag-ext extension not enabled."
+ raise cls.skipException(msg)
+
+ @classmethod
+ def resource_setup(cls):
+ super(TagsExtTest, cls).resource_setup()
+ cls.network = cls.create_network()
+ cls.subnet = cls.create_subnet(cls.network)
+ cls.port = cls.create_port(cls.network)
+ cls.router = cls.create_router()
+
+ subnetpool_name = data_utils.rand_name(cls.__name__ + '-Subnetpool')
+ prefix = CONF.network.default_network
+ cls.subnetpool = cls.subnetpools_client.create_subnetpool(
+ name=subnetpool_name, prefixes=prefix)['subnetpool']
+ cls.addClassResourceCleanup(cls.subnetpools_client.delete_subnetpool,
+ cls.subnetpool['id'])
+
+ def _create_tags_for_each_resource(self):
+ # Create a tag for each resource in `SUPPORTED_RESOURCES` and return
+ # the list of tag names.
+ tag_names = []
+
+ for resource in self.SUPPORTED_RESOURCES:
+ tag_name = data_utils.rand_name(self.__class__.__name__ + '-Tag')
+ tag_names.append(tag_name)
+ resource_object = getattr(self, resource[:-1])
+
+ self.tags_client.create_tag(resource, resource_object['id'],
+ tag_name)
+ self.addCleanup(self.tags_client.delete_all_tags, resource,
+ resource_object['id'])
+
+ return tag_names
+
+ @decorators.idempotent_id('c6231efa-9a89-4adf-b050-2a3156b8a1d9')
+ def test_create_check_list_and_delete_tags(self):
+ tag_names = self._create_tags_for_each_resource()
+
+ for i, resource in enumerate(self.SUPPORTED_RESOURCES):
+ # Ensure that a tag was created for each resource.
+ resource_object = getattr(self, resource[:-1])
+ retrieved_tags = self.tags_client.list_tags(
+ resource, resource_object['id'])['tags']
+ self.assertEqual(1, len(retrieved_tags))
+ self.assertEqual(tag_names[i], retrieved_tags[0])
+
+ # Check that the expected tag exists for each resource.
+ self.tags_client.check_tag_existence(
+ resource, resource_object['id'], tag_names[i])
+
+ # Delete the tag and ensure it was deleted.
+ self.tags_client.delete_tag(
+ resource, resource_object['id'], tag_names[i])
+ retrieved_tags = self.tags_client.list_tags(
+ resource, resource_object['id'])['tags']
+ self.assertEmpty(retrieved_tags)
+
+ @decorators.idempotent_id('663a90f5-f334-4b44-afe0-c5fc1d408791')
+ def test_update_and_delete_all_tags(self):
+ self._create_tags_for_each_resource()
+
+ for resource in self.SUPPORTED_RESOURCES:
+ # Generate 3 new tag names.
+ replace_tags = [data_utils.rand_name(
+ self.__class__.__name__ + '-Tag') for _ in range(3)]
+
+ # Replace the current tag with the 3 new tags and validate that the
+ # current resource has the 3 new tags.
+ resource_object = getattr(self, resource[:-1])
+ updated_tags = self.tags_client.update_all_tags(
+ resource, resource_object['id'], replace_tags)['tags']
+ self.assertEqual(3, len(updated_tags))
+ self.assertEqual(set(replace_tags), set(updated_tags))
+
+ # Delete all the tags and check that they have been removed.
+ self.tags_client.delete_all_tags(resource, resource_object['id'])
+ for i in range(3):
+ self.assertRaises(
+ lib_exc.NotFound, self.tags_client.check_tag_existence,
+ resource, resource_object['id'], replace_tags[i])
diff --git a/tempest/api/network/test_versions.py b/tempest/api/network/test_versions.py
new file mode 100644
index 0000000..2f01e50
--- /dev/null
+++ b/tempest/api/network/test_versions.py
@@ -0,0 +1,40 @@
+# Copyright 2016 VMware, Inc.
+#
+# 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.network import base
+from tempest.lib import decorators
+
+
+class NetworksApiDiscovery(base.BaseNetworkTest):
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('cac8a836-c2e0-4304-b556-cd299c7281d1')
+ def test_api_version_resources(self):
+ """Test that GET / returns expected resources.
+
+ The versions document returned by Neutron returns a few other
+ resources other than just available API versions: it also
+ states the status of each API version and provides links to
+ schema.
+ """
+
+ result = self.network_versions_client.list_versions()
+ expected_versions = ('v2.0')
+ expected_resources = ('id', 'links', 'status')
+ received_list = result.values()
+
+ for item in received_list:
+ for version in item:
+ for resource in expected_resources:
+ self.assertIn(resource, version)
+ self.assertIn(version['id'], expected_versions)
diff --git a/tempest/api/object_storage/base.py b/tempest/api/object_storage/base.py
index 2621581..e8f3f8b 100644
--- a/tempest/api/object_storage/base.py
+++ b/tempest/api/object_storage/base.py
@@ -13,15 +13,53 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
+import time
from tempest.common import custom_matchers
from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import exceptions as lib_exc
import tempest.test
CONF = config.CONF
+def delete_containers(containers, container_client, object_client):
+ """Remove containers and all objects in them.
+
+ The containers should be visible from the container_client given.
+ Will not throw any error if the containers don't exist.
+ Will not check that object and container deletions succeed.
+ After delete all the objects from a container, it will wait 2
+ seconds before delete the container itself, in order to deployments
+ using HA proxy sync the deletion properly, otherwise, the container
+ might fail to be deleted because it's not empty.
+
+ :param containers: List of containers(or string of a container)
+ to be deleted
+ :param container_client: Client to be used to delete containers
+ :param object_client: Client to be used to delete objects
+ """
+ if isinstance(containers, str):
+ containers = [containers]
+
+ for cont in containers:
+ try:
+ params = {'limit': 9999, 'format': 'json'}
+ _, objlist = container_client.list_container_objects(cont, params)
+ # delete every object in the container
+ for obj in objlist:
+ test_utils.call_and_ignore_notfound_exc(
+ object_client.delete_object, cont, obj['name'])
+ # sleep 2 seconds to sync the deletion of the objects
+ # in HA deployment
+ time.sleep(2)
+ container_client.delete_container(cont)
+ except lib_exc.NotFound:
+ pass
+
+
class BaseObjectTest(tempest.test.BaseTestCase):
credentials = [['operator', CONF.object_storage.operator_role]]
@@ -37,16 +75,15 @@
def setup_credentials(cls):
cls.set_network_resources()
super(BaseObjectTest, cls).setup_credentials()
- # credentials may be overwritten by children classes
- if hasattr(cls, 'os_roles_operator'):
- cls.os = cls.os_roles_operator
@classmethod
def setup_clients(cls):
super(BaseObjectTest, cls).setup_clients()
- cls.object_client = cls.os.object_client
- cls.container_client = cls.os.container_client
- cls.account_client = cls.os.account_client
+ cls.object_client = cls.os_roles_operator.object_client
+ cls.bulk_client = cls.os_roles_operator.bulk_client
+ cls.container_client = cls.os_roles_operator.container_client
+ cls.account_client = cls.os_roles_operator.account_client
+ cls.capabilities_client = cls.os_roles_operator.capabilities_client
@classmethod
def resource_setup(cls):
@@ -57,41 +94,53 @@
cls.container_client.auth_provider.clear_auth()
cls.account_client.auth_provider.clear_auth()
+ # make sure that discoverability is enabled and that the sections
+ # have not been disallowed by Swift
+ cls.policies = None
+
+ if CONF.object_storage_feature_enabled.discoverability:
+ body = cls.capabilities_client.list_capabilities()
+
+ if 'swift' in body and 'policies' in body['swift']:
+ cls.policies = body['swift']['policies']
+
+ cls.containers = []
+
@classmethod
- def delete_containers(cls, containers, container_client=None,
- object_client=None):
- """Remove given containers and all objects in them.
+ def create_container(cls):
+ # wrapper that returns a test container
+ container_name = data_utils.rand_name(name='TestContainer')
+ cls.container_client.update_container(container_name)
+ cls.containers.append(container_name)
- The containers should be visible from the container_client given.
- Will not throw any error if the containers don't exist.
- Will not check that object and container deletions succeed.
+ return container_name
- :param containers: list of container names to remove
- :param container_client: if None, use cls.container_client, this means
- that the default testing user will be used (see 'username' in
- 'etc/tempest.conf')
- :param object_client: if None, use cls.object_client
- """
+ @classmethod
+ def create_object(cls, container_name, object_name=None,
+ data=None, metadata=None):
+ # wrapper that returns a test object
+ if object_name is None:
+ object_name = data_utils.rand_name(name='TestObject')
+ if data is None:
+ data = data_utils.random_bytes()
+ cls.object_client.create_object(container_name,
+ object_name,
+ data,
+ metadata=metadata)
+
+ return object_name, data
+
+ @classmethod
+ def delete_containers(cls, container_client=None, object_client=None):
if container_client is None:
container_client = cls.container_client
if object_client is None:
object_client = cls.object_client
- for cont in containers:
- try:
- objlist = container_client.list_all_container_objects(cont)
- # delete every object in the container
- for obj in objlist:
- try:
- object_client.delete_object(cont, obj['name'])
- except lib_exc.NotFound:
- pass
- container_client.delete_container(cont)
- except lib_exc.NotFound:
- pass
+ delete_containers(cls.containers, container_client, object_client)
def assertHeaders(self, resp, target, method):
"""Check the existence and the format of response headers"""
self.assertThat(resp, custom_matchers.ExistsAllResponseHeaders(
- target, method))
+ target, method, self.policies))
self.assertThat(resp, custom_matchers.AreAllWellFormatted())
diff --git a/tempest/api/object_storage/test_account_bulk.py b/tempest/api/object_storage/test_account_bulk.py
index da4c80c..6599e43 100644
--- a/tempest/api/object_storage/test_account_bulk.py
+++ b/tempest/api/object_storage/test_account_bulk.py
@@ -17,7 +17,8 @@
from tempest.api.object_storage import base
from tempest.common import custom_matchers
-from tempest import test
+from tempest.common import utils
+from tempest.lib import decorators
class BulkTest(base.BaseObjectTest):
@@ -27,7 +28,10 @@
self.containers = []
def tearDown(self):
- self.delete_containers(self.containers)
+ # NOTE(andreaf) BulkTests needs to cleanup containers after each
+ # test is executed.
+ base.delete_containers(self.containers, self.container_client,
+ self.object_client)
super(BulkTest, self).tearDown()
def _create_archive(self):
@@ -52,12 +56,11 @@
def _upload_archive(self, filepath):
# upload an archived file
- params = {'extract-archive': 'tar'}
with open(filepath) as fh:
mydata = fh.read()
- resp, body = self.account_client.create_account(data=mydata,
- params=params)
- return resp, body
+ resp = self.bulk_client.upload_archive(
+ upload_path='', data=mydata, archive_file_format='tar')
+ return resp
def _check_contents_deleted(self, container_name):
param = {'format': 'txt'}
@@ -65,26 +68,25 @@
self.assertHeaders(resp, 'Account', 'GET')
self.assertNotIn(container_name, body)
- @test.idempotent_id('a407de51-1983-47cc-9f14-47c2b059413c')
- @test.requires_ext(extension='bulk', service='object')
+ @decorators.idempotent_id('a407de51-1983-47cc-9f14-47c2b059413c')
+ @utils.requires_ext(extension='bulk_upload', service='object')
def test_extract_archive(self):
# Test bulk operation of file upload with an archived file
filepath, container_name, object_name = self._create_archive()
- resp, _ = self._upload_archive(filepath)
-
+ resp = self._upload_archive(filepath)
self.containers.append(container_name)
# When uploading an archived file with the bulk operation, the response
# does not contain 'content-length' header. This is the special case,
# therefore the existence of response headers is checked without
# custom matcher.
- self.assertIn('transfer-encoding', resp)
- self.assertIn('content-type', resp)
- self.assertIn('x-trans-id', resp)
- self.assertIn('date', resp)
+ self.assertIn('transfer-encoding', resp.response)
+ self.assertIn('content-type', resp.response)
+ self.assertIn('x-trans-id', resp.response)
+ self.assertIn('date', resp.response)
# Check only the format of common headers with custom matcher
- self.assertThat(resp, custom_matchers.AreAllWellFormatted())
+ self.assertThat(resp.response, custom_matchers.AreAllWellFormatted())
param = {'format': 'json'}
resp, body = self.account_client.list_account_containers(param)
@@ -94,64 +96,60 @@
self.assertIn(container_name, [b['name'] for b in body])
param = {'format': 'json'}
- resp, contents_list = self.container_client.list_container_contents(
+ resp, contents_list = self.container_client.list_container_objects(
container_name, param)
self.assertHeaders(resp, 'Container', 'GET')
self.assertIn(object_name, [c['name'] for c in contents_list])
- @test.idempotent_id('c075e682-0d2a-43b2-808d-4116200d736d')
- @test.requires_ext(extension='bulk', service='object')
+ @decorators.idempotent_id('c075e682-0d2a-43b2-808d-4116200d736d')
+ @utils.requires_ext(extension='bulk_delete', service='object')
def test_bulk_delete(self):
# Test bulk operation of deleting multiple files
filepath, container_name, object_name = self._create_archive()
self._upload_archive(filepath)
data = '%s/%s\n%s' % (container_name, object_name, container_name)
- params = {'bulk-delete': ''}
- resp, body = self.account_client.delete_account(data=data,
- params=params)
+ resp = self.bulk_client.delete_bulk_data(data=data)
# When deleting multiple files using the bulk operation, the response
# does not contain 'content-length' header. This is the special case,
# therefore the existence of response headers is checked without
# custom matcher.
- self.assertIn('transfer-encoding', resp)
- self.assertIn('content-type', resp)
- self.assertIn('x-trans-id', resp)
- self.assertIn('date', resp)
+ self.assertIn('transfer-encoding', resp.response)
+ self.assertIn('content-type', resp.response)
+ self.assertIn('x-trans-id', resp.response)
+ self.assertIn('date', resp.response)
# Check only the format of common headers with custom matcher
- self.assertThat(resp, custom_matchers.AreAllWellFormatted())
+ self.assertThat(resp.response, custom_matchers.AreAllWellFormatted())
# Check if uploaded contents are completely deleted
self._check_contents_deleted(container_name)
- @test.idempotent_id('dbea2bcb-efbb-4674-ac8a-a5a0e33d1d79')
- @test.requires_ext(extension='bulk', service='object')
+ @decorators.idempotent_id('dbea2bcb-efbb-4674-ac8a-a5a0e33d1d79')
+ @utils.requires_ext(extension='bulk_delete', service='object')
def test_bulk_delete_by_POST(self):
# Test bulk operation of deleting multiple files
filepath, container_name, object_name = self._create_archive()
self._upload_archive(filepath)
data = '%s/%s\n%s' % (container_name, object_name, container_name)
- params = {'bulk-delete': ''}
- resp, body = self.account_client.create_account_metadata(
- {}, data=data, params=params)
+ resp = self.bulk_client.delete_bulk_data_with_post(data=data)
# When deleting multiple files using the bulk operation, the response
# does not contain 'content-length' header. This is the special case,
# therefore the existence of response headers is checked without
# custom matcher.
- self.assertIn('transfer-encoding', resp)
- self.assertIn('content-type', resp)
- self.assertIn('x-trans-id', resp)
- self.assertIn('date', resp)
+ self.assertIn('transfer-encoding', resp.response)
+ self.assertIn('content-type', resp.response)
+ self.assertIn('x-trans-id', resp.response)
+ self.assertIn('date', resp.response)
# Check only the format of common headers with custom matcher
- self.assertThat(resp, custom_matchers.AreAllWellFormatted())
+ self.assertThat(resp.response, custom_matchers.AreAllWellFormatted())
# Check if uploaded contents are completely deleted
self._check_contents_deleted(container_name)
diff --git a/tempest/api/object_storage/test_account_quotas.py b/tempest/api/object_storage/test_account_quotas.py
index 0f6a330..48f42ec 100644
--- a/tempest/api/object_storage/test_account_quotas.py
+++ b/tempest/api/object_storage/test_account_quotas.py
@@ -13,9 +13,10 @@
# under the License.
from tempest.api.object_storage import base
-from tempest.common.utils import data_utils
+from tempest.common import utils
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
CONF = config.CONF
@@ -28,14 +29,12 @@
@classmethod
def setup_credentials(cls):
super(AccountQuotasTest, cls).setup_credentials()
- cls.os = cls.os_roles_operator
cls.os_reselleradmin = cls.os_roles_reseller
@classmethod
def resource_setup(cls):
super(AccountQuotasTest, cls).resource_setup()
- cls.container_name = data_utils.rand_name(name="TestContainer")
- cls.container_client.create_container(cls.container_name)
+ cls.container_name = cls.create_container()
# Retrieve a ResellerAdmin auth data and use it to set a quota
# on the client's account
@@ -54,8 +53,8 @@
# Set a quota of 20 bytes on the user's account before each test
headers = {"X-Account-Meta-Quota-Bytes": "20"}
- self.os.account_client.request("POST", url="", headers=headers,
- body="")
+ self.os_roles_operator.account_client.request(
+ "POST", url="", headers=headers, body="")
def tearDown(self):
# Set the reselleradmin auth in headers for next account_client
@@ -67,19 +66,18 @@
# remove the quota from the container
headers = {"X-Remove-Account-Meta-Quota-Bytes": "x"}
- self.os.account_client.request("POST", url="", headers=headers,
- body="")
+ self.os_roles_operator.account_client.request(
+ "POST", url="", headers=headers, body="")
super(AccountQuotasTest, self).tearDown()
@classmethod
def resource_cleanup(cls):
- if hasattr(cls, "container_name"):
- cls.delete_containers([cls.container_name])
+ cls.delete_containers()
super(AccountQuotasTest, cls).resource_cleanup()
- @test.attr(type="smoke")
- @test.idempotent_id('a22ef352-a342-4587-8f47-3bbdb5b039c4')
- @test.requires_ext(extension='account_quotas', service='object')
+ @decorators.attr(type="smoke")
+ @decorators.idempotent_id('a22ef352-a342-4587-8f47-3bbdb5b039c4')
+ @utils.requires_ext(extension='account_quotas', service='object')
def test_upload_valid_object(self):
object_name = data_utils.rand_name(name="TestObject")
data = data_utils.arbitrary_string()
@@ -88,9 +86,9 @@
self.assertHeaders(resp, 'Object', 'PUT')
- @test.attr(type=["smoke"])
- @test.idempotent_id('63f51f9f-5f1d-4fc6-b5be-d454d70949d6')
- @test.requires_ext(extension='account_quotas', service='object')
+ @decorators.attr(type=["smoke"])
+ @decorators.idempotent_id('63f51f9f-5f1d-4fc6-b5be-d454d70949d6')
+ @utils.requires_ext(extension='account_quotas', service='object')
def test_admin_modify_quota(self):
"""Test ResellerAdmin can modify/remove the quota on a user's account
@@ -109,9 +107,8 @@
)
headers = {"X-Account-Meta-Quota-Bytes": quota}
- resp, _ = self.os.account_client.request("POST", url="",
- headers=headers,
- body="")
+ resp, _ = self.os_roles_operator.account_client.request(
+ "POST", url="", headers=headers, body="")
self.assertEqual(resp["status"], "204")
self.assertHeaders(resp, 'Account', 'POST')
diff --git a/tempest/api/object_storage/test_account_quotas_negative.py b/tempest/api/object_storage/test_account_quotas_negative.py
index aee17d3..798926b 100644
--- a/tempest/api/object_storage/test_account_quotas_negative.py
+++ b/tempest/api/object_storage/test_account_quotas_negative.py
@@ -12,13 +12,11 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import decorators
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.object_storage import base
-from tempest.common.utils import data_utils
+from tempest.common import utils
from tempest import config
-from tempest import test
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
CONF = config.CONF
@@ -31,14 +29,12 @@
@classmethod
def setup_credentials(cls):
super(AccountQuotasNegativeTest, cls).setup_credentials()
- cls.os = cls.os_roles_operator
cls.os_reselleradmin = cls.os_roles_reseller
@classmethod
def resource_setup(cls):
super(AccountQuotasNegativeTest, cls).resource_setup()
- cls.container_name = data_utils.rand_name(name="TestContainer")
- cls.container_client.create_container(cls.container_name)
+ cls.create_container()
# Retrieve a ResellerAdmin auth data and use it to set a quota
# on the client's account
@@ -56,8 +52,8 @@
# Set a quota of 20 bytes on the user's account before each test
headers = {"X-Account-Meta-Quota-Bytes": "20"}
- self.os.account_client.request("POST", url="", headers=headers,
- body="")
+ self.os_roles_operator.account_client.request(
+ "POST", url="", headers=headers, body="")
def tearDown(self):
# Set the reselleradmin auth in headers for next account_client
@@ -69,39 +65,29 @@
# remove the quota from the container
headers = {"X-Remove-Account-Meta-Quota-Bytes": "x"}
- self.os.account_client.request("POST", url="", headers=headers,
- body="")
+ self.os_roles_operator.account_client.request(
+ "POST", url="", headers=headers, body="")
super(AccountQuotasNegativeTest, self).tearDown()
@classmethod
def resource_cleanup(cls):
- if hasattr(cls, "container_name"):
- cls.delete_containers([cls.container_name])
+ cls.delete_containers()
super(AccountQuotasNegativeTest, cls).resource_cleanup()
- @test.attr(type=["negative"])
- @test.idempotent_id('d1dc5076-555e-4e6d-9697-28f1fe976324')
- @test.requires_ext(extension='account_quotas', service='object')
+ @decorators.attr(type=["negative"])
+ @decorators.idempotent_id('d1dc5076-555e-4e6d-9697-28f1fe976324')
+ @utils.requires_ext(extension='account_quotas', service='object')
def test_user_modify_quota(self):
"""Test that a user cannot modify or remove a quota on its account."""
# Not able to remove quota
- self.assertRaises(lib_exc.Forbidden,
- self.account_client.create_account_metadata,
- {"Quota-Bytes": ""})
+ self.assertRaises(
+ lib_exc.Forbidden,
+ self.account_client.create_update_or_delete_account_metadata,
+ create_update_metadata={"Quota-Bytes": ""})
# Not able to modify quota
- self.assertRaises(lib_exc.Forbidden,
- self.account_client.create_account_metadata,
- {"Quota-Bytes": "100"})
-
- @test.attr(type=["negative"])
- @decorators.skip_because(bug="1310597")
- @test.idempotent_id('cf9e21f5-3aa4-41b1-9462-28ac550d8d3f')
- @test.requires_ext(extension='account_quotas', service='object')
- def test_upload_large_object(self):
- object_name = data_utils.rand_name(name="TestObject")
- data = data_utils.arbitrary_string(30)
- self.assertRaises(lib_exc.OverLimit,
- self.object_client.create_object,
- self.container_name, object_name, data)
+ self.assertRaises(
+ lib_exc.Forbidden,
+ self.account_client.create_update_or_delete_account_metadata,
+ create_update_metadata={"Quota-Bytes": "100"})
diff --git a/tempest/api/object_storage/test_account_services.py b/tempest/api/object_storage/test_account_services.py
index 6bab9b3..c5c30e3 100644
--- a/tempest/api/object_storage/test_account_services.py
+++ b/tempest/api/object_storage/test_account_services.py
@@ -15,14 +15,14 @@
import random
-from six import moves
+import six
import testtools
from tempest.api.object_storage import base
from tempest.common import custom_matchers
-from tempest.common.utils import data_utils
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
CONF = config.CONF
@@ -36,35 +36,35 @@
@classmethod
def setup_credentials(cls):
super(AccountTest, cls).setup_credentials()
- cls.os = cls.os_roles_operator
cls.os_operator = cls.os_roles_operator_alt
@classmethod
def resource_setup(cls):
super(AccountTest, cls).resource_setup()
- for i in moves.xrange(ord('a'), ord('f') + 1):
- name = data_utils.rand_name(name='%s-' % chr(i))
- cls.container_client.create_container(name)
+ for i in range(ord('a'), ord('f') + 1):
+ name = data_utils.rand_name(name='%s-' % six.int2byte(i))
+ cls.container_client.update_container(name)
+ cls.addClassResourceCleanup(base.delete_containers,
+ [name],
+ cls.container_client,
+ cls.object_client)
cls.containers.append(name)
cls.containers_count = len(cls.containers)
- @classmethod
- def resource_cleanup(cls):
- cls.delete_containers(cls.containers)
- super(AccountTest, cls).resource_cleanup()
-
- @test.attr(type='smoke')
- @test.idempotent_id('3499406a-ae53-4f8c-b43a-133d4dc6fe3f')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('3499406a-ae53-4f8c-b43a-133d4dc6fe3f')
def test_list_containers(self):
# list of all containers should not be empty
resp, container_list = self.account_client.list_account_containers()
self.assertHeaders(resp, 'Account', 'GET')
self.assertIsNotNone(container_list)
- for container_name in self.containers:
- self.assertIn(container_name, container_list)
- @test.idempotent_id('884ec421-fbad-4fcc-916b-0580f2699565')
+ for container_name in self.containers:
+ self.assertIn(six.text_type(container_name).encode('utf-8'),
+ container_list)
+
+ @decorators.idempotent_id('884ec421-fbad-4fcc-916b-0580f2699565')
def test_list_no_containers(self):
# List request to empty account
@@ -78,7 +78,16 @@
# container request, the response does not contain 'accept-ranges'
# header. This is a special case, therefore the existence of response
# headers is checked without custom matcher.
- self.assertIn('content-length', resp)
+ #
+ # As the expected response is 204 No Content, Content-Length presence
+ # is not checked here intentionally. According to RFC 7230 a server
+ # MUST NOT send the header in such responses. Thus, clients should not
+ # depend on this header. However, the standard does not require them
+ # to validate the server's behavior. We leverage that to not refuse
+ # any implementation violating it like Swift [1] or some versions of
+ # Ceph RadosGW [2].
+ # [1] https://bugs.launchpad.net/swift/+bug/1537811
+ # [2] http://tracker.ceph.com/issues/13582
self.assertIn('x-timestamp', resp)
self.assertIn('x-account-bytes-used', resp)
self.assertIn('x-account-container-count', resp)
@@ -90,9 +99,9 @@
# Check only the format of common headers with custom matcher
self.assertThat(resp, custom_matchers.AreAllWellFormatted())
- self.assertEqual(len(container_list), 0)
+ self.assertEmpty(container_list)
- @test.idempotent_id('1c7efa35-e8a2-4b0b-b5ff-862c7fd83704')
+ @decorators.idempotent_id('1c7efa35-e8a2-4b0b-b5ff-862c7fd83704')
def test_list_containers_with_format_json(self):
# list containers setting format parameter to 'json'
params = {'format': 'json'}
@@ -104,7 +113,7 @@
self.assertTrue([c['count'] for c in container_list])
self.assertTrue([c['bytes'] for c in container_list])
- @test.idempotent_id('4477b609-1ca6-4d4b-b25d-ad3f01086089')
+ @decorators.idempotent_id('4477b609-1ca6-4d4b-b25d-ad3f01086089')
def test_list_containers_with_format_xml(self):
# list containers setting format parameter to 'xml'
params = {'format': 'xml'}
@@ -113,25 +122,26 @@
self.assertHeaders(resp, 'Account', 'GET')
self.assertIsNotNone(container_list)
self.assertEqual(container_list.tag, 'account')
- self.assertTrue('name' in container_list.keys())
+ self.assertIn('name', container_list.keys())
self.assertEqual(container_list.find(".//container").tag, 'container')
self.assertEqual(container_list.find(".//name").tag, 'name')
self.assertEqual(container_list.find(".//count").tag, 'count')
self.assertEqual(container_list.find(".//bytes").tag, 'bytes')
- @test.idempotent_id('6eb04a6a-4860-4e31-ba91-ea3347d76b58')
+ @decorators.idempotent_id('6eb04a6a-4860-4e31-ba91-ea3347d76b58')
@testtools.skipIf(
not CONF.object_storage_feature_enabled.discoverability,
'Discoverability function is disabled')
def test_list_extensions(self):
- resp, extensions = self.account_client.list_extensions()
+ resp = self.capabilities_client.list_capabilities()
self.assertThat(resp, custom_matchers.AreAllWellFormatted())
- @test.idempotent_id('5cfa4ab2-4373-48dd-a41f-a532b12b08b2')
+ @decorators.idempotent_id('5cfa4ab2-4373-48dd-a41f-a532b12b08b2')
def test_list_containers_with_limit(self):
# list containers one of them, half of them then all of them
- for limit in (1, self.containers_count / 2, self.containers_count):
+ for limit in (1, self.containers_count // 2,
+ self.containers_count):
params = {'limit': limit}
resp, container_list = \
self.account_client.list_account_containers(params=params)
@@ -139,7 +149,7 @@
self.assertEqual(len(container_list), limit)
- @test.idempotent_id('638f876d-6a43-482a-bbb3-0840bca101c6')
+ @decorators.idempotent_id('638f876d-6a43-482a-bbb3-0840bca101c6')
def test_list_containers_with_marker(self):
# list containers using marker param
# first expect to get 0 container as we specified last
@@ -150,16 +160,17 @@
self.account_client.list_account_containers(params=params)
self.assertHeaders(resp, 'Account', 'GET')
- self.assertEqual(len(container_list), 0)
+ self.assertEmpty(container_list)
- params = {'marker': self.containers[self.containers_count / 2]}
+ params = {'marker': self.containers[self.containers_count // 2]}
resp, container_list = \
self.account_client.list_account_containers(params=params)
self.assertHeaders(resp, 'Account', 'GET')
- self.assertEqual(len(container_list), self.containers_count / 2 - 1)
+ self.assertEqual(len(container_list),
+ self.containers_count // 2 - 1)
- @test.idempotent_id('5ca164e4-7bde-43fa-bafb-913b53b9e786')
+ @decorators.idempotent_id('5ca164e4-7bde-43fa-bafb-913b53b9e786')
def test_list_containers_with_end_marker(self):
# list containers using end_marker param
# first expect to get 0 container as we specified first container as
@@ -169,15 +180,15 @@
resp, container_list = \
self.account_client.list_account_containers(params=params)
self.assertHeaders(resp, 'Account', 'GET')
- self.assertEqual(len(container_list), 0)
+ self.assertEmpty(container_list)
- params = {'end_marker': self.containers[self.containers_count / 2]}
+ params = {'end_marker': self.containers[self.containers_count // 2]}
resp, container_list = \
self.account_client.list_account_containers(params=params)
self.assertHeaders(resp, 'Account', 'GET')
- self.assertEqual(len(container_list), self.containers_count / 2)
+ self.assertEqual(len(container_list), self.containers_count // 2)
- @test.idempotent_id('ac8502c2-d4e4-4f68-85a6-40befea2ef5e')
+ @decorators.idempotent_id('ac8502c2-d4e4-4f68-85a6-40befea2ef5e')
def test_list_containers_with_marker_and_end_marker(self):
# list containers combining marker and end_marker param
params = {'marker': self.containers[0],
@@ -187,7 +198,7 @@
self.assertHeaders(resp, 'Account', 'GET')
self.assertEqual(len(container_list), self.containers_count - 2)
- @test.idempotent_id('f7064ae8-dbcc-48da-b594-82feef6ea5af')
+ @decorators.idempotent_id('f7064ae8-dbcc-48da-b594-82feef6ea5af')
def test_list_containers_with_limit_and_marker(self):
# list containers combining marker and limit param
# result are always limitated by the limit whatever the marker
@@ -199,21 +210,22 @@
self.account_client.list_account_containers(params=params)
self.assertHeaders(resp, 'Account', 'GET')
- self.assertTrue(len(container_list) <= limit, str(container_list))
+ self.assertLessEqual(len(container_list), limit,
+ str(container_list))
- @test.idempotent_id('888a3f0e-7214-4806-8e50-5e0c9a69bb5e')
+ @decorators.idempotent_id('888a3f0e-7214-4806-8e50-5e0c9a69bb5e')
def test_list_containers_with_limit_and_end_marker(self):
# list containers combining limit and end_marker param
limit = random.randint(1, self.containers_count)
params = {'limit': limit,
- 'end_marker': self.containers[self.containers_count / 2]}
+ 'end_marker': self.containers[self.containers_count // 2]}
resp, container_list = self.account_client.list_account_containers(
params=params)
self.assertHeaders(resp, 'Account', 'GET')
self.assertEqual(len(container_list),
- min(limit, self.containers_count / 2))
+ min(limit, self.containers_count // 2))
- @test.idempotent_id('8cf98d9c-e3a0-4e44-971b-c87656fdddbd')
+ @decorators.idempotent_id('8cf98d9c-e3a0-4e44-971b-c87656fdddbd')
def test_list_containers_with_limit_and_marker_and_end_marker(self):
# list containers combining limit, marker and end_marker param
limit = random.randint(1, self.containers_count)
@@ -226,87 +238,122 @@
self.assertEqual(len(container_list),
min(limit, self.containers_count - 2))
- @test.attr(type='smoke')
- @test.idempotent_id('4894c312-6056-4587-8d6f-86ffbf861f80')
+ @decorators.idempotent_id('365e6fc7-1cfe-463b-a37c-8bd08d47b6aa')
+ def test_list_containers_with_prefix(self):
+ # list containers that have a name that starts with a prefix
+ prefix = 'tempest-a'
+ params = {'prefix': prefix}
+ resp, container_list = self.account_client.list_account_containers(
+ params=params)
+ self.assertHeaders(resp, 'Account', 'GET')
+ for container in container_list:
+ self.assertEqual(True, container.decode(
+ 'utf-8').startswith(prefix))
+
+ @decorators.idempotent_id('b1811cff-d1ed-4c15-a52e-efd8de41cf34')
+ def test_list_containers_reverse_order(self):
+ # list containers in reverse order
+ _, orig_container_list = self.account_client.list_account_containers()
+
+ params = {'reverse': True}
+ resp, container_list = self.account_client.list_account_containers(
+ params=params)
+ self.assertHeaders(resp, 'Account', 'GET')
+ self.assertEqual(sorted(orig_container_list, reverse=True),
+ container_list)
+
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('4894c312-6056-4587-8d6f-86ffbf861f80')
def test_list_account_metadata(self):
# list all account metadata
# set metadata to account
metadata = {'test-account-meta1': 'Meta1',
'test-account-meta2': 'Meta2'}
- resp, _ = self.account_client.create_account_metadata(metadata)
+ resp, _ = self.account_client.create_update_or_delete_account_metadata(
+ create_update_metadata=metadata)
resp, _ = self.account_client.list_account_metadata()
self.assertHeaders(resp, 'Account', 'HEAD')
self.assertIn('x-account-meta-test-account-meta1', resp)
self.assertIn('x-account-meta-test-account-meta2', resp)
- self.account_client.delete_account_metadata(metadata)
+ self.account_client.create_update_or_delete_account_metadata(
+ delete_metadata=metadata)
- @test.idempotent_id('b904c2e3-24c2-4dba-ad7d-04e90a761be5')
+ @decorators.idempotent_id('b904c2e3-24c2-4dba-ad7d-04e90a761be5')
def test_list_no_account_metadata(self):
# list no account metadata
resp, _ = self.account_client.list_account_metadata()
self.assertHeaders(resp, 'Account', 'HEAD')
self.assertNotIn('x-account-meta-', str(resp))
- @test.idempotent_id('e2a08b5f-3115-4768-a3ee-d4287acd6c08')
+ @decorators.idempotent_id('e2a08b5f-3115-4768-a3ee-d4287acd6c08')
def test_update_account_metadata_with_create_metadata(self):
# add metadata to account
metadata = {'test-account-meta1': 'Meta1'}
- resp, _ = self.account_client.create_account_metadata(metadata)
+ resp, _ = self.account_client.create_update_or_delete_account_metadata(
+ create_update_metadata=metadata)
self.assertHeaders(resp, 'Account', 'POST')
- resp, body = self.account_client.list_account_metadata()
+ resp, _ = self.account_client.list_account_metadata()
self.assertIn('x-account-meta-test-account-meta1', resp)
self.assertEqual(resp['x-account-meta-test-account-meta1'],
metadata['test-account-meta1'])
- self.account_client.delete_account_metadata(metadata)
+ self.account_client.create_update_or_delete_account_metadata(
+ delete_metadata=metadata)
- @test.idempotent_id('9f60348d-c46f-4465-ae06-d51dbd470953')
- def test_update_account_metadata_with_delete_matadata(self):
+ @decorators.idempotent_id('9f60348d-c46f-4465-ae06-d51dbd470953')
+ def test_update_account_metadata_with_delete_metadata(self):
# delete metadata from account
metadata = {'test-account-meta1': 'Meta1'}
- self.account_client.create_account_metadata(metadata)
- resp, _ = self.account_client.delete_account_metadata(metadata)
+ self.account_client.create_update_or_delete_account_metadata(
+ create_update_metadata=metadata)
+ resp, _ = self.account_client.create_update_or_delete_account_metadata(
+ delete_metadata=metadata)
self.assertHeaders(resp, 'Account', 'POST')
resp, _ = self.account_client.list_account_metadata()
self.assertNotIn('x-account-meta-test-account-meta1', resp)
- @test.idempotent_id('64fd53f3-adbd-4639-af54-436e4982dbfb')
- def test_update_account_metadata_with_create_matadata_key(self):
+ @decorators.idempotent_id('64fd53f3-adbd-4639-af54-436e4982dbfb')
+ def test_update_account_metadata_with_create_metadata_key(self):
# if the value of metadata is not set, the metadata is not
# registered at a server
metadata = {'test-account-meta1': ''}
- resp, _ = self.account_client.create_account_metadata(metadata)
+ resp, _ = self.account_client.create_update_or_delete_account_metadata(
+ create_update_metadata=metadata)
self.assertHeaders(resp, 'Account', 'POST')
resp, _ = self.account_client.list_account_metadata()
self.assertNotIn('x-account-meta-test-account-meta1', resp)
- @test.idempotent_id('d4d884d3-4696-4b85-bc98-4f57c4dd2bf1')
- def test_update_account_metadata_with_delete_matadata_key(self):
+ @decorators.idempotent_id('d4d884d3-4696-4b85-bc98-4f57c4dd2bf1')
+ def test_update_account_metadata_with_delete_metadata_key(self):
# Although the value of metadata is not set, the feature of
# deleting metadata is valid
metadata_1 = {'test-account-meta1': 'Meta1'}
- self.account_client.create_account_metadata(metadata_1)
+ self.account_client.create_update_or_delete_account_metadata(
+ create_update_metadata=metadata_1)
metadata_2 = {'test-account-meta1': ''}
- resp, _ = self.account_client.delete_account_metadata(metadata_2)
+ resp, _ = self.account_client.create_update_or_delete_account_metadata(
+ delete_metadata=metadata_2)
self.assertHeaders(resp, 'Account', 'POST')
resp, _ = self.account_client.list_account_metadata()
self.assertNotIn('x-account-meta-test-account-meta1', resp)
- @test.idempotent_id('8e5fc073-59b9-42ee-984a-29ed11b2c749')
+ @decorators.idempotent_id('8e5fc073-59b9-42ee-984a-29ed11b2c749')
def test_update_account_metadata_with_create_and_delete_metadata(self):
# Send a request adding and deleting metadata requests simultaneously
metadata_1 = {'test-account-meta1': 'Meta1'}
- self.account_client.create_account_metadata(metadata_1)
+ self.account_client.create_update_or_delete_account_metadata(
+ create_update_metadata=metadata_1)
metadata_2 = {'test-account-meta2': 'Meta2'}
- resp, body = self.account_client.create_and_delete_account_metadata(
- metadata_2,
- metadata_1)
+ resp, _ = (
+ self.account_client.create_update_or_delete_account_metadata(
+ create_update_metadata=metadata_2,
+ delete_metadata=metadata_1))
self.assertHeaders(resp, 'Account', 'POST')
resp, _ = self.account_client.list_account_metadata()
@@ -315,4 +362,5 @@
self.assertEqual(resp['x-account-meta-test-account-meta2'],
metadata_2['test-account-meta2'])
- self.account_client.delete_account_metadata(metadata_2)
+ self.account_client.create_update_or_delete_account_metadata(
+ delete_metadata=metadata_2)
diff --git a/tempest/api/object_storage/test_account_services_negative.py b/tempest/api/object_storage/test_account_services_negative.py
index 998c2bd..3e664d7 100644
--- a/tempest/api/object_storage/test_account_services_negative.py
+++ b/tempest/api/object_storage/test_account_services_negative.py
@@ -12,11 +12,10 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.object_storage import base
from tempest import config
-from tempest import test
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
CONF = config.CONF
@@ -29,11 +28,10 @@
@classmethod
def setup_credentials(cls):
super(AccountNegativeTest, cls).setup_credentials()
- cls.os = cls.os_roles_operator
cls.os_operator = cls.os_roles_operator_alt
- @test.attr(type=['negative'])
- @test.idempotent_id('070e6aca-6152-4867-868d-1118d68fb38c')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('070e6aca-6152-4867-868d-1118d68fb38c')
def test_list_containers_with_non_authorized_user(self):
# list containers using non-authorized user
diff --git a/tempest/api/object_storage/test_container_acl.py b/tempest/api/object_storage/test_container_acl.py
index c1b6711..765bc6d 100644
--- a/tempest/api/object_storage/test_container_acl.py
+++ b/tempest/api/object_storage/test_container_acl.py
@@ -14,9 +14,9 @@
# under the License.
from tempest.api.object_storage import base
-from tempest.common.utils import data_utils
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
CONF = config.CONF
@@ -26,69 +26,64 @@
credentials = [['operator', CONF.object_storage.operator_role],
['operator_alt', CONF.object_storage.operator_role]]
- @classmethod
- def setup_credentials(cls):
- super(ObjectTestACLs, cls).setup_credentials()
- cls.os = cls.os_roles_operator
- cls.os_operator = cls.os_roles_operator_alt
-
- @classmethod
- def resource_setup(cls):
- super(ObjectTestACLs, cls).resource_setup()
- cls.test_auth_data = cls.os_operator.auth_provider.auth_data
-
def setUp(self):
super(ObjectTestACLs, self).setUp()
- self.container_name = data_utils.rand_name(name='TestContainer')
- self.container_client.create_container(self.container_name)
+ self.container_name = self.create_container()
def tearDown(self):
- self.delete_containers([self.container_name])
+ self.delete_containers()
super(ObjectTestACLs, self).tearDown()
- @test.idempotent_id('a3270f3f-7640-4944-8448-c7ea783ea5b6')
+ @decorators.idempotent_id('a3270f3f-7640-4944-8448-c7ea783ea5b6')
def test_read_object_with_rights(self):
# attempt to read object using authorized user
# update X-Container-Read metadata ACL
- tenant_name = self.os_operator.credentials.tenant_name
- username = self.os_operator.credentials.username
+ tenant_name = self.os_roles_operator_alt.credentials.tenant_name
+ username = self.os_roles_operator_alt.credentials.username
cont_headers = {'X-Container-Read': tenant_name + ':' + username}
- resp_meta, body = self.container_client.update_container_metadata(
- self.container_name, metadata=cont_headers,
- metadata_prefix='')
+ container_client = self.os_roles_operator.container_client
+ resp_meta, _ = (
+ container_client.create_update_or_delete_container_metadata(
+ self.container_name, create_update_metadata=cont_headers,
+ create_update_metadata_prefix=''))
self.assertHeaders(resp_meta, 'Container', 'POST')
# create object
object_name = data_utils.rand_name(name='Object')
- resp, _ = self.object_client.create_object(self.container_name,
- object_name, 'data')
+ resp, _ = self.os_roles_operator.object_client.create_object(
+ self.container_name, object_name, 'data')
self.assertHeaders(resp, 'Object', 'PUT')
- # Trying to read the object with rights
- self.object_client.auth_provider.set_alt_auth_data(
+ # set alternative authentication data; cannot simply use the
+ # other object client.
+ self.os_roles_operator.object_client.auth_provider.set_alt_auth_data(
request_part='headers',
- auth_data=self.test_auth_data
- )
- resp, _ = self.object_client.get_object(
+ auth_data=self.os_roles_operator_alt.object_client.auth_provider.
+ auth_data)
+ resp, _ = self.os_roles_operator.object_client.get_object(
self.container_name, object_name)
self.assertHeaders(resp, 'Object', 'GET')
- @test.idempotent_id('aa58bfa5-40d9-4bc3-82b4-d07f4a9e392a')
+ @decorators.idempotent_id('aa58bfa5-40d9-4bc3-82b4-d07f4a9e392a')
def test_write_object_with_rights(self):
# attempt to write object using authorized user
# update X-Container-Write metadata ACL
- tenant_name = self.os_operator.credentials.tenant_name
- username = self.os_operator.credentials.username
+ tenant_name = self.os_roles_operator_alt.credentials.tenant_name
+ username = self.os_roles_operator_alt.credentials.username
cont_headers = {'X-Container-Write': tenant_name + ':' + username}
- resp_meta, body = self.container_client.update_container_metadata(
- self.container_name, metadata=cont_headers,
- metadata_prefix='')
+ container_client = self.os_roles_operator.container_client
+ resp_meta, _ = (
+ container_client.create_update_or_delete_container_metadata(
+ self.container_name, create_update_metadata=cont_headers,
+ create_update_metadata_prefix=''))
self.assertHeaders(resp_meta, 'Container', 'POST')
- # Trying to write the object with rights
- self.object_client.auth_provider.set_alt_auth_data(
+ # set alternative authentication data; cannot simply use the
+ # other object client.
+ self.os_roles_operator.object_client.auth_provider.set_alt_auth_data(
request_part='headers',
- auth_data=self.test_auth_data
- )
+ auth_data=self.os_roles_operator_alt.object_client.auth_provider.
+ auth_data)
+ # Trying to write the object with rights
object_name = data_utils.rand_name(name='Object')
- resp, _ = self.object_client.create_object(
+ resp, _ = self.os_roles_operator.object_client.create_object(
self.container_name,
object_name, 'data', headers={})
self.assertHeaders(resp, 'Object', 'PUT')
diff --git a/tempest/api/object_storage/test_container_acl_negative.py b/tempest/api/object_storage/test_container_acl_negative.py
index 3bb47f0..90b24b4 100644
--- a/tempest/api/object_storage/test_container_acl_negative.py
+++ b/tempest/api/object_storage/test_container_acl_negative.py
@@ -12,12 +12,11 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.object_storage import base
-from tempest.common.utils import data_utils
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
CONF = config.CONF
@@ -30,7 +29,6 @@
@classmethod
def setup_credentials(cls):
super(ObjectACLsNegativeTest, cls).setup_credentials()
- cls.os = cls.os_roles_operator
cls.os_operator = cls.os_roles_operator_alt
@classmethod
@@ -41,14 +39,14 @@
def setUp(self):
super(ObjectACLsNegativeTest, self).setUp()
self.container_name = data_utils.rand_name(name='TestContainer')
- self.container_client.create_container(self.container_name)
+ self.container_client.update_container(self.container_name)
def tearDown(self):
self.delete_containers([self.container_name])
super(ObjectACLsNegativeTest, self).tearDown()
- @test.attr(type=['negative'])
- @test.idempotent_id('af587587-0c24-4e15-9822-8352ce711013')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('af587587-0c24-4e15-9822-8352ce711013')
def test_write_object_without_using_creds(self):
# trying to create object with empty headers
# X-Auth-Token is not provided
@@ -61,13 +59,13 @@
self.object_client.create_object,
self.container_name, object_name, 'data', headers={})
- @test.attr(type=['negative'])
- @test.idempotent_id('af85af0b-a025-4e72-a90e-121babf55720')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('af85af0b-a025-4e72-a90e-121babf55720')
def test_delete_object_without_using_creds(self):
# create object
object_name = data_utils.rand_name(name='Object')
- resp, _ = self.object_client.create_object(self.container_name,
- object_name, 'data')
+ self.object_client.create_object(self.container_name, object_name,
+ 'data')
# trying to delete object with empty headers
# X-Auth-Token is not provided
self.object_client.auth_provider.set_alt_auth_data(
@@ -78,8 +76,8 @@
self.object_client.delete_object,
self.container_name, object_name)
- @test.attr(type=['negative'])
- @test.idempotent_id('63d84e37-55a6-42e2-9e5f-276e60e26a00')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('63d84e37-55a6-42e2-9e5f-276e60e26a00')
def test_write_object_with_non_authorized_user(self):
# attempt to upload another file using non-authorized user
# User provided token is forbidden. ACL are not set
@@ -93,8 +91,8 @@
self.object_client.create_object,
self.container_name, object_name, 'data', headers={})
- @test.attr(type=['negative'])
- @test.idempotent_id('abf63359-be52-4feb-87dd-447689fc77fd')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('abf63359-be52-4feb-87dd-447689fc77fd')
def test_read_object_with_non_authorized_user(self):
# attempt to read object using non-authorized user
# User provided token is forbidden. ACL are not set
@@ -111,8 +109,8 @@
self.object_client.get_object,
self.container_name, object_name)
- @test.attr(type=['negative'])
- @test.idempotent_id('7343ac3d-cfed-4198-9bb0-00149741a492')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('7343ac3d-cfed-4198-9bb0-00149741a492')
def test_delete_object_with_non_authorized_user(self):
# attempt to delete object using non-authorized user
# User provided token is forbidden. ACL are not set
@@ -129,15 +127,16 @@
self.object_client.delete_object,
self.container_name, object_name)
- @test.attr(type=['negative'])
- @test.idempotent_id('9ed01334-01e9-41ea-87ea-e6f465582823')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('9ed01334-01e9-41ea-87ea-e6f465582823')
def test_read_object_without_rights(self):
# attempt to read object using non-authorized user
# update X-Container-Read metadata ACL
cont_headers = {'X-Container-Read': 'badtenant:baduser'}
- resp_meta, body = self.container_client.update_container_metadata(
- self.container_name, metadata=cont_headers,
- metadata_prefix='')
+ resp_meta, _ = (
+ self.container_client.create_update_or_delete_container_metadata(
+ self.container_name, create_update_metadata=cont_headers,
+ create_update_metadata_prefix=''))
self.assertHeaders(resp_meta, 'Container', 'POST')
# create object
object_name = data_utils.rand_name(name='Object')
@@ -153,15 +152,16 @@
self.object_client.get_object,
self.container_name, object_name)
- @test.attr(type=['negative'])
- @test.idempotent_id('a3a585a7-d8cf-4b65-a1a0-edc2b1204f85')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('a3a585a7-d8cf-4b65-a1a0-edc2b1204f85')
def test_write_object_without_rights(self):
# attempt to write object using non-authorized user
# update X-Container-Write metadata ACL
cont_headers = {'X-Container-Write': 'badtenant:baduser'}
- resp_meta, body = self.container_client.update_container_metadata(
- self.container_name, metadata=cont_headers,
- metadata_prefix='')
+ resp_meta, _ = (
+ self.container_client.create_update_or_delete_container_metadata(
+ self.container_name, create_update_metadata=cont_headers,
+ create_update_metadata_prefix=''))
self.assertHeaders(resp_meta, 'Container', 'POST')
# Trying to write the object without rights
self.object_client.auth_provider.set_alt_auth_data(
@@ -174,8 +174,8 @@
self.container_name,
object_name, 'data', headers={})
- @test.attr(type=['negative'])
- @test.idempotent_id('8ba512ad-aa6e-444e-b882-2906a0ea2052')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('8ba512ad-aa6e-444e-b882-2906a0ea2052')
def test_write_object_without_write_rights(self):
# attempt to write object using non-authorized user
# update X-Container-Read and X-Container-Write metadata ACL
@@ -184,9 +184,10 @@
cont_headers = {'X-Container-Read':
tenant_name + ':' + username,
'X-Container-Write': ''}
- resp_meta, body = self.container_client.update_container_metadata(
- self.container_name, metadata=cont_headers,
- metadata_prefix='')
+ resp_meta, _ = (
+ self.container_client.create_update_or_delete_container_metadata(
+ self.container_name, create_update_metadata=cont_headers,
+ create_update_metadata_prefix=''))
self.assertHeaders(resp_meta, 'Container', 'POST')
# Trying to write the object without write rights
self.object_client.auth_provider.set_alt_auth_data(
@@ -199,8 +200,8 @@
self.container_name,
object_name, 'data', headers={})
- @test.attr(type=['negative'])
- @test.idempotent_id('b4e366f8-f185-47ab-b789-df4416f9ecdb')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('b4e366f8-f185-47ab-b789-df4416f9ecdb')
def test_delete_object_without_write_rights(self):
# attempt to delete object using non-authorized user
# update X-Container-Read and X-Container-Write metadata ACL
@@ -209,9 +210,10 @@
cont_headers = {'X-Container-Read':
tenant_name + ':' + username,
'X-Container-Write': ''}
- resp_meta, body = self.container_client.update_container_metadata(
- self.container_name, metadata=cont_headers,
- metadata_prefix='')
+ resp_meta, _ = (
+ self.container_client.create_update_or_delete_container_metadata(
+ self.container_name, create_update_metadata=cont_headers,
+ create_update_metadata_prefix=''))
self.assertHeaders(resp_meta, 'Container', 'POST')
# create object
object_name = data_utils.rand_name(name='Object')
diff --git a/tempest/api/object_storage/test_container_quotas.py b/tempest/api/object_storage/test_container_quotas.py
index 896352b..982c4a1 100644
--- a/tempest/api/object_storage/test_container_quotas.py
+++ b/tempest/api/object_storage/test_container_quotas.py
@@ -13,14 +13,12 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.object_storage import base
-from tempest.common.utils import data_utils
-from tempest import config
-from tempest import test
+from tempest.common import utils
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
-CONF = config.CONF
QUOTA_BYTES = 10
QUOTA_COUNT = 3
@@ -39,21 +37,20 @@
Maximum object count of the container.
"""
super(ContainerQuotasTest, self).setUp()
- self.container_name = data_utils.rand_name(name="TestContainer")
- self.container_client.create_container(self.container_name)
+ self.container_name = self.create_container()
metadata = {"quota-bytes": str(QUOTA_BYTES),
"quota-count": str(QUOTA_COUNT), }
- self.container_client.update_container_metadata(
- self.container_name, metadata)
+ self.container_client.create_update_or_delete_container_metadata(
+ self.container_name, create_update_metadata=metadata)
def tearDown(self):
"""Cleans the container of any object after each test."""
- self.delete_containers([self.container_name])
+ self.delete_containers()
super(ContainerQuotasTest, self).tearDown()
- @test.idempotent_id('9a0fb034-86af-4df0-86fa-f8bd7db21ae0')
- @test.requires_ext(extension='container_quotas', service='object')
- @test.attr(type="smoke")
+ @decorators.idempotent_id('9a0fb034-86af-4df0-86fa-f8bd7db21ae0')
+ @utils.requires_ext(extension='container_quotas', service='object')
+ @decorators.attr(type="smoke")
def test_upload_valid_object(self):
"""Attempts to uploads an object smaller than the bytes quota."""
object_name = data_utils.rand_name(name="TestObject")
@@ -68,11 +65,11 @@
nafter = self._get_bytes_used()
self.assertEqual(nbefore + len(data), nafter)
- @test.idempotent_id('22eeeb2b-3668-4160-baef-44790f65a5a0')
- @test.requires_ext(extension='container_quotas', service='object')
- @test.attr(type="smoke")
+ @decorators.idempotent_id('22eeeb2b-3668-4160-baef-44790f65a5a0')
+ @utils.requires_ext(extension='container_quotas', service='object')
+ @decorators.attr(type="smoke")
def test_upload_large_object(self):
- """Attempts to upload an object lagger than the bytes quota."""
+ """Attempts to upload an object larger than the bytes quota."""
object_name = data_utils.rand_name(name="TestObject")
data = data_utils.arbitrary_string(QUOTA_BYTES + 1)
@@ -85,9 +82,9 @@
nafter = self._get_bytes_used()
self.assertEqual(nbefore, nafter)
- @test.idempotent_id('3a387039-697a-44fc-a9c0-935de31f426b')
- @test.requires_ext(extension='container_quotas', service='object')
- @test.attr(type="smoke")
+ @decorators.idempotent_id('3a387039-697a-44fc-a9c0-935de31f426b')
+ @utils.requires_ext(extension='container_quotas', service='object')
+ @decorators.attr(type="smoke")
def test_upload_too_many_objects(self):
"""Attempts to upload many objects that exceeds the count limit."""
for _ in range(QUOTA_COUNT):
diff --git a/tempest/api/object_storage/test_container_services.py b/tempest/api/object_storage/test_container_services.py
index 1cc9437..cdc420e 100644
--- a/tempest/api/object_storage/test_container_services.py
+++ b/tempest/api/object_storage/test_container_services.py
@@ -13,66 +13,42 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib.common.utils import data_utils
-
from tempest.api.object_storage import base
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
class ContainerTest(base.BaseObjectTest):
- def setUp(self):
- super(ContainerTest, self).setUp()
- self.containers = []
-
def tearDown(self):
- self.delete_containers(self.containers)
+ self.delete_containers()
super(ContainerTest, self).tearDown()
- def _create_container(self):
- # setup container
- container_name = data_utils.rand_name(name='TestContainer')
- self.container_client.create_container(container_name)
- self.containers.append(container_name)
-
- return container_name
-
- def _create_object(self, container_name, object_name=None):
- # setup object
- if object_name is None:
- object_name = data_utils.rand_name(name='TestObject')
- data = data_utils.arbitrary_string()
- self.object_client.create_object(container_name,
- object_name,
- data)
-
- return object_name
-
- @test.attr(type='smoke')
- @test.idempotent_id('92139d73-7819-4db1-85f8-3f2f22a8d91f')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('92139d73-7819-4db1-85f8-3f2f22a8d91f')
def test_create_container(self):
container_name = data_utils.rand_name(name='TestContainer')
- resp, body = self.container_client.create_container(container_name)
+ resp, _ = self.container_client.update_container(container_name)
self.containers.append(container_name)
self.assertHeaders(resp, 'Container', 'PUT')
- @test.idempotent_id('49f866ed-d6af-4395-93e7-4187eb56d322')
+ @decorators.idempotent_id('49f866ed-d6af-4395-93e7-4187eb56d322')
def test_create_container_overwrite(self):
# overwrite container with the same name
container_name = data_utils.rand_name(name='TestContainer')
- self.container_client.create_container(container_name)
+ self.container_client.update_container(container_name)
self.containers.append(container_name)
- resp, _ = self.container_client.create_container(container_name)
+ resp, _ = self.container_client.update_container(container_name)
self.assertHeaders(resp, 'Container', 'PUT')
- @test.idempotent_id('c2ac4d59-d0f5-40d5-ba19-0635056d48cd')
+ @decorators.idempotent_id('c2ac4d59-d0f5-40d5-ba19-0635056d48cd')
def test_create_container_with_metadata_key(self):
# create container with the blank value of metadata
container_name = data_utils.rand_name(name='TestContainer')
- metadata = {'test-container-meta': ''}
- resp, _ = self.container_client.create_container(
+ headers = {'X-Container-Meta-test-container-meta': ''}
+ resp, _ = self.container_client.update_container(
container_name,
- metadata=metadata)
+ **headers)
self.containers.append(container_name)
self.assertHeaders(resp, 'Container', 'PUT')
@@ -82,15 +58,16 @@
# in the server
self.assertNotIn('x-container-meta-test-container-meta', resp)
- @test.idempotent_id('e1e8df32-7b22-44e1-aa08-ccfd8d446b58')
+ @decorators.idempotent_id('e1e8df32-7b22-44e1-aa08-ccfd8d446b58')
def test_create_container_with_metadata_value(self):
# create container with metadata value
container_name = data_utils.rand_name(name='TestContainer')
- metadata = {'test-container-meta': 'Meta1'}
- resp, _ = self.container_client.create_container(
+ # metadata name using underscores should be converted to hyphens
+ headers = {'X-Container-Meta-test_container_meta': 'Meta1'}
+ resp, _ = self.container_client.update_container(
container_name,
- metadata=metadata)
+ **headers)
self.containers.append(container_name)
self.assertHeaders(resp, 'Container', 'PUT')
@@ -98,112 +75,108 @@
container_name)
self.assertIn('x-container-meta-test-container-meta', resp)
self.assertEqual(resp['x-container-meta-test-container-meta'],
- metadata['test-container-meta'])
+ headers['X-Container-Meta-test_container_meta'])
- @test.idempotent_id('24d16451-1c0c-4e4f-b59c-9840a3aba40e')
+ @decorators.idempotent_id('24d16451-1c0c-4e4f-b59c-9840a3aba40e')
def test_create_container_with_remove_metadata_key(self):
# create container with the blank value of remove metadata
container_name = data_utils.rand_name(name='TestContainer')
- metadata_1 = {'test-container-meta': 'Meta1'}
- self.container_client.create_container(
- container_name,
- metadata=metadata_1)
+ headers = {'X-Container-Meta-test-container-meta': 'Meta1'}
+ self.container_client.update_container(container_name, **headers)
self.containers.append(container_name)
- metadata_2 = {'test-container-meta': ''}
- resp, _ = self.container_client.create_container(
+ headers = {'X-Remove-Container-Meta-test-container-meta': ''}
+ resp, _ = self.container_client.update_container(
container_name,
- remove_metadata=metadata_2)
+ **headers)
self.assertHeaders(resp, 'Container', 'PUT')
resp, _ = self.container_client.list_container_metadata(
container_name)
self.assertNotIn('x-container-meta-test-container-meta', resp)
- @test.idempotent_id('8a21ebad-a5c7-4e29-b428-384edc8cd156')
+ @decorators.idempotent_id('8a21ebad-a5c7-4e29-b428-384edc8cd156')
def test_create_container_with_remove_metadata_value(self):
# create container with remove metadata
container_name = data_utils.rand_name(name='TestContainer')
- metadata = {'test-container-meta': 'Meta1'}
- self.container_client.create_container(container_name,
- metadata=metadata)
+ headers = {'X-Container-Meta-test-container-meta': 'Meta1'}
+ self.container_client.update_container(container_name, **headers)
self.containers.append(container_name)
-
- resp, _ = self.container_client.create_container(
+ headers = {'X-Remove-Container-Meta-test-container-meta': 'Meta1'}
+ resp, _ = self.container_client.update_container(
container_name,
- remove_metadata=metadata)
+ **headers)
self.assertHeaders(resp, 'Container', 'PUT')
resp, _ = self.container_client.list_container_metadata(
container_name)
self.assertNotIn('x-container-meta-test-container-meta', resp)
- @test.idempotent_id('95d3a249-b702-4082-a2c4-14bb860cf06a')
+ @decorators.idempotent_id('95d3a249-b702-4082-a2c4-14bb860cf06a')
def test_delete_container(self):
# create a container
- container_name = self._create_container()
+ container_name = self.create_container()
# delete container, success asserted within
resp, _ = self.container_client.delete_container(container_name)
self.assertHeaders(resp, 'Container', 'DELETE')
- self.containers.remove(container_name)
- @test.attr(type='smoke')
- @test.idempotent_id('312ff6bd-5290-497f-bda1-7c5fec6697ab')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('312ff6bd-5290-497f-bda1-7c5fec6697ab')
def test_list_container_contents(self):
# get container contents list
- container_name = self._create_container()
- object_name = self._create_object(container_name)
+ container_name = self.create_container()
+ object_name, _ = self.create_object(container_name)
- resp, object_list = self.container_client.list_container_contents(
+ resp, object_list = self.container_client.list_container_objects(
container_name)
self.assertHeaders(resp, 'Container', 'GET')
- self.assertEqual(object_name, object_list.strip('\n'))
+ self.assertEqual([object_name], object_list)
- @test.idempotent_id('4646ac2d-9bfb-4c7d-a3c5-0f527402b3df')
+ @decorators.idempotent_id('4646ac2d-9bfb-4c7d-a3c5-0f527402b3df')
def test_list_container_contents_with_no_object(self):
# get empty container contents list
- container_name = self._create_container()
+ container_name = self.create_container()
- resp, object_list = self.container_client.list_container_contents(
+ resp, object_list = self.container_client.list_container_objects(
container_name)
self.assertHeaders(resp, 'Container', 'GET')
- self.assertEqual('', object_list.strip('\n'))
+ self.assertEmpty(object_list)
- @test.idempotent_id('fe323a32-57b9-4704-a996-2e68f83b09bc')
+ @decorators.idempotent_id('fe323a32-57b9-4704-a996-2e68f83b09bc')
def test_list_container_contents_with_delimiter(self):
# get container contents list using delimiter param
- container_name = self._create_container()
+ container_name = self.create_container()
object_name = data_utils.rand_name(name='TestObject/')
- self._create_object(container_name, object_name)
+ self.create_object(container_name, object_name)
params = {'delimiter': '/'}
- resp, object_list = self.container_client.list_container_contents(
+ resp, object_list = self.container_client.list_container_objects(
container_name,
params=params)
self.assertHeaders(resp, 'Container', 'GET')
- self.assertEqual(object_name.split('/')[0], object_list.strip('/\n'))
+ self.assertEqual([object_name.split('/')[0] + '/'], object_list)
- @test.idempotent_id('55b4fa5c-e12e-4ca9-8fcf-a79afe118522')
+ @decorators.idempotent_id('55b4fa5c-e12e-4ca9-8fcf-a79afe118522')
def test_list_container_contents_with_end_marker(self):
# get container contents list using end_marker param
- container_name = self._create_container()
- object_name = self._create_object(container_name)
+ container_name = self.create_container()
+ object_name, _ = self.create_object(container_name)
- params = {'end_marker': 'ZzzzObject1234567890'}
- resp, object_list = self.container_client.list_container_contents(
+ params = {'end_marker': object_name + 'zzzz'}
+ resp, object_list = self.container_client.list_container_objects(
container_name,
params=params)
self.assertHeaders(resp, 'Container', 'GET')
- self.assertEqual(object_name, object_list.strip('\n'))
+ self.assertEqual([object_name], object_list)
- @test.idempotent_id('196f5034-6ab0-4032-9da9-a937bbb9fba9')
+ @decorators.idempotent_id('196f5034-6ab0-4032-9da9-a937bbb9fba9')
def test_list_container_contents_with_format_json(self):
# get container contents list using format_json param
- container_name = self._create_container()
- self._create_object(container_name)
+ container_name = self.create_container()
+ self.create_object(container_name)
params = {'format': 'json'}
- resp, object_list = self.container_client.list_container_contents(
+ resp, object_list = self.container_client.list_container_objects(
container_name,
params=params)
self.assertHeaders(resp, 'Container', 'GET')
@@ -215,21 +188,21 @@
self.assertTrue([c['content_type'] for c in object_list])
self.assertTrue([c['last_modified'] for c in object_list])
- @test.idempotent_id('655a53ca-4d15-408c-a377-f4c6dbd0a1fa')
+ @decorators.idempotent_id('655a53ca-4d15-408c-a377-f4c6dbd0a1fa')
def test_list_container_contents_with_format_xml(self):
# get container contents list using format_xml param
- container_name = self._create_container()
- self._create_object(container_name)
+ container_name = self.create_container()
+ self.create_object(container_name)
params = {'format': 'xml'}
- resp, object_list = self.container_client.list_container_contents(
+ resp, object_list = self.container_client.list_container_objects(
container_name,
params=params)
self.assertHeaders(resp, 'Container', 'GET')
self.assertIsNotNone(object_list)
self.assertEqual(object_list.tag, 'container')
- self.assertTrue('name' in object_list.keys())
+ self.assertIn('name', object_list.keys())
self.assertEqual(object_list.find(".//object").tag, 'object')
self.assertEqual(object_list.find(".//name").tag, 'name')
self.assertEqual(object_list.find(".//hash").tag, 'hash')
@@ -239,70 +212,71 @@
self.assertEqual(object_list.find(".//last_modified").tag,
'last_modified')
- @test.idempotent_id('297ec38b-2b61-4ff4-bcd1-7fa055e97b61')
+ @decorators.idempotent_id('297ec38b-2b61-4ff4-bcd1-7fa055e97b61')
def test_list_container_contents_with_limit(self):
# get container contents list using limit param
- container_name = self._create_container()
- object_name = self._create_object(container_name)
+ container_name = self.create_container()
+ object_name, _ = self.create_object(container_name)
params = {'limit': data_utils.rand_int_id(1, 10000)}
- resp, object_list = self.container_client.list_container_contents(
+ resp, object_list = self.container_client.list_container_objects(
container_name,
params=params)
self.assertHeaders(resp, 'Container', 'GET')
- self.assertEqual(object_name, object_list.strip('\n'))
+ self.assertEqual([object_name], object_list)
- @test.idempotent_id('c31ddc63-2a58-4f6b-b25c-94d2937e6867')
+ @decorators.idempotent_id('c31ddc63-2a58-4f6b-b25c-94d2937e6867')
def test_list_container_contents_with_marker(self):
# get container contents list using marker param
- container_name = self._create_container()
- object_name = self._create_object(container_name)
+ container_name = self.create_container()
+ object_name, _ = self.create_object(container_name)
params = {'marker': 'AaaaObject1234567890'}
- resp, object_list = self.container_client.list_container_contents(
+ resp, object_list = self.container_client.list_container_objects(
container_name,
params=params)
self.assertHeaders(resp, 'Container', 'GET')
- self.assertEqual(object_name, object_list.strip('\n'))
+ self.assertEqual([object_name], object_list)
- @test.idempotent_id('58ca6cc9-6af0-408d-aaec-2a6a7b2f0df9')
+ @decorators.idempotent_id('58ca6cc9-6af0-408d-aaec-2a6a7b2f0df9')
def test_list_container_contents_with_path(self):
# get container contents list using path param
- container_name = self._create_container()
- object_name = data_utils.rand_name(name='Swift/TestObject')
- self._create_object(container_name, object_name)
+ container_name = self.create_container()
+ object_name = data_utils.rand_name(name='TestObject')
+ object_name = 'Swift/' + object_name
+ self.create_object(container_name, object_name)
params = {'path': 'Swift'}
- resp, object_list = self.container_client.list_container_contents(
+ resp, object_list = self.container_client.list_container_objects(
container_name,
params=params)
self.assertHeaders(resp, 'Container', 'GET')
- self.assertEqual(object_name, object_list.strip('\n'))
+ self.assertEqual([object_name], object_list)
- @test.idempotent_id('77e742c7-caf2-4ec9-8aa4-f7d509a3344c')
+ @decorators.idempotent_id('77e742c7-caf2-4ec9-8aa4-f7d509a3344c')
def test_list_container_contents_with_prefix(self):
# get container contents list using prefix param
- container_name = self._create_container()
- object_name = self._create_object(container_name)
+ container_name = self.create_container()
+ object_name, _ = self.create_object(container_name)
prefix_key = object_name[0:8]
params = {'prefix': prefix_key}
- resp, object_list = self.container_client.list_container_contents(
+ resp, object_list = self.container_client.list_container_objects(
container_name,
params=params)
self.assertHeaders(resp, 'Container', 'GET')
- self.assertEqual(object_name, object_list.strip('\n'))
+ self.assertEqual([object_name], object_list)
- @test.attr(type='smoke')
- @test.idempotent_id('96e68f0e-19ec-4aa2-86f3-adc6a45e14dd')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('96e68f0e-19ec-4aa2-86f3-adc6a45e14dd')
def test_list_container_metadata(self):
# List container metadata
- container_name = self._create_container()
+ container_name = self.create_container()
metadata = {'name': 'Pictures'}
- self.container_client.update_container_metadata(
+ self.container_client.create_update_or_delete_container_metadata(
container_name,
- metadata=metadata)
+ create_update_metadata=metadata)
resp, _ = self.container_client.list_container_metadata(
container_name)
@@ -310,30 +284,30 @@
self.assertIn('x-container-meta-name', resp)
self.assertEqual(resp['x-container-meta-name'], metadata['name'])
- @test.idempotent_id('a2faf936-6b13-4f8d-92a2-c2278355821e')
+ @decorators.idempotent_id('a2faf936-6b13-4f8d-92a2-c2278355821e')
def test_list_no_container_metadata(self):
# HEAD container without metadata
- container_name = self._create_container()
+ container_name = self.create_container()
resp, _ = self.container_client.list_container_metadata(
container_name)
self.assertHeaders(resp, 'Container', 'HEAD')
self.assertNotIn('x-container-meta-', str(resp))
- @test.idempotent_id('cf19bc0b-7e16-4a5a-aaed-cb0c2fe8deef')
+ @decorators.idempotent_id('cf19bc0b-7e16-4a5a-aaed-cb0c2fe8deef')
def test_update_container_metadata_with_create_and_delete_metadata(self):
# Send one request of adding and deleting metadata
container_name = data_utils.rand_name(name='TestContainer')
- metadata_1 = {'test-container-meta1': 'Meta1'}
- self.container_client.create_container(container_name,
- metadata=metadata_1)
+ metadata_1 = {'X-Container-Meta-test-container-meta1': 'Meta1'}
+ self.container_client.update_container(container_name, **metadata_1)
self.containers.append(container_name)
metadata_2 = {'test-container-meta2': 'Meta2'}
- resp, _ = self.container_client.update_container_metadata(
- container_name,
- metadata=metadata_2,
- remove_metadata=metadata_1)
+ resp, _ = (
+ self.container_client.create_update_or_delete_container_metadata(
+ container_name,
+ create_update_metadata=metadata_2,
+ delete_metadata={'test-container-meta1': 'Meta1'}))
self.assertHeaders(resp, 'Container', 'POST')
resp, _ = self.container_client.list_container_metadata(
@@ -343,15 +317,16 @@
self.assertEqual(resp['x-container-meta-test-container-meta2'],
metadata_2['test-container-meta2'])
- @test.idempotent_id('2ae5f295-4bf1-4e04-bfad-21e54b62cec5')
+ @decorators.idempotent_id('2ae5f295-4bf1-4e04-bfad-21e54b62cec5')
def test_update_container_metadata_with_create_metadata(self):
# update container metadata using add metadata
- container_name = self._create_container()
+ container_name = self.create_container()
metadata = {'test-container-meta1': 'Meta1'}
- resp, _ = self.container_client.update_container_metadata(
- container_name,
- metadata=metadata)
+ resp, _ = (
+ self.container_client.create_update_or_delete_container_metadata(
+ container_name,
+ create_update_metadata=metadata))
self.assertHeaders(resp, 'Container', 'POST')
resp, _ = self.container_client.list_container_metadata(
@@ -360,52 +335,53 @@
self.assertEqual(resp['x-container-meta-test-container-meta1'],
metadata['test-container-meta1'])
- @test.idempotent_id('3a5ce7d4-6e4b-47d0-9d87-7cd42c325094')
+ @decorators.idempotent_id('3a5ce7d4-6e4b-47d0-9d87-7cd42c325094')
def test_update_container_metadata_with_delete_metadata(self):
# update container metadata using delete metadata
container_name = data_utils.rand_name(name='TestContainer')
- metadata = {'test-container-meta1': 'Meta1'}
- self.container_client.create_container(container_name,
- metadata=metadata)
+ metadata = {'X-Container-Meta-test-container-meta1': 'Meta1'}
+ self.container_client.update_container(container_name, **metadata)
self.containers.append(container_name)
- resp, _ = self.container_client.delete_container_metadata(
- container_name,
- metadata=metadata)
+ resp, _ = (
+ self.container_client.create_update_or_delete_container_metadata(
+ container_name,
+ delete_metadata={'test-container-meta1': 'Meta1'}))
self.assertHeaders(resp, 'Container', 'POST')
resp, _ = self.container_client.list_container_metadata(
container_name)
self.assertNotIn('x-container-meta-test-container-meta1', resp)
- @test.idempotent_id('31f40a5f-6a52-4314-8794-cd89baed3040')
+ @decorators.idempotent_id('31f40a5f-6a52-4314-8794-cd89baed3040')
def test_update_container_metadata_with_create_metadata_key(self):
# update container metadata with a blank value of metadata
- container_name = self._create_container()
+ container_name = self.create_container()
metadata = {'test-container-meta1': ''}
- resp, _ = self.container_client.update_container_metadata(
- container_name,
- metadata=metadata)
+ resp, _ = (
+ self.container_client.create_update_or_delete_container_metadata(
+ container_name,
+ create_update_metadata=metadata))
self.assertHeaders(resp, 'Container', 'POST')
resp, _ = self.container_client.list_container_metadata(
container_name)
self.assertNotIn('x-container-meta-test-container-meta1', resp)
- @test.idempotent_id('a2e36378-6f1f-43f4-840a-ffd9cfd61914')
+ @decorators.idempotent_id('a2e36378-6f1f-43f4-840a-ffd9cfd61914')
def test_update_container_metadata_with_delete_metadata_key(self):
# update container metadata with a blank value of metadata
container_name = data_utils.rand_name(name='TestContainer')
- metadata = {'test-container-meta1': 'Meta1'}
- self.container_client.create_container(container_name,
- metadata=metadata)
+ headers = {'X-Container-Meta-test-container-meta1': 'Meta1'}
+ self.container_client.update_container(container_name, **headers)
self.containers.append(container_name)
metadata = {'test-container-meta1': ''}
- resp, _ = self.container_client.delete_container_metadata(
- container_name,
- metadata=metadata)
+ resp, _ = (
+ self.container_client.create_update_or_delete_container_metadata(
+ container_name,
+ delete_metadata=metadata))
self.assertHeaders(resp, 'Container', 'POST')
resp, _ = self.container_client.list_container_metadata(container_name)
diff --git a/tempest/api/object_storage/test_container_services_negative.py b/tempest/api/object_storage/test_container_services_negative.py
new file mode 100644
index 0000000..b8c83b7
--- /dev/null
+++ b/tempest/api/object_storage/test_container_services_negative.py
@@ -0,0 +1,184 @@
+# Copyright 2016 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import testtools
+
+from tempest.api.object_storage import base
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions
+
+CONF = config.CONF
+
+
+class ContainerNegativeTest(base.BaseObjectTest):
+
+ @classmethod
+ def resource_setup(cls):
+ super(ContainerNegativeTest, cls).resource_setup()
+
+ if CONF.object_storage_feature_enabled.discoverability:
+ # use /info to get default constraints
+ body = cls.capabilities_client.list_capabilities()
+ cls.constraints = body['swift']
+
+ @decorators.attr(type=["negative"])
+ @decorators.idempotent_id('30686921-4bed-4764-a038-40d741ed4e78')
+ @testtools.skipUnless(
+ CONF.object_storage_feature_enabled.discoverability,
+ 'Discoverability function is disabled')
+ def test_create_container_name_exceeds_max_length(self):
+ # Attempts to create a container name that is longer than max
+ max_length = self.constraints['max_container_name_length']
+ # create a container with long name
+ container_name = data_utils.arbitrary_string(size=max_length + 1)
+ ex = self.assertRaises(
+ exceptions.BadRequest,
+ self.container_client.update_container,
+ container_name)
+ self.assertIn('Container name length of ' + str(max_length + 1) +
+ ' longer than ' + str(max_length), str(ex))
+
+ @decorators.attr(type=["negative"])
+ @decorators.idempotent_id('41e645bf-2e68-4f84-bf7b-c71aa5cd76ce')
+ @testtools.skipUnless(
+ CONF.object_storage_feature_enabled.discoverability,
+ 'Discoverability function is disabled')
+ def test_create_container_metadata_name_exceeds_max_length(self):
+ # Attempts to create container with metadata name
+ # that is longer than max.
+ max_length = self.constraints['max_meta_name_length']
+ container_name = data_utils.rand_name(name='TestContainer')
+ metadata_name = 'X-Container-Meta-' + data_utils.arbitrary_string(
+ size=max_length + 1)
+ metadata = {metadata_name: 'penguin'}
+ ex = self.assertRaises(
+ exceptions.BadRequest,
+ self.container_client.update_container,
+ container_name, **metadata)
+ self.assertIn('Metadata name too long', str(ex))
+
+ @decorators.attr(type=["negative"])
+ @decorators.idempotent_id('81e36922-326b-4b7c-8155-3bbceecd7a82')
+ @testtools.skipUnless(
+ CONF.object_storage_feature_enabled.discoverability,
+ 'Discoverability function is disabled')
+ def test_create_container_metadata_value_exceeds_max_length(self):
+ # Attempts to create container with metadata value
+ # that is longer than max.
+ max_length = self.constraints['max_meta_value_length']
+ container_name = data_utils.rand_name(name='TestContainer')
+ metadata_value = data_utils.arbitrary_string(size=max_length + 1)
+ metadata = {'X-Container-Meta-animal': metadata_value}
+ ex = self.assertRaises(
+ exceptions.BadRequest,
+ self.container_client.update_container,
+ container_name, **metadata)
+ self.assertIn('Metadata value longer than ' + str(max_length), str(ex))
+
+ @decorators.attr(type=["negative"])
+ @decorators.idempotent_id('ac666539-d566-4f02-8ceb-58e968dfb732')
+ @testtools.skipUnless(
+ CONF.object_storage_feature_enabled.discoverability,
+ 'Discoverability function is disabled')
+ def test_create_container_metadata_exceeds_overall_metadata_count(self):
+ # Attempts to create container with metadata that exceeds the
+ # default count
+ max_count = self.constraints['max_meta_count']
+ container_name = data_utils.rand_name(name='TestContainer')
+ metadata = {}
+ for i in range(max_count + 1):
+ metadata['X-Container-Meta-animal-' + str(i)] = 'penguin'
+
+ ex = self.assertRaises(
+ exceptions.BadRequest,
+ self.container_client.update_container,
+ container_name, **metadata)
+ self.assertIn('Too many metadata items; max ' + str(max_count),
+ str(ex))
+
+ @decorators.attr(type=["negative"])
+ @decorators.idempotent_id('1a95ab2e-b712-4a98-8a4d-8ce21b7557d6')
+ def test_get_metadata_headers_with_invalid_container_name(self):
+ # Attempts to retrieve metadata headers with an invalid
+ # container name.
+ self.assertRaises(exceptions.NotFound,
+ self.container_client.list_container_metadata,
+ 'invalid_container_name')
+
+ @decorators.attr(type=["negative"])
+ @decorators.idempotent_id('125a24fa-90a7-4cfc-b604-44e49d788390')
+ def test_update_metadata_with_nonexistent_container_name(self):
+ # Attempts to update metadata using a nonexistent container name.
+ metadata = {'animal': 'penguin'}
+
+ self.assertRaises(
+ exceptions.NotFound,
+ self.container_client.create_update_or_delete_container_metadata,
+ 'nonexistent_container_name', create_update_metadata=metadata)
+
+ @decorators.attr(type=["negative"])
+ @decorators.idempotent_id('65387dbf-a0e2-4aac-9ddc-16eb3f1f69ba')
+ def test_delete_with_nonexistent_container_name(self):
+ # Attempts to delete metadata using a nonexistent container name.
+ metadata = {'animal': 'penguin'}
+
+ self.assertRaises(
+ exceptions.NotFound,
+ self.container_client.create_update_or_delete_container_metadata,
+ 'nonexistent_container_name', delete_metadata=metadata)
+
+ @decorators.attr(type=["negative"])
+ @decorators.idempotent_id('14331d21-1e81-420a-beea-19cb5e5207f5')
+ def test_list_all_container_objects_with_nonexistent_container(self):
+ # Attempts to get a listing of all objects on a container
+ # that doesn't exist.
+ params = {'limit': 9999, 'format': 'json'}
+ self.assertRaises(exceptions.NotFound,
+ self.container_client.list_container_objects,
+ 'nonexistent_container_name', params)
+
+ @decorators.attr(type=["negative"])
+ @decorators.idempotent_id('86b2ab08-92d5-493d-acd2-85f0c848819e')
+ def test_list_all_container_objects_on_deleted_container(self):
+ # Attempts to get a listing of all objects on a container
+ # that was deleted.
+ container_name = self.create_container()
+ # delete container
+ resp, _ = self.container_client.delete_container(container_name)
+ self.assertHeaders(resp, 'Container', 'DELETE')
+ params = {'limit': 9999, 'format': 'json'}
+ self.assertRaises(exceptions.NotFound,
+ self.container_client.list_container_objects,
+ container_name, params)
+
+ @decorators.attr(type=["negative"])
+ @decorators.idempotent_id('42da116e-1e8c-4c96-9e06-2f13884ed2b1')
+ def test_delete_non_empty_container(self):
+ # create a container and an object within it
+ # attempt to delete a container that isn't empty.
+ container_name = self.create_container()
+ self.addCleanup(self.container_client.delete_container,
+ container_name)
+ object_name, _ = self.create_object(container_name)
+ self.addCleanup(self.object_client.delete_object,
+ container_name, object_name)
+
+ ex = self.assertRaises(exceptions.Conflict,
+ self.container_client.delete_container,
+ container_name)
+ self.assertIn('There was a conflict when trying to complete your '
+ 'request.', str(ex))
diff --git a/tempest/api/object_storage/test_container_staticweb.py b/tempest/api/object_storage/test_container_staticweb.py
index 18593f3..1243b83 100644
--- a/tempest/api/object_storage/test_container_staticweb.py
+++ b/tempest/api/object_storage/test_container_staticweb.py
@@ -12,12 +12,12 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.object_storage import base
from tempest.common import custom_matchers
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.common import utils
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class StaticWebTest(base.BaseObjectTest):
@@ -25,37 +25,32 @@
@classmethod
def resource_setup(cls):
super(StaticWebTest, cls).resource_setup()
- cls.container_name = data_utils.rand_name(name="TestContainer")
# This header should be posted on the container before every test
- cls.headers_public_read_acl = {'Read': '.r:*,.rlistings'}
+ headers_public_read_acl = {'Read': '.r:*,.rlistings'}
# Create test container and create one object in it
- cls.container_client.create_container(cls.container_name)
- cls.object_name = data_utils.rand_name(name="TestObject")
- cls.object_data = data_utils.arbitrary_string()
- cls.object_client.create_object(cls.container_name,
- cls.object_name,
- cls.object_data)
+ cls.container_name = cls.create_container()
+ cls.object_name, cls.object_data = cls.create_object(
+ cls.container_name)
- cls.container_client.update_container_metadata(
+ cls.container_client.create_update_or_delete_container_metadata(
cls.container_name,
- metadata=cls.headers_public_read_acl,
- metadata_prefix="X-Container-")
+ create_update_metadata=headers_public_read_acl,
+ create_update_metadata_prefix="X-Container-")
@classmethod
def resource_cleanup(cls):
- if hasattr(cls, "container_name"):
- cls.delete_containers([cls.container_name])
+ cls.delete_containers()
super(StaticWebTest, cls).resource_cleanup()
- @test.idempotent_id('c1f055ab-621d-4a6a-831f-846fcb578b8b')
- @test.requires_ext(extension='staticweb', service='object')
+ @decorators.idempotent_id('c1f055ab-621d-4a6a-831f-846fcb578b8b')
+ @utils.requires_ext(extension='staticweb', service='object')
def test_web_index(self):
headers = {'web-index': self.object_name}
- self.container_client.update_container_metadata(
- self.container_name, metadata=headers)
+ self.container_client.create_update_or_delete_container_metadata(
+ self.container_name, create_update_metadata=headers)
# Maintain original headers, no auth added
self.account_client.auth_provider.set_alt_auth_data(
@@ -73,20 +68,21 @@
self.assertEqual(body, self.object_data)
# clean up before exiting
- self.container_client.update_container_metadata(self.container_name,
- {'web-index': ""})
+ self.container_client.create_update_or_delete_container_metadata(
+ self.container_name,
+ create_update_metadata={'web-index': ""})
_, body = self.container_client.list_container_metadata(
self.container_name)
self.assertNotIn('x-container-meta-web-index', body)
- @test.idempotent_id('941814cf-db9e-4b21-8112-2b6d0af10ee5')
- @test.requires_ext(extension='staticweb', service='object')
+ @decorators.idempotent_id('941814cf-db9e-4b21-8112-2b6d0af10ee5')
+ @utils.requires_ext(extension='staticweb', service='object')
def test_web_listing(self):
headers = {'web-listings': 'true'}
- self.container_client.update_container_metadata(
- self.container_name, metadata=headers)
+ self.container_client.create_update_or_delete_container_metadata(
+ self.container_name, create_update_metadata=headers)
# test GET on http://account_url/container_name
# we should retrieve a listing of objects
@@ -102,24 +98,24 @@
# Check only the format of common headers with custom matcher
self.assertThat(resp, custom_matchers.AreAllWellFormatted())
- self.assertIn(self.object_name, body)
+ self.assertIn(self.object_name, body.decode())
# clean up before exiting
- self.container_client.update_container_metadata(self.container_name,
- {'web-listings': ""})
-
+ self.container_client.create_update_or_delete_container_metadata(
+ self.container_name,
+ create_update_metadata={'web-listings': ""})
_, body = self.container_client.list_container_metadata(
self.container_name)
self.assertNotIn('x-container-meta-web-listings', body)
- @test.idempotent_id('bc37ec94-43c8-4990-842e-0e5e02fc8926')
- @test.requires_ext(extension='staticweb', service='object')
+ @decorators.idempotent_id('bc37ec94-43c8-4990-842e-0e5e02fc8926')
+ @utils.requires_ext(extension='staticweb', service='object')
def test_web_listing_css(self):
headers = {'web-listings': 'true',
'web-listings-css': 'listings.css'}
- self.container_client.update_container_metadata(
- self.container_name, metadata=headers)
+ self.container_client.create_update_or_delete_container_metadata(
+ self.container_name, create_update_metadata=headers)
# Maintain original headers, no auth added
self.account_client.auth_provider.set_alt_auth_data(
@@ -129,21 +125,20 @@
# test GET on http://account_url/container_name
# we should retrieve a listing of objects
- resp, body = self.account_client.request("GET",
- self.container_name,
- headers={})
- self.assertIn(self.object_name, body)
+ _, body = self.account_client.request("GET", self.container_name,
+ headers={})
+ self.assertIn(self.object_name, body.decode())
css = '<link rel="stylesheet" type="text/css" href="listings.css" />'
- self.assertIn(css, body)
+ self.assertIn(css, body.decode())
- @test.idempotent_id('f18b4bef-212e-45e7-b3ca-59af3a465f82')
- @test.requires_ext(extension='staticweb', service='object')
+ @decorators.idempotent_id('f18b4bef-212e-45e7-b3ca-59af3a465f82')
+ @utils.requires_ext(extension='staticweb', service='object')
def test_web_error(self):
headers = {'web-listings': 'true',
'web-error': self.object_name}
- self.container_client.update_container_metadata(
- self.container_name, metadata=headers)
+ self.container_client.create_update_or_delete_container_metadata(
+ self.container_name, create_update_metadata=headers)
# Create object to return when requested object not found
object_name_404 = "404" + self.object_name
diff --git a/tempest/api/object_storage/test_container_sync.py b/tempest/api/object_storage/test_container_sync.py
index 0e39b7e..322579c 100644
--- a/tempest/api/object_storage/test_container_sync.py
+++ b/tempest/api/object_storage/test_container_sync.py
@@ -16,14 +16,12 @@
import time
from six.moves.urllib import parse as urlparse
-from tempest_lib import decorators
import testtools
-
from tempest.api.object_storage import base
-from tempest.common.utils import data_utils
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
CONF = config.CONF
@@ -35,15 +33,12 @@
class ContainerSyncTest(base.BaseObjectTest):
- clients = {}
-
credentials = [['operator', CONF.object_storage.operator_role],
['operator_alt', CONF.object_storage.operator_role]]
@classmethod
def setup_credentials(cls):
super(ContainerSyncTest, cls).setup_credentials()
- cls.os = cls.os_roles_operator
cls.os_alt = cls.os_roles_operator_alt
@classmethod
@@ -57,6 +52,7 @@
super(ContainerSyncTest, cls).resource_setup()
cls.containers = []
cls.objects = []
+ cls.clients = {}
# Default container-server config only allows localhost
cls.local_ip = '127.0.0.1'
@@ -75,14 +71,12 @@
(cls.container_client_alt, cls.object_client_alt)
for cont_name, client in cls.clients.items():
client[0].create_container(cont_name)
+ cls.addClassResourceCleanup(base.delete_containers,
+ cont_name,
+ client[0],
+ client[1])
cls.containers.append(cont_name)
- @classmethod
- def resource_cleanup(cls):
- for client in cls.clients.values():
- cls.delete_containers(cls.containers, client[0], client[1])
- super(ContainerSyncTest, cls).resource_cleanup()
-
def _test_container_synchronization(self, make_headers):
# container to container synchronization
# to allow/accept sync requests to/from other accounts
@@ -92,11 +86,10 @@
cont_client = [self.clients[c][0] for c in cont]
obj_client = [self.clients[c][1] for c in cont]
headers = make_headers(cont[1], cont_client[1])
- resp, body = \
- cont_client[0].put(str(cont[0]), body=None, headers=headers)
+ cont_client[0].put(str(cont[0]), body=None, headers=headers)
# create object in container
object_name = data_utils.rand_name(name='TestSyncObject')
- data = object_name[::-1] # data_utils.arbitrary_string()
+ data = object_name[::-1].encode() # Raw data, we need bytes
resp, _ = obj_client[0].create_object(cont[0], object_name, data)
self.objects.append(object_name)
@@ -106,7 +99,7 @@
while self.attempts > 0:
object_lists = []
for c_client, cont in zip(cont_client, self.containers):
- resp, object_list = c_client.list_container_contents(
+ resp, object_list = c_client.list_container_objects(
cont, params=params)
object_lists.append(dict(
(obj['name'], obj) for obj in object_list))
@@ -127,11 +120,11 @@
for obj_client, cont in obj_clients:
for obj_name in object_lists[0]:
resp, object_content = obj_client.get_object(cont, obj_name)
- self.assertEqual(object_content, obj_name[::-1])
+ self.assertEqual(object_content, obj_name[::-1].encode())
- @test.attr(type='slow')
+ @decorators.attr(type='slow')
@decorators.skip_because(bug='1317133')
- @test.idempotent_id('be008325-1bba-4925-b7dd-93b58f22ce9b')
+ @decorators.idempotent_id('be008325-1bba-4925-b7dd-93b58f22ce9b')
@testtools.skipIf(
not CONF.object_storage_feature_enabled.container_sync,
'Old-style container sync function is disabled')
diff --git a/tempest/api/object_storage/test_container_sync_middleware.py b/tempest/api/object_storage/test_container_sync_middleware.py
index 4491a84..e77b079 100644
--- a/tempest/api/object_storage/test_container_sync_middleware.py
+++ b/tempest/api/object_storage/test_container_sync_middleware.py
@@ -13,8 +13,9 @@
# under the License.
from tempest.api.object_storage import test_container_sync
+from tempest.common import utils
from tempest import config
-from tempest import test
+from tempest.lib import decorators
CONF = config.CONF
@@ -36,9 +37,9 @@
cls.key = 'sync_key'
cls.cluster_name = CONF.object_storage.cluster_name
- @test.attr(type='slow')
- @test.idempotent_id('ea4645a1-d147-4976-82f7-e5a7a3065f80')
- @test.requires_ext(extension='container_sync', service='object')
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('ea4645a1-d147-4976-82f7-e5a7a3065f80')
+ @utils.requires_ext(extension='container_sync', service='object')
def test_container_synchronization(self):
def make_headers(cont, cont_client):
# tell first container to synchronize to a second
diff --git a/tempest/api/object_storage/test_crossdomain.py b/tempest/api/object_storage/test_crossdomain.py
index 8dbfd06..f61d9f8 100644
--- a/tempest/api/object_storage/test_crossdomain.py
+++ b/tempest/api/object_storage/test_crossdomain.py
@@ -14,7 +14,8 @@
from tempest.api.object_storage import base
from tempest.common import custom_matchers
-from tempest import test
+from tempest.common import utils
+from tempest.lib import decorators
class CrossdomainTest(base.BaseObjectTest):
@@ -36,10 +37,11 @@
# Turning http://.../v1/foobar into http://.../
self.account_client.skip_path()
- @test.idempotent_id('d1b8b031-b622-4010-82f9-ff78a9e915c7')
- @test.requires_ext(extension='crossdomain', service='object')
+ @decorators.idempotent_id('d1b8b031-b622-4010-82f9-ff78a9e915c7')
+ @utils.requires_ext(extension='crossdomain', service='object')
def test_get_crossdomain_policy(self):
resp, body = self.account_client.get("crossdomain.xml", {})
+ body = body.decode()
self.assertTrue(body.startswith(self.xml_start) and
body.endswith(self.xml_end))
diff --git a/tempest/api/object_storage/test_healthcheck.py b/tempest/api/object_storage/test_healthcheck.py
index 104253a..a186f9e 100644
--- a/tempest/api/object_storage/test_healthcheck.py
+++ b/tempest/api/object_storage/test_healthcheck.py
@@ -15,7 +15,7 @@
from tempest.api.object_storage import base
from tempest.common import custom_matchers
-from tempest import test
+from tempest.lib import decorators
class HealthcheckTest(base.BaseObjectTest):
@@ -25,7 +25,7 @@
# Turning http://.../v1/foobar into http://.../
self.account_client.skip_path()
- @test.idempotent_id('db5723b1-f25c-49a9-bfeb-7b5640caf337')
+ @decorators.idempotent_id('db5723b1-f25c-49a9-bfeb-7b5640caf337')
def test_get_healthcheck(self):
resp, _ = self.account_client.get("healthcheck", {})
diff --git a/tempest/api/object_storage/test_object_expiry.py b/tempest/api/object_storage/test_object_expiry.py
index 1c9d582..86f7c8c 100644
--- a/tempest/api/object_storage/test_object_expiry.py
+++ b/tempest/api/object_storage/test_object_expiry.py
@@ -13,40 +13,37 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
import time
from tempest.api.object_storage import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class ObjectExpiryTest(base.BaseObjectTest):
@classmethod
def resource_setup(cls):
super(ObjectExpiryTest, cls).resource_setup()
- cls.container_name = data_utils.rand_name(name='TestContainer')
- cls.container_client.create_container(cls.container_name)
+ cls.container_name = cls.create_container()
def setUp(self):
super(ObjectExpiryTest, self).setUp()
# create object
- self.object_name = data_utils.rand_name(name='TestObject')
- resp, _ = self.object_client.create_object(self.container_name,
- self.object_name, '')
+ self.object_name, _ = self.create_object(
+ self.container_name)
@classmethod
def resource_cleanup(cls):
- cls.delete_containers([cls.container_name])
+ cls.delete_containers()
super(ObjectExpiryTest, cls).resource_cleanup()
def _test_object_expiry(self, metadata):
# update object metadata
resp, _ = \
- self.object_client.update_object_metadata(self.container_name,
- self.object_name,
- metadata,
- metadata_prefix='')
+ self.object_client.create_or_update_object_metadata(
+ self.container_name,
+ self.object_name,
+ headers=metadata)
# verify object metadata
resp, _ = \
self.object_client.list_object_metadata(self.container_name,
@@ -57,8 +54,8 @@
# actually expire, so figure out how many secs in the future that is.
sleepy_time = int(resp['x-delete-at']) - int(time.time())
sleepy_time = sleepy_time if sleepy_time > 0 else 0
- resp, body = self.object_client.get_object(self.container_name,
- self.object_name)
+ resp, _ = self.object_client.get_object(self.container_name,
+ self.object_name)
self.assertHeaders(resp, 'Object', 'GET')
self.assertIn('x-delete-at', resp)
@@ -84,14 +81,14 @@
self.container_name,
self.object_name)
- @test.idempotent_id('fb024a42-37f3-4ba5-9684-4f40a7910b41')
+ @decorators.idempotent_id('fb024a42-37f3-4ba5-9684-4f40a7910b41')
def test_get_object_after_expiry_time(self):
# the 10s is important, because the get calls can take 3s each
# some times
metadata = {'X-Delete-After': '10'}
self._test_object_expiry(metadata)
- @test.idempotent_id('e592f18d-679c-48fe-9e36-4be5f47102c5')
+ @decorators.idempotent_id('e592f18d-679c-48fe-9e36-4be5f47102c5')
def test_get_object_at_expiry_time(self):
metadata = {'X-Delete-At': str(int(time.time()) + 10)}
self._test_object_expiry(metadata)
diff --git a/tempest/api/object_storage/test_object_formpost.py b/tempest/api/object_storage/test_object_formpost.py
index 356f560..cd834bf 100644
--- a/tempest/api/object_storage/test_object_formpost.py
+++ b/tempest/api/object_storage/test_object_formpost.py
@@ -19,8 +19,9 @@
from six.moves.urllib import parse as urlparse
from tempest.api.object_storage import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.common import utils
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
class ObjectFormPostTest(base.BaseObjectTest):
@@ -31,15 +32,13 @@
@classmethod
def resource_setup(cls):
super(ObjectFormPostTest, cls).resource_setup()
- cls.container_name = data_utils.rand_name(name='TestContainer')
+ cls.container_name = cls.create_container()
cls.object_name = data_utils.rand_name(name='ObjectTemp')
- cls.container_client.create_container(cls.container_name)
- cls.containers = [cls.container_name]
-
cls.key = 'Meta'
cls.metadata = {'Temp-URL-Key': cls.key}
- cls.account_client.create_account_metadata(metadata=cls.metadata)
+ cls.account_client.create_update_or_delete_account_metadata(
+ create_update_metadata=cls.metadata)
def setUp(self):
super(ObjectFormPostTest, self).setUp()
@@ -55,8 +54,9 @@
@classmethod
def resource_cleanup(cls):
- cls.account_client.delete_account_metadata(metadata=cls.metadata)
- cls.delete_containers(cls.containers)
+ cls.account_client.create_update_or_delete_account_metadata(
+ delete_metadata=cls.metadata)
+ cls.delete_containers()
super(ObjectFormPostTest, cls).resource_cleanup()
def get_multipart_form(self, expires=600):
@@ -75,7 +75,9 @@
max_file_count,
expires)
- signature = hmac.new(self.key, hmac_body, hashlib.sha1).hexdigest()
+ signature = hmac.new(
+ self.key.encode(), hmac_body.encode(), hashlib.sha1
+ ).hexdigest()
fields = {'redirect': redirect,
'max_file_size': str(max_file_size),
@@ -105,8 +107,8 @@
content_type = 'multipart/form-data; boundary=%s' % boundary
return body, content_type
- @test.idempotent_id('80fac02b-6e54-4f7b-be0d-a965b5cbef76')
- @test.requires_ext(extension='formpost', service='object')
+ @decorators.idempotent_id('80fac02b-6e54-4f7b-be0d-a965b5cbef76')
+ @utils.requires_ext(extension='formpost', service='object')
def test_post_object_using_form(self):
body, content_type = self.get_multipart_form()
@@ -122,4 +124,4 @@
resp, body = self.object_client.get("%s/%s%s" % (
self.container_name, self.object_name, "testfile"))
self.assertHeaders(resp, "Object", "GET")
- self.assertEqual(body, "hello world")
+ self.assertEqual(body.decode(), "hello world")
diff --git a/tempest/api/object_storage/test_object_formpost_negative.py b/tempest/api/object_storage/test_object_formpost_negative.py
index 7d9e115..df6a0fd 100644
--- a/tempest/api/object_storage/test_object_formpost_negative.py
+++ b/tempest/api/object_storage/test_object_formpost_negative.py
@@ -17,11 +17,12 @@
import time
from six.moves.urllib import parse as urlparse
-from tempest_lib import exceptions as lib_exc
from tempest.api.object_storage import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.common import utils
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class ObjectFormPostNegativeTest(base.BaseObjectTest):
@@ -32,15 +33,13 @@
@classmethod
def resource_setup(cls):
super(ObjectFormPostNegativeTest, cls).resource_setup()
- cls.container_name = data_utils.rand_name(name='TestContainer')
+ cls.container_name = cls.create_container()
cls.object_name = data_utils.rand_name(name='ObjectTemp')
- cls.container_client.create_container(cls.container_name)
- cls.containers = [cls.container_name]
-
cls.key = 'Meta'
cls.metadata = {'Temp-URL-Key': cls.key}
- cls.account_client.create_account_metadata(metadata=cls.metadata)
+ cls.account_client.create_update_or_delete_account_metadata(
+ create_update_metadata=cls.metadata)
def setUp(self):
super(ObjectFormPostNegativeTest, self).setUp()
@@ -56,8 +55,9 @@
@classmethod
def resource_cleanup(cls):
- cls.account_client.delete_account_metadata(metadata=cls.metadata)
- cls.delete_containers(cls.containers)
+ cls.account_client.create_update_or_delete_account_metadata(
+ delete_metadata=cls.metadata)
+ cls.delete_containers()
super(ObjectFormPostNegativeTest, cls).resource_cleanup()
def get_multipart_form(self, expires=600):
@@ -76,7 +76,9 @@
max_file_count,
expires)
- signature = hmac.new(self.key, hmac_body, hashlib.sha1).hexdigest()
+ signature = hmac.new(
+ self.key.encode(), hmac_body.encode(), hashlib.sha1
+ ).hexdigest()
fields = {'redirect': redirect,
'max_file_size': str(max_file_size),
@@ -106,9 +108,9 @@
content_type = 'multipart/form-data; boundary=%s' % boundary
return body, content_type
- @test.idempotent_id('d3fb3c4d-e627-48ce-9379-a1631f21336d')
- @test.requires_ext(extension='formpost', service='object')
- @test.attr(type=['negative'])
+ @decorators.idempotent_id('d3fb3c4d-e627-48ce-9379-a1631f21336d')
+ @utils.requires_ext(extension='formpost', service='object')
+ @decorators.attr(type=['negative'])
def test_post_object_using_form_expired(self):
body, content_type = self.get_multipart_form(expires=1)
time.sleep(2)
@@ -123,8 +125,9 @@
url, body, headers=headers)
self.assertIn('FormPost: Form Expired', str(exc))
- @test.idempotent_id('b277257f-113c-4499-b8d1-5fead79f7360')
- @test.requires_ext(extension='formpost', service='object')
+ @decorators.idempotent_id('b277257f-113c-4499-b8d1-5fead79f7360')
+ @utils.requires_ext(extension='formpost', service='object')
+ @decorators.attr(type=['negative'])
def test_post_object_using_form_invalid_signature(self):
self.key = "Wrong"
body, content_type = self.get_multipart_form()
diff --git a/tempest/api/object_storage/test_object_services.py b/tempest/api/object_storage/test_object_services.py
index e8b035b..acb578d 100644
--- a/tempest/api/object_storage/test_object_services.py
+++ b/tempest/api/object_storage/test_object_services.py
@@ -19,14 +19,11 @@
import time
import zlib
-import six
-from six import moves
-
from tempest.api.object_storage import base
from tempest.common import custom_matchers
-from tempest.common.utils import data_utils
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
CONF = config.CONF
@@ -36,34 +33,24 @@
@classmethod
def resource_setup(cls):
super(ObjectTest, cls).resource_setup()
- cls.container_name = data_utils.rand_name(name='TestContainer')
- cls.container_client.create_container(cls.container_name)
- cls.containers = [cls.container_name]
+ cls.container_name = cls.create_container()
@classmethod
def resource_cleanup(cls):
- cls.delete_containers(cls.containers)
+ cls.delete_containers()
super(ObjectTest, cls).resource_cleanup()
- def _create_object(self, metadata=None):
- # setup object
- object_name = data_utils.rand_name(name='TestObject')
- data = data_utils.arbitrary_string()
- self.object_client.create_object(self.container_name,
- object_name, data, metadata=metadata)
-
- return object_name, data
-
def _upload_segments(self):
# create object
object_name = data_utils.rand_name(name='LObject')
data = data_utils.arbitrary_string()
segments = 10
- data_segments = [data + str(i) for i in six.moves.xrange(segments)]
+ data_segments = [data + str(i) for i in range(segments)]
# uploading segments
- for i in six.moves.xrange(segments):
- resp, _ = self.object_client.create_object_segments(
- self.container_name, object_name, i, data_segments[i])
+ for i in range(segments):
+ obj_name = "%s/%s" % (object_name, i)
+ self.object_client.create_object(
+ self.container_name, obj_name, data_segments[i])
return object_name, data_segments
@@ -88,17 +75,17 @@
for meta_key in not_in_meta:
self.assertNotIn('x-object-meta-' + meta_key, resp)
- @test.attr(type='smoke')
- @test.idempotent_id('5b4ce26f-3545-46c9-a2ba-5754358a4c62')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('5b4ce26f-3545-46c9-a2ba-5754358a4c62')
def test_create_object(self):
# create object
object_name = data_utils.rand_name(name='TestObject')
- data = data_utils.arbitrary_string()
+ data = data_utils.random_bytes()
resp, _ = self.object_client.create_object(self.container_name,
object_name, data)
# create another object
object_name = data_utils.rand_name(name='TestObject')
- data = data_utils.arbitrary_string()
+ data = data_utils.random_bytes()
resp, _ = self.object_client.create_object(self.container_name,
object_name, data)
self.assertHeaders(resp, 'Object', 'PUT')
@@ -108,11 +95,11 @@
object_name)
self.assertEqual(data, body)
- @test.idempotent_id('5daebb1d-f0d5-4dc9-b541-69672eff00b0')
+ @decorators.idempotent_id('5daebb1d-f0d5-4dc9-b541-69672eff00b0')
def test_create_object_with_content_disposition(self):
# create object with content_disposition
object_name = data_utils.rand_name(name='TestObject')
- data = data_utils.arbitrary_string()
+ data = data_utils.random_bytes()
metadata = {}
metadata['content-disposition'] = 'inline'
resp, _ = self.object_client.create_object(
@@ -130,13 +117,13 @@
self.assertEqual(resp['content-disposition'], 'inline')
self.assertEqual(body, data)
- @test.idempotent_id('605f8317-f945-4bee-ae91-013f1da8f0a0')
+ @decorators.idempotent_id('605f8317-f945-4bee-ae91-013f1da8f0a0')
def test_create_object_with_content_encoding(self):
# create object with content_encoding
object_name = data_utils.rand_name(name='TestObject')
# put compressed string
- data_before = 'x' * 2000
+ data_before = b'x' * 2000
data = zlib.compress(data_before)
metadata = {}
metadata['content-encoding'] = 'deflate'
@@ -157,11 +144,11 @@
metadata=metadata)
self.assertEqual(body, data_before)
- @test.idempotent_id('73820093-0503-40b1-a478-edf0e69c7d1f')
+ @decorators.idempotent_id('73820093-0503-40b1-a478-edf0e69c7d1f')
def test_create_object_with_etag(self):
# create object with etag
object_name = data_utils.rand_name(name='TestObject')
- data = data_utils.arbitrary_string()
+ data = data_utils.random_bytes()
md5 = hashlib.md5(data).hexdigest()
metadata = {'Etag': md5}
resp, _ = self.object_client.create_object(
@@ -176,54 +163,48 @@
object_name)
self.assertEqual(data, body)
- @test.idempotent_id('84dafe57-9666-4f6d-84c8-0814d37923b8')
+ @decorators.idempotent_id('84dafe57-9666-4f6d-84c8-0814d37923b8')
def test_create_object_with_expect_continue(self):
# create object with expect_continue
+
object_name = data_utils.rand_name(name='TestObject')
- data = data_utils.arbitrary_string()
- metadata = {'Expect': '100-continue'}
- resp = self.object_client.create_object_continue(
- self.container_name,
- object_name,
- data,
- metadata=metadata)
+ data = data_utils.random_bytes()
- self.assertIn('status', resp)
- self.assertEqual(resp['status'], '100')
+ status, _ = self.object_client.create_object_continue(
+ self.container_name, object_name, data)
- self.object_client.create_object_continue(
- self.container_name,
- object_name,
- data,
- metadata=None)
+ self.assertEqual(status, 201)
# check uploaded content
_, body = self.object_client.get_object(self.container_name,
object_name)
self.assertEqual(data, body)
- @test.idempotent_id('4f84422a-e2f2-4403-b601-726a4220b54e')
+ @decorators.idempotent_id('4f84422a-e2f2-4403-b601-726a4220b54e')
def test_create_object_with_transfer_encoding(self):
# create object with transfer_encoding
object_name = data_utils.rand_name(name='TestObject')
- data = data_utils.arbitrary_string(1024)
- status, _, resp_headers = self.object_client.put_object_with_chunk(
- container=self.container_name,
- name=object_name,
- contents=moves.cStringIO(data),
- chunk_size=512)
- self.assertHeaders(resp_headers, 'Object', 'PUT')
+ data = data_utils.random_bytes(1024)
+ headers = {'Transfer-Encoding': 'chunked'}
+ resp, _ = self.object_client.create_object(
+ self.container_name,
+ object_name,
+ data=data_utils.chunkify(data, 512),
+ headers=headers,
+ chunked=True)
+
+ self.assertHeaders(resp, 'Object', 'PUT')
# check uploaded content
_, body = self.object_client.get_object(self.container_name,
object_name)
self.assertEqual(data, body)
- @test.idempotent_id('0f3d62a6-47e3-4554-b0e5-1a5dc372d501')
+ @decorators.idempotent_id('0f3d62a6-47e3-4554-b0e5-1a5dc372d501')
def test_create_object_with_x_fresh_metadata(self):
# create object with x_fresh_metadata
object_name_base = data_utils.rand_name(name='TestObject')
- data = data_utils.arbitrary_string()
+ data = data_utils.random_bytes()
metadata_1 = {'X-Object-Meta-test-meta': 'Meta'}
self.object_client.create_object(self.container_name,
object_name_base,
@@ -245,11 +226,11 @@
self.assertNotIn('x-object-meta-test-meta', resp)
self.assertEqual(data, body)
- @test.idempotent_id('1c7ed3e4-2099-406b-b843-5301d4811baf')
+ @decorators.idempotent_id('1c7ed3e4-2099-406b-b843-5301d4811baf')
def test_create_object_with_x_object_meta(self):
# create object with object_meta
object_name = data_utils.rand_name(name='TestObject')
- data = data_utils.arbitrary_string()
+ data = data_utils.random_bytes()
metadata = {'X-Object-Meta-test-meta': 'Meta'}
resp, _ = self.object_client.create_object(
self.container_name,
@@ -264,11 +245,11 @@
self.assertEqual(resp['x-object-meta-test-meta'], 'Meta')
self.assertEqual(data, body)
- @test.idempotent_id('e4183917-33db-4153-85cc-4dacbb938865')
+ @decorators.idempotent_id('e4183917-33db-4153-85cc-4dacbb938865')
def test_create_object_with_x_object_metakey(self):
# create object with the blank value of metadata
object_name = data_utils.rand_name(name='TestObject')
- data = data_utils.arbitrary_string()
+ data = data_utils.random_bytes()
metadata = {'X-Object-Meta-test-meta': ''}
resp, _ = self.object_client.create_object(
self.container_name,
@@ -283,11 +264,11 @@
self.assertEqual(resp['x-object-meta-test-meta'], '')
self.assertEqual(data, body)
- @test.idempotent_id('ce798afc-b278-45de-a5ce-2ea124b98b99')
+ @decorators.idempotent_id('ce798afc-b278-45de-a5ce-2ea124b98b99')
def test_create_object_with_x_remove_object_meta(self):
# create object with x_remove_object_meta
object_name = data_utils.rand_name(name='TestObject')
- data = data_utils.arbitrary_string()
+ data = data_utils.random_bytes()
metadata_add = {'X-Object-Meta-test-meta': 'Meta'}
self.object_client.create_object(self.container_name,
object_name,
@@ -306,11 +287,11 @@
self.assertNotIn('x-object-meta-test-meta', resp)
self.assertEqual(data, body)
- @test.idempotent_id('ad21e342-7916-4f9e-ab62-a1f885f2aaf9')
+ @decorators.idempotent_id('ad21e342-7916-4f9e-ab62-a1f885f2aaf9')
def test_create_object_with_x_remove_object_metakey(self):
# create object with the blank value of remove metadata
object_name = data_utils.rand_name(name='TestObject')
- data = data_utils.arbitrary_string()
+ data = data_utils.random_bytes()
metadata_add = {'X-Object-Meta-test-meta': 'Meta'}
self.object_client.create_object(self.container_name,
object_name,
@@ -329,11 +310,11 @@
self.assertNotIn('x-object-meta-test-meta', resp)
self.assertEqual(data, body)
- @test.idempotent_id('17738d45-03bd-4d45-9e0b-7b2f58f98687')
+ @decorators.idempotent_id('17738d45-03bd-4d45-9e0b-7b2f58f98687')
def test_delete_object(self):
# create object
object_name = data_utils.rand_name(name='TestObject')
- data = data_utils.arbitrary_string()
+ data = data_utils.random_bytes()
resp, _ = self.object_client.create_object(self.container_name,
object_name, data)
# delete object
@@ -341,18 +322,17 @@
object_name)
self.assertHeaders(resp, 'Object', 'DELETE')
- @test.attr(type='smoke')
- @test.idempotent_id('7a94c25d-66e6-434c-9c38-97d4e2c29945')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('7a94c25d-66e6-434c-9c38-97d4e2c29945')
def test_update_object_metadata(self):
# update object metadata
- object_name, data = self._create_object()
+ object_name, _ = self.create_object(self.container_name)
metadata = {'X-Object-Meta-test-meta': 'Meta'}
- resp, _ = self.object_client.update_object_metadata(
+ resp, _ = self.object_client.create_or_update_object_metadata(
self.container_name,
object_name,
- metadata,
- metadata_prefix='')
+ headers=metadata)
self.assertHeaders(resp, 'Object', 'POST')
resp, _ = self.object_client.list_object_metadata(
@@ -361,11 +341,11 @@
self.assertIn('x-object-meta-test-meta', resp)
self.assertEqual(resp['x-object-meta-test-meta'], 'Meta')
- @test.idempotent_id('48650ed0-c189-4e1e-ad6b-1d4770c6e134')
+ @decorators.idempotent_id('48650ed0-c189-4e1e-ad6b-1d4770c6e134')
def test_update_object_metadata_with_remove_metadata(self):
# update object metadata with remove metadata
object_name = data_utils.rand_name(name='TestObject')
- data = data_utils.arbitrary_string()
+ data = data_utils.random_bytes()
create_metadata = {'X-Object-Meta-test-meta1': 'Meta1'}
self.object_client.create_object(self.container_name,
object_name,
@@ -373,11 +353,10 @@
metadata=create_metadata)
update_metadata = {'X-Remove-Object-Meta-test-meta1': 'Meta1'}
- resp, _ = self.object_client.update_object_metadata(
+ resp, _ = self.object_client.create_or_update_object_metadata(
self.container_name,
object_name,
- update_metadata,
- metadata_prefix='')
+ headers=update_metadata)
self.assertHeaders(resp, 'Object', 'POST')
resp, _ = self.object_client.list_object_metadata(
@@ -385,11 +364,11 @@
object_name)
self.assertNotIn('x-object-meta-test-meta1', resp)
- @test.idempotent_id('f726174b-2ded-4708-bff7-729d12ce1f84')
+ @decorators.idempotent_id('f726174b-2ded-4708-bff7-729d12ce1f84')
def test_update_object_metadata_with_create_and_remove_metadata(self):
# creation and deletion of metadata with one request
object_name = data_utils.rand_name(name='TestObject')
- data = data_utils.arbitrary_string()
+ data = data_utils.random_bytes()
create_metadata = {'X-Object-Meta-test-meta1': 'Meta1'}
self.object_client.create_object(self.container_name,
object_name,
@@ -398,11 +377,10 @@
update_metadata = {'X-Object-Meta-test-meta2': 'Meta2',
'X-Remove-Object-Meta-test-meta1': 'Meta1'}
- resp, _ = self.object_client.update_object_metadata(
+ resp, _ = self.object_client.create_or_update_object_metadata(
self.container_name,
object_name,
- update_metadata,
- metadata_prefix='')
+ headers=update_metadata)
self.assertHeaders(resp, 'Object', 'POST')
resp, _ = self.object_client.list_object_metadata(
@@ -412,12 +390,12 @@
self.assertIn('x-object-meta-test-meta2', resp)
self.assertEqual(resp['x-object-meta-test-meta2'], 'Meta2')
- @test.idempotent_id('08854588-6449-4bb7-8cca-f2e1040f5e6f')
+ @decorators.idempotent_id('08854588-6449-4bb7-8cca-f2e1040f5e6f')
def test_update_object_metadata_with_x_object_manifest(self):
# update object metadata with x_object_manifest
# uploading segments
- object_name, data_segments = self._upload_segments()
+ object_name, _ = self._upload_segments()
# creating a manifest file
data_empty = ''
self.object_client.create_object(self.container_name,
@@ -426,30 +404,28 @@
metadata=None)
object_prefix = '%s/%s' % (self.container_name, object_name)
update_metadata = {'X-Object-Manifest': object_prefix}
- resp, _ = self.object_client.update_object_metadata(
+ resp, _ = self.object_client.create_or_update_object_metadata(
self.container_name,
object_name,
- update_metadata,
- metadata_prefix='')
+ headers=update_metadata)
self.assertHeaders(resp, 'Object', 'POST')
resp, _ = self.object_client.list_object_metadata(
self.container_name,
object_name)
self.assertIn('x-object-manifest', resp)
- self.assertNotEqual(len(resp['x-object-manifest']), 0)
+ self.assertNotEmpty(resp['x-object-manifest'])
- @test.idempotent_id('0dbbe89c-6811-4d84-a2df-eca2bdd40c0e')
+ @decorators.idempotent_id('0dbbe89c-6811-4d84-a2df-eca2bdd40c0e')
def test_update_object_metadata_with_x_object_metakey(self):
- # update object metadata with a blenk value of metadata
- object_name, data = self._create_object()
+ # update object metadata with a blank value of metadata
+ object_name, _ = self.create_object(self.container_name)
update_metadata = {'X-Object-Meta-test-meta': ''}
- resp, _ = self.object_client.update_object_metadata(
+ resp, _ = self.object_client.create_or_update_object_metadata(
self.container_name,
object_name,
- update_metadata,
- metadata_prefix='')
+ headers=update_metadata)
self.assertHeaders(resp, 'Object', 'POST')
resp, _ = self.object_client.list_object_metadata(
@@ -458,7 +434,7 @@
self.assertIn('x-object-meta-test-meta', resp)
self.assertEqual(resp['x-object-meta-test-meta'], '')
- @test.idempotent_id('9a88dca4-b684-425b-806f-306cd0e57e42')
+ @decorators.idempotent_id('9a88dca4-b684-425b-806f-306cd0e57e42')
def test_update_object_metadata_with_x_remove_object_metakey(self):
# update object metadata with a blank value of remove metadata
object_name = data_utils.rand_name(name='TestObject')
@@ -470,11 +446,10 @@
metadata=create_metadata)
update_metadata = {'X-Remove-Object-Meta-test-meta': ''}
- resp, _ = self.object_client.update_object_metadata(
+ resp, _ = self.object_client.create_or_update_object_metadata(
self.container_name,
object_name,
- update_metadata,
- metadata_prefix='')
+ headers=update_metadata)
self.assertHeaders(resp, 'Object', 'POST')
resp, _ = self.object_client.list_object_metadata(
@@ -482,12 +457,12 @@
object_name)
self.assertNotIn('x-object-meta-test-meta', resp)
- @test.attr(type='smoke')
- @test.idempotent_id('9a447cf6-de06-48de-8226-a8c6ed31caf2')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('9a447cf6-de06-48de-8226-a8c6ed31caf2')
def test_list_object_metadata(self):
# get object metadata
object_name = data_utils.rand_name(name='TestObject')
- data = data_utils.arbitrary_string()
+ data = data_utils.random_bytes()
metadata = {'X-Object-Meta-test-meta': 'Meta'}
self.object_client.create_object(self.container_name,
object_name,
@@ -501,10 +476,10 @@
self.assertIn('x-object-meta-test-meta', resp)
self.assertEqual(resp['x-object-meta-test-meta'], 'Meta')
- @test.idempotent_id('170fb90e-f5c3-4b1f-ae1b-a18810821172')
+ @decorators.idempotent_id('170fb90e-f5c3-4b1f-ae1b-a18810821172')
def test_list_no_object_metadata(self):
# get empty list of object metadata
- object_name, data = self._create_object()
+ object_name, _ = self.create_object(self.container_name)
resp, _ = self.object_client.list_object_metadata(
self.container_name,
@@ -512,12 +487,12 @@
self.assertHeaders(resp, 'Object', 'HEAD')
self.assertNotIn('x-object-meta-', str(resp))
- @test.idempotent_id('23a3674c-d6de-46c3-86af-ff92bfc8a3da')
+ @decorators.idempotent_id('23a3674c-d6de-46c3-86af-ff92bfc8a3da')
def test_list_object_metadata_with_x_object_manifest(self):
# get object metadata with x_object_manifest
# uploading segments
- object_name, data_segments = self._upload_segments()
+ object_name, _ = self._upload_segments()
# creating a manifest file
object_prefix = '%s/%s' % (self.container_name, object_name)
metadata = {'X-Object-Manifest': object_prefix}
@@ -543,22 +518,22 @@
self.assertTrue(resp['etag'].startswith('\"'))
self.assertTrue(resp['etag'].endswith('\"'))
self.assertTrue(resp['etag'].strip('\"').isalnum())
- self.assertTrue(re.match("^\d+\.?\d*\Z", resp['x-timestamp']))
- self.assertNotEqual(len(resp['content-type']), 0)
+ self.assertTrue(re.match(r"^\d+\.?\d*\Z", resp['x-timestamp']))
+ self.assertNotEmpty(resp['content-type'])
self.assertTrue(re.match("^tx[0-9a-f]{21}-[0-9a-f]{10}.*",
resp['x-trans-id']))
- self.assertNotEqual(len(resp['date']), 0)
+ self.assertNotEmpty(resp['date'])
self.assertEqual(resp['accept-ranges'], 'bytes')
self.assertEqual(resp['x-object-manifest'],
'%s/%s' % (self.container_name, object_name))
- @test.attr(type='smoke')
- @test.idempotent_id('02610ba7-86b7-4272-9ed8-aa8d417cb3cd')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('02610ba7-86b7-4272-9ed8-aa8d417cb3cd')
def test_get_object(self):
# retrieve object's data (in response body)
# create object
- object_name, data = self._create_object()
+ object_name, data = self.create_object(self.container_name)
# get object
resp, body = self.object_client.get_object(self.container_name,
object_name)
@@ -566,11 +541,11 @@
self.assertEqual(body, data)
- @test.idempotent_id('005f9bf6-e06d-41ec-968e-96c78e0b1d82')
+ @decorators.idempotent_id('005f9bf6-e06d-41ec-968e-96c78e0b1d82')
def test_get_object_with_metadata(self):
# get object with metadata
object_name = data_utils.rand_name(name='TestObject')
- data = data_utils.arbitrary_string()
+ data = data_utils.random_bytes()
metadata = {'X-Object-Meta-test-meta': 'Meta'}
self.object_client.create_object(self.container_name,
object_name,
@@ -585,11 +560,11 @@
self.assertEqual(resp['x-object-meta-test-meta'], 'Meta')
self.assertEqual(body, data)
- @test.idempotent_id('05a1890e-7db9-4a6c-90a8-ce998a2bddfa')
+ @decorators.idempotent_id('05a1890e-7db9-4a6c-90a8-ce998a2bddfa')
def test_get_object_with_range(self):
# get object with range
object_name = data_utils.rand_name(name='TestObject')
- data = data_utils.arbitrary_string(100)
+ data = data_utils.random_bytes(100)
self.object_client.create_object(self.container_name,
object_name,
data,
@@ -603,7 +578,7 @@
self.assertHeaders(resp, 'Object', 'GET')
self.assertEqual(body, data[rand_num - 3: rand_num])
- @test.idempotent_id('11b4515b-7ba7-4ca8-8838-357ded86fc10')
+ @decorators.idempotent_id('11b4515b-7ba7-4ca8-8838-357ded86fc10')
def test_get_object_with_x_object_manifest(self):
# get object with x_object_manifest
@@ -635,22 +610,22 @@
self.assertTrue(resp['etag'].startswith('\"'))
self.assertTrue(resp['etag'].endswith('\"'))
self.assertTrue(resp['etag'].strip('\"').isalnum())
- self.assertTrue(re.match("^\d+\.?\d*\Z", resp['x-timestamp']))
- self.assertNotEqual(len(resp['content-type']), 0)
+ self.assertTrue(re.match(r"^\d+\.?\d*\Z", resp['x-timestamp']))
+ self.assertNotEmpty(resp['content-type'])
self.assertTrue(re.match("^tx[0-9a-f]{21}-[0-9a-f]{10}.*",
resp['x-trans-id']))
- self.assertNotEqual(len(resp['date']), 0)
+ self.assertNotEmpty(resp['date'])
self.assertEqual(resp['accept-ranges'], 'bytes')
self.assertEqual(resp['x-object-manifest'],
'%s/%s' % (self.container_name, object_name))
- self.assertEqual(''.join(data_segments), body)
+ self.assertEqual(''.join(data_segments), body.decode())
- @test.idempotent_id('c05b4013-e4de-47af-be84-e598062b16fc')
+ @decorators.idempotent_id('c05b4013-e4de-47af-be84-e598062b16fc')
def test_get_object_with_if_match(self):
# get object with if_match
object_name = data_utils.rand_name(name='TestObject')
- data = data_utils.arbitrary_string(10)
+ data = data_utils.random_bytes(10)
create_md5 = hashlib.md5(data).hexdigest()
create_metadata = {'Etag': create_md5}
self.object_client.create_object(self.container_name,
@@ -666,11 +641,11 @@
self.assertHeaders(resp, 'Object', 'GET')
self.assertEqual(body, data)
- @test.idempotent_id('be133639-e5d2-4313-9b1f-2d59fc054a16')
+ @decorators.idempotent_id('be133639-e5d2-4313-9b1f-2d59fc054a16')
def test_get_object_with_if_modified_since(self):
# get object with if_modified_since
object_name = data_utils.rand_name(name='TestObject')
- data = data_utils.arbitrary_string()
+ data = data_utils.random_bytes()
time_now = time.time()
self.object_client.create_object(self.container_name,
object_name,
@@ -686,11 +661,11 @@
self.assertHeaders(resp, 'Object', 'GET')
self.assertEqual(body, data)
- @test.idempotent_id('641500d5-1612-4042-a04d-01fc4528bc30')
+ @decorators.idempotent_id('641500d5-1612-4042-a04d-01fc4528bc30')
def test_get_object_with_if_none_match(self):
# get object with if_none_match
object_name = data_utils.rand_name(name='TestObject')
- data = data_utils.arbitrary_string(10)
+ data = data_utils.random_bytes()
create_md5 = hashlib.md5(data).hexdigest()
create_metadata = {'Etag': create_md5}
self.object_client.create_object(self.container_name,
@@ -698,7 +673,7 @@
data,
metadata=create_metadata)
- list_data = data_utils.arbitrary_string(15)
+ list_data = data_utils.random_bytes()
list_md5 = hashlib.md5(list_data).hexdigest()
list_metadata = {'If-None-Match': list_md5}
resp, body = self.object_client.get_object(
@@ -708,10 +683,10 @@
self.assertHeaders(resp, 'Object', 'GET')
self.assertEqual(body, data)
- @test.idempotent_id('0aa1201c-10aa-467a-bee7-63cbdd463152')
+ @decorators.idempotent_id('0aa1201c-10aa-467a-bee7-63cbdd463152')
def test_get_object_with_if_unmodified_since(self):
# get object with if_unmodified_since
- object_name, data = self._create_object()
+ object_name, data = self.create_object(self.container_name)
time_now = time.time()
http_date = time.ctime(time_now + 86400)
@@ -723,10 +698,10 @@
self.assertHeaders(resp, 'Object', 'GET')
self.assertEqual(body, data)
- @test.idempotent_id('94587078-475f-48f9-a40f-389c246e31cd')
+ @decorators.idempotent_id('94587078-475f-48f9-a40f-389c246e31cd')
def test_get_object_with_x_newest(self):
# get object with x_newest
- object_name, data = self._create_object()
+ object_name, data = self.create_object(self.container_name)
list_metadata = {'X-Newest': 'true'}
resp, body = self.object_client.get_object(
@@ -736,25 +711,28 @@
self.assertHeaders(resp, 'Object', 'GET')
self.assertEqual(body, data)
- @test.idempotent_id('1a9ab572-1b66-4981-8c21-416e2a5e6011')
+ @decorators.idempotent_id('1a9ab572-1b66-4981-8c21-416e2a5e6011')
def test_copy_object_in_same_container(self):
# create source object
src_object_name = data_utils.rand_name(name='SrcObject')
- src_data = data_utils.arbitrary_string(size=len(src_object_name) * 2,
- base_text=src_object_name)
+ src_data = data_utils.random_bytes(size=len(src_object_name) * 2)
resp, _ = self.object_client.create_object(self.container_name,
src_object_name,
src_data)
# create destination object
dst_object_name = data_utils.rand_name(name='DstObject')
- dst_data = data_utils.arbitrary_string(size=len(dst_object_name) * 3,
- base_text=dst_object_name)
+ dst_data = data_utils.random_bytes(size=len(dst_object_name) * 3)
resp, _ = self.object_client.create_object(self.container_name,
dst_object_name,
dst_data)
# copy source object to destination
- resp, _ = self.object_client.copy_object_in_same_container(
- self.container_name, src_object_name, dst_object_name)
+ headers = {}
+ headers['X-Copy-From'] = "%s/%s" % (str(self.container_name),
+ str(src_object_name))
+ resp, body = self.object_client.create_object(self.container_name,
+ dst_object_name,
+ data=None,
+ headers=headers)
self.assertHeaders(resp, 'Object', 'PUT')
# check data
@@ -762,20 +740,26 @@
dst_object_name)
self.assertEqual(body, src_data)
- @test.idempotent_id('2248abba-415d-410b-9c30-22dff9cd6e67')
+ @decorators.idempotent_id('2248abba-415d-410b-9c30-22dff9cd6e67')
def test_copy_object_to_itself(self):
# change the content type of an existing object
# create object
- object_name, data = self._create_object()
+ object_name, _ = self.create_object(self.container_name)
# get the old content type
resp_tmp, _ = self.object_client.list_object_metadata(
self.container_name, object_name)
# change the content type of the object
metadata = {'content-type': 'text/plain; charset=UTF-8'}
self.assertNotEqual(resp_tmp['content-type'], metadata['content-type'])
- resp, _ = self.object_client.copy_object_in_same_container(
- self.container_name, object_name, object_name, metadata)
+ headers = {}
+ headers['X-Copy-From'] = "%s/%s" % (str(self.container_name),
+ str(object_name))
+ resp, body = self.object_client.create_object(self.container_name,
+ object_name,
+ data=None,
+ metadata=metadata,
+ headers=headers)
self.assertHeaders(resp, 'Object', 'PUT')
# check the content type
@@ -783,18 +767,16 @@
object_name)
self.assertEqual(resp['content-type'], metadata['content-type'])
- @test.idempotent_id('06f90388-2d0e-40aa-934c-e9a8833e958a')
+ @decorators.idempotent_id('06f90388-2d0e-40aa-934c-e9a8833e958a')
def test_copy_object_2d_way(self):
# create source object
src_object_name = data_utils.rand_name(name='SrcObject')
- src_data = data_utils.arbitrary_string(size=len(src_object_name) * 2,
- base_text=src_object_name)
+ src_data = data_utils.random_bytes(size=len(src_object_name) * 2)
resp, _ = self.object_client.create_object(self.container_name,
src_object_name, src_data)
# create destination object
dst_object_name = data_utils.rand_name(name='DstObject')
- dst_data = data_utils.arbitrary_string(size=len(dst_object_name) * 3,
- base_text=dst_object_name)
+ dst_data = data_utils.random_bytes(size=len(dst_object_name) * 3)
resp, _ = self.object_client.create_object(self.container_name,
dst_object_name, dst_data)
# copy source object to destination
@@ -809,36 +791,40 @@
# check data
self._check_copied_obj(dst_object_name, src_data)
- @test.idempotent_id('aa467252-44f3-472a-b5ae-5b57c3c9c147')
+ @decorators.idempotent_id('aa467252-44f3-472a-b5ae-5b57c3c9c147')
def test_copy_object_across_containers(self):
# create a container to use as a source container
src_container_name = data_utils.rand_name(name='TestSourceContainer')
- self.container_client.create_container(src_container_name)
+ self.container_client.update_container(src_container_name)
self.containers.append(src_container_name)
# create a container to use as a destination container
dst_container_name = data_utils.rand_name(
name='TestDestinationContainer')
- self.container_client.create_container(dst_container_name)
+ self.container_client.update_container(dst_container_name)
self.containers.append(dst_container_name)
# create object in source container
object_name = data_utils.rand_name(name='Object')
- data = data_utils.arbitrary_string(size=len(object_name) * 2,
- base_text=object_name)
+ data = data_utils.random_bytes(size=len(object_name) * 2)
resp, _ = self.object_client.create_object(src_container_name,
object_name, data)
# set object metadata
meta_key = data_utils.rand_name(name='test')
meta_value = data_utils.rand_name(name='MetaValue')
- orig_metadata = {meta_key: meta_value}
- resp, _ = self.object_client.update_object_metadata(src_container_name,
- object_name,
- orig_metadata)
+ orig_metadata = {'X-Object-Meta-' + meta_key: meta_value}
+ resp, _ = self.object_client.create_or_update_object_metadata(
+ src_container_name,
+ object_name,
+ headers=orig_metadata)
self.assertHeaders(resp, 'Object', 'POST')
# copy object from source container to destination container
- resp, _ = self.object_client.copy_object_across_containers(
- src_container_name, object_name, dst_container_name,
- object_name)
+ headers = {}
+ headers['X-Copy-From'] = "%s/%s" % (str(src_container_name),
+ str(object_name))
+ resp, body = self.object_client.create_object(dst_container_name,
+ object_name,
+ data=None,
+ headers=headers)
self.assertHeaders(resp, 'Object', 'PUT')
# check if object is present in destination container
@@ -849,11 +835,12 @@
self.assertIn(actual_meta_key, resp)
self.assertEqual(resp[actual_meta_key], meta_value)
- @test.idempotent_id('5a9e2cc6-85b6-46fc-916d-0cbb7a88e5fd')
+ @decorators.idempotent_id('5a9e2cc6-85b6-46fc-916d-0cbb7a88e5fd')
def test_copy_object_with_x_fresh_metadata(self):
# create source object
metadata = {'x-object-meta-src': 'src_value'}
- src_object_name, data = self._create_object(metadata)
+ src_object_name, data = self.create_object(self.container_name,
+ metadata=metadata)
# copy source object with x_fresh_metadata header
metadata = {'X-Fresh-Metadata': 'true'}
@@ -869,11 +856,12 @@
# check that destination object does NOT have any object-meta
self._check_copied_obj(dst_object_name, data, not_in_meta=["src"])
- @test.idempotent_id('a28a8b99-e701-4d7e-9d84-3b66f121460b')
+ @decorators.idempotent_id('a28a8b99-e701-4d7e-9d84-3b66f121460b')
def test_copy_object_with_x_object_metakey(self):
# create source object
metadata = {'x-object-meta-src': 'src_value'}
- src_obj_name, data = self._create_object(metadata)
+ src_obj_name, data = self.create_object(self.container_name,
+ metadata=metadata)
# copy source object to destination with x-object-meta-key
metadata = {'x-object-meta-test': ''}
@@ -884,18 +872,19 @@
expected = {'x-object-meta-test': '',
'x-object-meta-src': 'src_value',
'x-copied-from': self.container_name + "/" + src_obj_name}
- for key, value in six.iteritems(expected):
+ for key, value in expected.items():
self.assertIn(key, resp)
self.assertEqual(value, resp[key])
# check destination object
self._check_copied_obj(dst_obj_name, data, in_meta=["test", "src"])
- @test.idempotent_id('edabedca-24c3-4322-9b70-d6d9f942a074')
+ @decorators.idempotent_id('edabedca-24c3-4322-9b70-d6d9f942a074')
def test_copy_object_with_x_object_meta(self):
# create source object
metadata = {'x-object-meta-src': 'src_value'}
- src_obj_name, data = self._create_object(metadata)
+ src_obj_name, data = self.create_object(self.container_name,
+ metadata=metadata)
# copy source object to destination with object metadata
metadata = {'x-object-meta-test': 'value'}
@@ -906,24 +895,25 @@
expected = {'x-object-meta-test': 'value',
'x-object-meta-src': 'src_value',
'x-copied-from': self.container_name + "/" + src_obj_name}
- for key, value in six.iteritems(expected):
+ for key, value in expected.items():
self.assertIn(key, resp)
self.assertEqual(value, resp[key])
# check destination object
self._check_copied_obj(dst_obj_name, data, in_meta=["test", "src"])
- @test.idempotent_id('e3e6a64a-9f50-4955-b987-6ce6767c97fb')
+ @decorators.idempotent_id('e3e6a64a-9f50-4955-b987-6ce6767c97fb')
def test_object_upload_in_segments(self):
# create object
object_name = data_utils.rand_name(name='LObject')
data = data_utils.arbitrary_string()
segments = 10
- data_segments = [data + str(i) for i in six.moves.xrange(segments)]
+ data_segments = [data + str(i) for i in range(segments)]
# uploading segments
- for i in six.moves.xrange(segments):
- resp, _ = self.object_client.create_object_segments(
- self.container_name, object_name, i, data_segments[i])
+ for i in range(segments):
+ obj_name = "%s/%s" % (object_name, i)
+ resp, _ = self.object_client.create_object(
+ self.container_name, obj_name, data_segments[i])
# creating a manifest file
metadata = {'X-Object-Manifest': '%s/%s/'
% (self.container_name, object_name)}
@@ -931,8 +921,8 @@
object_name, data='')
self.assertHeaders(resp, 'Object', 'PUT')
- resp, _ = self.object_client.update_object_metadata(
- self.container_name, object_name, metadata, metadata_prefix='')
+ resp, _ = self.object_client.create_or_update_object_metadata(
+ self.container_name, object_name, headers=metadata)
self.assertHeaders(resp, 'Object', 'POST')
resp, _ = self.object_client.list_object_metadata(
@@ -953,15 +943,15 @@
# downloading the object
resp, body = self.object_client.get_object(
self.container_name, object_name)
- self.assertEqual(''.join(data_segments), body)
+ self.assertEqual(''.join(data_segments), body.decode())
- @test.idempotent_id('50d01f12-526f-4360-9ac2-75dd508d7b68')
+ @decorators.idempotent_id('50d01f12-526f-4360-9ac2-75dd508d7b68')
def test_get_object_if_different(self):
# http://en.wikipedia.org/wiki/HTTP_ETag
# Make a conditional request for an object using the If-None-Match
# header, it should get downloaded only if the local file is different,
# otherwise the response code should be 304 Not Modified
- object_name, data = self._create_object()
+ object_name, data = self.create_object(self.container_name)
# local copy is identical, no download
md5 = hashlib.md5(data).hexdigest()
headers = {'If-None-Match': md5}
@@ -972,18 +962,15 @@
# When the file is not downloaded from Swift server, response does
# not contain 'X-Timestamp' header. This is the special case, therefore
# the existence of response headers is checked without custom matcher.
- self.assertIn('content-type', resp)
- self.assertIn('x-trans-id', resp)
self.assertIn('date', resp)
- self.assertIn('accept-ranges', resp)
# Check only the format of common headers with custom matcher
self.assertThat(resp, custom_matchers.AreAllWellFormatted())
# local copy is different, download
local_data = "something different"
- md5 = hashlib.md5(local_data).hexdigest()
+ md5 = hashlib.md5(local_data.encode()).hexdigest()
headers = {'If-None-Match': md5}
- resp, body = self.object_client.get(url, headers=headers)
+ resp, _ = self.object_client.get(url, headers=headers)
self.assertHeaders(resp, 'Object', 'GET')
@@ -995,38 +982,39 @@
@classmethod
def setup_credentials(cls):
super(PublicObjectTest, cls).setup_credentials()
- cls.os = cls.os_roles_operator
cls.os_alt = cls.os_roles_operator_alt
@classmethod
def setup_clients(cls):
super(PublicObjectTest, cls).setup_clients()
- cls.identity_client_alt = cls.os_alt.identity_client
+ cls.object_client_alt = cls.os_alt.object_client
def setUp(self):
super(PublicObjectTest, self).setUp()
self.container_name = data_utils.rand_name(name='TestContainer')
- self.container_client.create_container(self.container_name)
+ self.container_client.update_container(self.container_name)
def tearDown(self):
self.delete_containers([self.container_name])
super(PublicObjectTest, self).tearDown()
- @test.idempotent_id('07c9cf95-c0d4-4b49-b9c8-0ef2c9b27193')
+ @decorators.idempotent_id('07c9cf95-c0d4-4b49-b9c8-0ef2c9b27193')
def test_access_public_container_object_without_using_creds(self):
# make container public-readable and access an object in it object
# anonymously, without using credentials
# update container metadata to make it publicly readable
cont_headers = {'X-Container-Read': '.r:*,.rlistings'}
- resp_meta, body = self.container_client.update_container_metadata(
- self.container_name, metadata=cont_headers, metadata_prefix='')
+ resp_meta, body = (
+ self.container_client.create_update_or_delete_container_metadata(
+ self.container_name,
+ create_update_metadata=cont_headers,
+ create_update_metadata_prefix=''))
self.assertHeaders(resp_meta, 'Container', 'POST')
# create object
object_name = data_utils.rand_name(name='Object')
- data = data_utils.arbitrary_string(size=len(object_name),
- base_text=object_name)
+ data = data_utils.random_bytes(size=len(object_name))
resp, _ = self.object_client.create_object(self.container_name,
object_name, data)
self.assertHeaders(resp, 'Object', 'PUT')
@@ -1050,20 +1038,20 @@
self.assertEqual(body, data)
- @test.idempotent_id('54e2a2fe-42dc-491b-8270-8e4217dd4cdc')
+ @decorators.idempotent_id('54e2a2fe-42dc-491b-8270-8e4217dd4cdc')
def test_access_public_object_with_another_user_creds(self):
# make container public-readable and access an object in it using
# another user's credentials
cont_headers = {'X-Container-Read': '.r:*,.rlistings'}
- resp_meta, body = self.container_client.update_container_metadata(
- self.container_name, metadata=cont_headers,
- metadata_prefix='')
+ resp_meta, body = (
+ self.container_client.create_update_or_delete_container_metadata(
+ self.container_name, create_update_metadata=cont_headers,
+ create_update_metadata_prefix=''))
self.assertHeaders(resp_meta, 'Container', 'POST')
# create object
object_name = data_utils.rand_name(name='Object')
- data = data_utils.arbitrary_string(size=len(object_name) * 1,
- base_text=object_name)
+ data = data_utils.random_bytes(size=len(object_name))
resp, _ = self.object_client.create_object(self.container_name,
object_name, data)
self.assertHeaders(resp, 'Object', 'PUT')
@@ -1077,7 +1065,7 @@
self.assertEqual(resp['x-container-read'], '.r:*,.rlistings')
# get auth token of alternative user
- alt_auth_data = self.identity_client_alt.auth_provider.auth_data
+ alt_auth_data = self.object_client_alt.auth_provider.auth_data
self.object_client.auth_provider.set_alt_auth_data(
request_part='headers',
auth_data=alt_auth_data
diff --git a/tempest/api/object_storage/test_object_slo.py b/tempest/api/object_storage/test_object_slo.py
index 5811cb8..c66776e 100644
--- a/tempest/api/object_storage/test_object_slo.py
+++ b/tempest/api/object_storage/test_object_slo.py
@@ -15,12 +15,13 @@
import hashlib
from oslo_serialization import jsonutils as json
-from tempest_lib import exceptions as lib_exc
from tempest.api.object_storage import base
from tempest.common import custom_matchers
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.common import utils
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
# Each segment, except for the final one, must be at least 1 megabyte
MIN_SEGMENT_SIZE = 1024 * 1024
@@ -30,18 +31,14 @@
def setUp(self):
super(ObjectSloTest, self).setUp()
- self.container_name = data_utils.rand_name(name='TestContainer')
- self.container_client.create_container(self.container_name)
+ self.container_name = self.create_container()
self.objects = []
def tearDown(self):
for obj in self.objects:
- try:
- self.object_client.delete_object(
- self.container_name,
- obj)
- except lib_exc.NotFound:
- pass
+ test_utils.call_and_ignore_notfound_exc(
+ self.object_client.delete_object,
+ self.container_name, obj)
self.container_client.delete_container(self.container_name)
super(ObjectSloTest, self).tearDown()
@@ -60,7 +57,7 @@
object_name_base_1 = object_name + '_01'
object_name_base_2 = object_name + '_02'
data_size = MIN_SEGMENT_SIZE
- self.content = data_utils.arbitrary_string(data_size)
+ self.content = data_utils.random_bytes(data_size)
self._create_object(self.container_name,
object_name_base_1,
self.content)
@@ -109,8 +106,8 @@
resp['etag'] = resp['etag'].strip('"')
self.assertHeaders(resp, 'Object', method)
- @test.idempotent_id('2c3f24a6-36e8-4711-9aa2-800ee1fc7b5b')
- @test.requires_ext(extension='slo', service='object')
+ @decorators.idempotent_id('2c3f24a6-36e8-4711-9aa2-800ee1fc7b5b')
+ @utils.requires_ext(extension='slo', service='object')
def test_upload_manifest(self):
# create static large object from multipart manifest
manifest = self._create_manifest()
@@ -124,20 +121,20 @@
self._assertHeadersSLO(resp, 'PUT')
- @test.idempotent_id('e69ad766-e1aa-44a2-bdd2-bf62c09c1456')
- @test.requires_ext(extension='slo', service='object')
+ @decorators.idempotent_id('e69ad766-e1aa-44a2-bdd2-bf62c09c1456')
+ @utils.requires_ext(extension='slo', service='object')
def test_list_large_object_metadata(self):
# list static large object metadata using multipart manifest
object_name = self._create_large_object()
- resp, body = self.object_client.list_object_metadata(
+ resp, _ = self.object_client.list_object_metadata(
self.container_name,
object_name)
self._assertHeadersSLO(resp, 'HEAD')
- @test.idempotent_id('49bc49bc-dd1b-4c0f-904e-d9f10b830ee8')
- @test.requires_ext(extension='slo', service='object')
+ @decorators.idempotent_id('49bc49bc-dd1b-4c0f-904e-d9f10b830ee8')
+ @utils.requires_ext(extension='slo', service='object')
def test_retrieve_large_object(self):
# list static large object using multipart manifest
object_name = self._create_large_object()
@@ -151,14 +148,14 @@
sum_data = self.content + self.content
self.assertEqual(body, sum_data)
- @test.idempotent_id('87b6dfa1-abe9-404d-8bf0-6c3751e6aa77')
- @test.requires_ext(extension='slo', service='object')
+ @decorators.idempotent_id('87b6dfa1-abe9-404d-8bf0-6c3751e6aa77')
+ @utils.requires_ext(extension='slo', service='object')
def test_delete_large_object(self):
# delete static large object using multipart manifest
object_name = self._create_large_object()
params_del = {'multipart-manifest': 'delete'}
- resp, body = self.object_client.delete_object(
+ resp, _ = self.object_client.delete_object(
self.container_name,
object_name,
params=params_del)
@@ -175,6 +172,6 @@
# Check only the format of common headers with custom matcher
self.assertThat(resp, custom_matchers.AreAllWellFormatted())
- resp, body = self.container_client.list_container_contents(
+ resp, body = self.container_client.list_container_objects(
self.container_name)
self.assertEqual(int(resp['x-container-object-count']), 0)
diff --git a/tempest/api/object_storage/test_object_temp_url.py b/tempest/api/object_storage/test_object_temp_url.py
index 3d28f6e..b99f93a 100644
--- a/tempest/api/object_storage/test_object_temp_url.py
+++ b/tempest/api/object_storage/test_object_temp_url.py
@@ -19,11 +19,9 @@
from six.moves.urllib import parse as urlparse
from tempest.api.object_storage import base
-from tempest.common.utils import data_utils
-from tempest import config
-from tempest import test
-
-CONF = config.CONF
+from tempest.common import utils
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
class ObjectTempUrlTest(base.BaseObjectTest):
@@ -32,31 +30,26 @@
def resource_setup(cls):
super(ObjectTempUrlTest, cls).resource_setup()
# create a container
- cls.container_name = data_utils.rand_name(name='TestContainer')
- cls.container_client.create_container(cls.container_name)
- cls.containers = [cls.container_name]
+ cls.container_name = cls.create_container()
# update account metadata
cls.key = 'Meta'
cls.metadatas = []
metadata = {'Temp-URL-Key': cls.key}
cls.metadatas.append(metadata)
- cls.account_client.create_account_metadata(metadata=metadata)
+ cls.account_client.create_update_or_delete_account_metadata(
+ create_update_metadata=metadata)
# create an object
- cls.object_name = data_utils.rand_name(name='ObjectTemp')
- cls.content = data_utils.arbitrary_string(size=len(cls.object_name),
- base_text=cls.object_name)
- cls.object_client.create_object(cls.container_name,
- cls.object_name, cls.content)
+ cls.object_name, cls.content = cls.create_object(cls.container_name)
@classmethod
def resource_cleanup(cls):
for metadata in cls.metadatas:
- cls.account_client.delete_account_metadata(
- metadata=metadata)
+ cls.account_client.create_update_or_delete_account_metadata(
+ delete_metadata=metadata)
- cls.delete_containers(cls.containers)
+ cls.delete_containers()
super(ObjectTempUrlTest, cls).resource_cleanup()
@@ -84,7 +77,9 @@
container, object_name)
hmac_body = '%s\n%s\n%s' % (method, expires, path)
- sig = hmac.new(key, hmac_body, hashlib.sha1).hexdigest()
+ sig = hmac.new(
+ key.encode(), hmac_body.encode(), hashlib.sha1
+ ).hexdigest()
url = "%s/%s?temp_url_sig=%s&temp_url_expires=%s" % (container,
object_name,
@@ -92,8 +87,8 @@
return url
- @test.idempotent_id('f91c96d4-1230-4bba-8eb9-84476d18d991')
- @test.requires_ext(extension='tempurl', service='object')
+ @decorators.idempotent_id('f91c96d4-1230-4bba-8eb9-84476d18d991')
+ @utils.requires_ext(extension='tempurl', service='object')
def test_get_object_using_temp_url(self):
expires = self._get_expiry_date()
@@ -108,15 +103,16 @@
self.assertEqual(body, self.content)
# Testing a HEAD on this Temp URL
- resp, body = self.object_client.head(url)
+ resp, _ = self.object_client.head(url)
self.assertHeaders(resp, 'Object', 'HEAD')
- @test.idempotent_id('671f9583-86bd-4128-a034-be282a68c5d8')
- @test.requires_ext(extension='tempurl', service='object')
+ @decorators.idempotent_id('671f9583-86bd-4128-a034-be282a68c5d8')
+ @utils.requires_ext(extension='tempurl', service='object')
def test_get_object_using_temp_url_key_2(self):
key2 = 'Meta2-'
metadata = {'Temp-URL-Key-2': key2}
- self.account_client.create_account_metadata(metadata=metadata)
+ self.account_client.create_update_or_delete_account_metadata(
+ create_update_metadata=metadata)
self.metadatas.append(metadata)
# make sure the metadata has been set
@@ -132,15 +128,13 @@
url = self._get_temp_url(self.container_name,
self.object_name, "GET",
expires, key2)
- resp, body = self.object_client.get(url)
+ _, body = self.object_client.get(url)
self.assertEqual(body, self.content)
- @test.idempotent_id('9b08dade-3571-4152-8a4f-a4f2a873a735')
- @test.requires_ext(extension='tempurl', service='object')
+ @decorators.idempotent_id('9b08dade-3571-4152-8a4f-a4f2a873a735')
+ @utils.requires_ext(extension='tempurl', service='object')
def test_put_object_using_temp_url(self):
- new_data = data_utils.arbitrary_string(
- size=len(self.object_name),
- base_text=data_utils.rand_name(name="random"))
+ new_data = data_utils.random_bytes(size=len(self.object_name))
expires = self._get_expiry_date()
url = self._get_temp_url(self.container_name,
@@ -148,11 +142,11 @@
expires, self.key)
# trying to put random data in the object using temp url
- resp, body = self.object_client.put(url, new_data, None)
+ resp, _ = self.object_client.put(url, new_data, None)
self.assertHeaders(resp, 'Object', 'PUT')
# Testing a HEAD on this Temp URL
- resp, body = self.object_client.head(url)
+ resp, _ = self.object_client.head(url)
self.assertHeaders(resp, 'Object', 'HEAD')
# Validate that the content of the object has been modified
@@ -163,8 +157,8 @@
_, body = self.object_client.get(url)
self.assertEqual(body, new_data)
- @test.idempotent_id('249a0111-5ad3-4534-86a7-1993d55f9185')
- @test.requires_ext(extension='tempurl', service='object')
+ @decorators.idempotent_id('249a0111-5ad3-4534-86a7-1993d55f9185')
+ @utils.requires_ext(extension='tempurl', service='object')
def test_head_object_using_temp_url(self):
expires = self._get_expiry_date()
@@ -174,11 +168,11 @@
expires, self.key)
# Testing a HEAD on this Temp URL
- resp, body = self.object_client.head(url)
+ resp, _ = self.object_client.head(url)
self.assertHeaders(resp, 'Object', 'HEAD')
- @test.idempotent_id('9d9cfd90-708b-465d-802c-e4a8090b823d')
- @test.requires_ext(extension='tempurl', service='object')
+ @decorators.idempotent_id('9d9cfd90-708b-465d-802c-e4a8090b823d')
+ @utils.requires_ext(extension='tempurl', service='object')
def test_get_object_using_temp_url_with_inline_query_parameter(self):
expires = self._get_expiry_date()
diff --git a/tempest/api/object_storage/test_object_temp_url_negative.py b/tempest/api/object_storage/test_object_temp_url_negative.py
index 6d06143..17ae6c1 100644
--- a/tempest/api/object_storage/test_object_temp_url_negative.py
+++ b/tempest/api/object_storage/test_object_temp_url_negative.py
@@ -17,11 +17,12 @@
import time
from six.moves.urllib import parse as urlparse
-from tempest_lib import exceptions as lib_exc
from tempest.api.object_storage import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.common import utils
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class ObjectTempUrlNegativeTest(base.BaseObjectTest):
@@ -33,23 +34,22 @@
def resource_setup(cls):
super(ObjectTempUrlNegativeTest, cls).resource_setup()
- cls.container_name = data_utils.rand_name(name='TestContainer')
- cls.container_client.create_container(cls.container_name)
- cls.containers = [cls.container_name]
+ cls.container_name = cls.create_container()
# update account metadata
cls.key = 'Meta'
cls.metadata = {'Temp-URL-Key': cls.key}
- cls.account_client.create_account_metadata(metadata=cls.metadata)
+ cls.account_client.create_update_or_delete_account_metadata(
+ create_update_metadata=cls.metadata)
cls.account_client_metadata, _ = \
cls.account_client.list_account_metadata()
@classmethod
def resource_cleanup(cls):
- resp, _ = cls.account_client.delete_account_metadata(
- metadata=cls.metadata)
+ cls.account_client.create_update_or_delete_account_metadata(
+ delete_metadata=cls.metadata)
- cls.delete_containers(cls.containers)
+ cls.delete_containers()
super(ObjectTempUrlNegativeTest, cls).resource_cleanup()
@@ -65,10 +65,10 @@
# create object
self.object_name = data_utils.rand_name(name='ObjectTemp')
- self.content = data_utils.arbitrary_string(size=len(self.object_name),
- base_text=self.object_name)
+ content = data_utils.arbitrary_string(size=len(self.object_name),
+ base_text=self.object_name)
self.object_client.create_object(self.container_name,
- self.object_name, self.content)
+ self.object_name, content)
def _get_expiry_date(self, expiration_time=1000):
return int(time.time() + expiration_time)
@@ -82,7 +82,9 @@
container, object_name)
hmac_body = '%s\n%s\n%s' % (method, expires, path)
- sig = hmac.new(key, hmac_body, hashlib.sha1).hexdigest()
+ sig = hmac.new(
+ key.encode(), hmac_body.encode(), hashlib.sha1
+ ).hexdigest()
url = "%s/%s?temp_url_sig=%s&temp_url_expires=%s" % (container,
object_name,
@@ -90,9 +92,9 @@
return url
- @test.attr(type=['negative'])
- @test.idempotent_id('5a583aca-c804-41ba-9d9a-e7be132bdf0b')
- @test.requires_ext(extension='tempurl', service='object')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('5a583aca-c804-41ba-9d9a-e7be132bdf0b')
+ @utils.requires_ext(extension='tempurl', service='object')
def test_get_object_after_expiration_time(self):
expires = self._get_expiry_date(1)
diff --git a/tempest/api/object_storage/test_object_version.py b/tempest/api/object_storage/test_object_version.py
index 24ec3f5..75111b6 100644
--- a/tempest/api/object_storage/test_object_version.py
+++ b/tempest/api/object_storage/test_object_version.py
@@ -16,24 +16,14 @@
import testtools
from tempest.api.object_storage import base
-from tempest.common.utils import data_utils
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
CONF = config.CONF
class ContainerTest(base.BaseObjectTest):
- @classmethod
- def resource_setup(cls):
- super(ContainerTest, cls).resource_setup()
- cls.containers = []
-
- @classmethod
- def resource_cleanup(cls):
- cls.delete_containers(cls.containers)
- super(ContainerTest, cls).resource_cleanup()
-
def assertContainer(self, container, count, byte, versioned):
resp, _ = self.container_client.list_container_metadata(container)
self.assertHeaders(resp, 'Container', 'HEAD')
@@ -44,50 +34,56 @@
header_value = resp.get('x-versions-location', 'Missing Header')
self.assertEqual(header_value, versioned)
- @test.idempotent_id('a151e158-dcbf-4a1f-a1e7-46cd65895a6f')
+ @decorators.idempotent_id('a151e158-dcbf-4a1f-a1e7-46cd65895a6f')
@testtools.skipIf(
not CONF.object_storage_feature_enabled.object_versioning,
'Object-versioning is disabled')
def test_versioned_container(self):
# create container
vers_container_name = data_utils.rand_name(name='TestVersionContainer')
- resp, body = self.container_client.create_container(
- vers_container_name)
- self.containers.append(vers_container_name)
+ resp, _ = self.container_client.update_container(vers_container_name)
+ self.addCleanup(base.delete_containers,
+ [vers_container_name],
+ self.container_client,
+ self.object_client)
self.assertHeaders(resp, 'Container', 'PUT')
self.assertContainer(vers_container_name, '0', '0', 'Missing Header')
base_container_name = data_utils.rand_name(name='TestBaseContainer')
headers = {'X-versions-Location': vers_container_name}
- resp, body = self.container_client.create_container(
+ resp, _ = self.container_client.update_container(
base_container_name,
- metadata=headers,
- metadata_prefix='')
- self.containers.append(base_container_name)
+ **headers)
+ self.addCleanup(base.delete_containers,
+ [base_container_name],
+ self.container_client,
+ self.object_client)
self.assertHeaders(resp, 'Container', 'PUT')
self.assertContainer(base_container_name, '0', '0',
vers_container_name)
object_name = data_utils.rand_name(name='TestObject')
# create object
+ data_1 = data_utils.random_bytes()
resp, _ = self.object_client.create_object(base_container_name,
- object_name, '1')
+ object_name, data_1)
# create 2nd version of object
+ data_2 = data_utils.random_bytes()
resp, _ = self.object_client.create_object(base_container_name,
- object_name, '2')
- resp, body = self.object_client.get_object(base_container_name,
- object_name)
- self.assertEqual(body, '2')
+ object_name, data_2)
+ _, body = self.object_client.get_object(base_container_name,
+ object_name)
+ self.assertEqual(body, data_2)
# delete object version 2
resp, _ = self.object_client.delete_object(base_container_name,
object_name)
- self.assertContainer(base_container_name, '1', '1',
+ self.assertContainer(base_container_name, '1', '1024',
vers_container_name)
- resp, body = self.object_client.get_object(base_container_name,
- object_name)
- self.assertEqual(body, '1')
+ _, body = self.object_client.get_object(base_container_name,
+ object_name)
+ self.assertEqual(body, data_1)
# delete object version 1
- resp, _ = self.object_client.delete_object(base_container_name,
- object_name)
+ self.object_client.delete_object(base_container_name,
+ object_name)
# containers should be empty
self.assertContainer(base_container_name, '0', '0',
vers_container_name)
diff --git a/tempest/api/orchestration/__init__.py b/tempest/api/orchestration/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/api/orchestration/__init__.py
+++ /dev/null
diff --git a/tempest/api/orchestration/base.py b/tempest/api/orchestration/base.py
deleted file mode 100644
index 4968835..0000000
--- a/tempest/api/orchestration/base.py
+++ /dev/null
@@ -1,184 +0,0 @@
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-import os.path
-
-from oslo_log import log as logging
-from tempest_lib import exceptions as lib_exc
-import yaml
-
-from tempest.common.utils import data_utils
-from tempest import config
-import tempest.test
-
-CONF = config.CONF
-
-LOG = logging.getLogger(__name__)
-
-
-class BaseOrchestrationTest(tempest.test.BaseTestCase):
- """Base test case class for all Orchestration API tests."""
-
- credentials = ['primary']
-
- @classmethod
- def skip_checks(cls):
- super(BaseOrchestrationTest, cls).skip_checks()
- if not CONF.service_available.heat:
- raise cls.skipException("Heat support is required")
-
- @classmethod
- def setup_credentials(cls):
- super(BaseOrchestrationTest, cls).setup_credentials()
- stack_owner_role = CONF.orchestration.stack_owner_role
- cls.os = cls.get_client_manager(roles=[stack_owner_role])
-
- @classmethod
- def setup_clients(cls):
- super(BaseOrchestrationTest, cls).setup_clients()
- cls.orchestration_client = cls.os.orchestration_client
- cls.client = cls.orchestration_client
- cls.servers_client = cls.os.servers_client
- cls.keypairs_client = cls.os.keypairs_client
- cls.network_client = cls.os.network_client
- cls.networks_client = cls.os.networks_client
- cls.volumes_client = cls.os.volumes_client
- cls.images_v2_client = cls.os.image_client_v2
-
- if CONF.volume_feature_enabled.api_v2:
- cls.volumes_client = cls.os.volumes_v2_client
- else:
- cls.volumes_client = cls.os.volumes_client
-
- @classmethod
- def resource_setup(cls):
- super(BaseOrchestrationTest, cls).resource_setup()
- cls.build_timeout = CONF.orchestration.build_timeout
- cls.build_interval = CONF.orchestration.build_interval
- cls.stacks = []
- cls.keypairs = []
- cls.images = []
-
- @classmethod
- def create_stack(cls, stack_name, template_data, parameters=None,
- environment=None, files=None):
- if parameters is None:
- parameters = {}
- body = cls.client.create_stack(
- stack_name,
- template=template_data,
- parameters=parameters,
- environment=environment,
- files=files)
- stack_id = body.response['location'].split('/')[-1]
- stack_identifier = '%s/%s' % (stack_name, stack_id)
- cls.stacks.append(stack_identifier)
- return stack_identifier
-
- @classmethod
- def _clear_stacks(cls):
- for stack_identifier in cls.stacks:
- try:
- cls.client.delete_stack(stack_identifier)
- except lib_exc.NotFound:
- pass
-
- for stack_identifier in cls.stacks:
- try:
- cls.client.wait_for_stack_status(
- stack_identifier, 'DELETE_COMPLETE')
- except lib_exc.NotFound:
- pass
-
- @classmethod
- def _create_keypair(cls, name_start='keypair-heat-'):
- kp_name = data_utils.rand_name(name_start)
- body = cls.keypairs_client.create_keypair(name=kp_name)['keypair']
- cls.keypairs.append(kp_name)
- return body
-
- @classmethod
- def _clear_keypairs(cls):
- for kp_name in cls.keypairs:
- try:
- cls.keypairs_client.delete_keypair(kp_name)
- except Exception:
- pass
-
- @classmethod
- def _create_image(cls, name_start='image-heat-', container_format='bare',
- disk_format='iso'):
- image_name = data_utils.rand_name(name_start)
- body = cls.images_v2_client.create_image(image_name,
- container_format,
- disk_format)
- image_id = body['id']
- cls.images.append(image_id)
- return body
-
- @classmethod
- def _clear_images(cls):
- for image_id in cls.images:
- try:
- cls.images_v2_client.delete_image(image_id)
- except lib_exc.NotFound:
- pass
-
- @classmethod
- def read_template(cls, name, ext='yaml'):
- loc = ["stacks", "templates", "%s.%s" % (name, ext)]
- fullpath = os.path.join(os.path.dirname(__file__), *loc)
-
- with open(fullpath, "r") as f:
- content = f.read()
- return content
-
- @classmethod
- def load_template(cls, name, ext='yaml'):
- loc = ["stacks", "templates", "%s.%s" % (name, ext)]
- fullpath = os.path.join(os.path.dirname(__file__), *loc)
-
- with open(fullpath, "r") as f:
- return yaml.safe_load(f)
-
- @classmethod
- def resource_cleanup(cls):
- cls._clear_stacks()
- cls._clear_keypairs()
- cls._clear_images()
- super(BaseOrchestrationTest, cls).resource_cleanup()
-
- @staticmethod
- def stack_output(stack, output_key):
- """Return a stack output value for a given key."""
- return next((o['output_value'] for o in stack['outputs']
- if o['output_key'] == output_key), None)
-
- def assert_fields_in_dict(self, obj, *fields):
- for field in fields:
- self.assertIn(field, obj)
-
- def list_resources(self, stack_identifier):
- """Get a dict mapping of resource names to types."""
- resources = self.client.list_resources(stack_identifier)['resources']
- self.assertIsInstance(resources, list)
- for res in resources:
- self.assert_fields_in_dict(res, 'logical_resource_id',
- 'resource_type', 'resource_status',
- 'updated_time')
-
- return dict((r['resource_name'], r['resource_type'])
- for r in resources)
-
- def get_stack_output(self, stack_identifier, output_key):
- body = self.client.show_stack(stack_identifier)['stack']
- return self.stack_output(body, output_key)
diff --git a/tempest/api/orchestration/stacks/__init__.py b/tempest/api/orchestration/stacks/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/api/orchestration/stacks/__init__.py
+++ /dev/null
diff --git a/tempest/api/orchestration/stacks/templates/cinder_basic.yaml b/tempest/api/orchestration/stacks/templates/cinder_basic.yaml
deleted file mode 100644
index ffff580..0000000
--- a/tempest/api/orchestration/stacks/templates/cinder_basic.yaml
+++ /dev/null
@@ -1,28 +0,0 @@
-heat_template_version: 2013-05-23
-
-resources:
- volume:
- type: OS::Cinder::Volume
- properties:
- size: 1
- description: a descriptive description
- name: volume_name
-
-outputs:
- status:
- description: status
- value: { get_attr: ['volume', 'status'] }
-
- size:
- description: size
- value: { get_attr: ['volume', 'size'] }
-
- display_description:
- description: display_description
- value: { get_attr: ['volume', 'display_description'] }
-
- display_name:
- value: { get_attr: ['volume', 'display_name'] }
-
- volume_id:
- value: { get_resource: volume }
diff --git a/tempest/api/orchestration/stacks/templates/cinder_basic_delete_retain.yaml b/tempest/api/orchestration/stacks/templates/cinder_basic_delete_retain.yaml
deleted file mode 100644
index b660c19..0000000
--- a/tempest/api/orchestration/stacks/templates/cinder_basic_delete_retain.yaml
+++ /dev/null
@@ -1,29 +0,0 @@
-heat_template_version: 2013-05-23
-
-resources:
- volume:
- deletion_policy: 'Retain'
- type: OS::Cinder::Volume
- properties:
- size: 1
- description: a descriptive description
- name: volume_name
-
-outputs:
- status:
- description: status
- value: { get_attr: ['volume', 'status'] }
-
- size:
- description: size
- value: { get_attr: ['volume', 'size'] }
-
- display_description:
- description: display_description
- value: { get_attr: ['volume', 'display_description'] }
-
- display_name:
- value: { get_attr: ['volume', 'display_name'] }
-
- volume_id:
- value: { get_resource: volume }
diff --git a/tempest/api/orchestration/stacks/templates/neutron_basic.yaml b/tempest/api/orchestration/stacks/templates/neutron_basic.yaml
deleted file mode 100644
index be33c94..0000000
--- a/tempest/api/orchestration/stacks/templates/neutron_basic.yaml
+++ /dev/null
@@ -1,72 +0,0 @@
-heat_template_version: '2013-05-23'
-description: |
- Template which creates single EC2 instance
-parameters:
- KeyName:
- type: string
- InstanceType:
- type: string
- ImageId:
- type: string
- SubNetCidr:
- type: string
- ExternalNetworkId:
- type: string
- DNSServers:
- type: comma_delimited_list
- timeout:
- type: number
-resources:
- Network:
- type: OS::Neutron::Net
- properties:
- name: NewNetwork
- Subnet:
- type: OS::Neutron::Subnet
- properties:
- network_id: {Ref: Network}
- name: NewSubnet
- ip_version: 4
- cidr: { get_param: SubNetCidr }
- dns_nameservers: { get_param: DNSServers }
- Router:
- type: OS::Neutron::Router
- properties:
- name: NewRouter
- admin_state_up: true
- external_gateway_info:
- network: {get_param: ExternalNetworkId}
- RouterInterface:
- type: OS::Neutron::RouterInterface
- properties:
- router_id: {get_resource: Router}
- subnet_id: {get_resource: Subnet}
- Server:
- type: OS::Nova::Server
- metadata:
- Name: SmokeServerNeutron
- properties:
- image: {get_param: ImageId}
- flavor: {get_param: InstanceType}
- key_name: {get_param: KeyName}
- networks:
- - network: {get_resource: Network}
- user_data_format: RAW
- user_data:
- str_replace:
- template: |
- #!/bin/sh -v
-
- SIGNAL_DATA='{"Status": "SUCCESS", "Reason": "SmokeServerNeutron created", "Data": "Application has completed configuration.", "UniqueId": "00000"}'
- while ! curl --fail -X PUT -H 'Content-Type:' --data-binary "$SIGNAL_DATA" \
- 'wait_handle' ; do sleep 3; done
- params:
- wait_handle: {get_resource: WaitHandleNeutron}
- WaitHandleNeutron:
- type: AWS::CloudFormation::WaitConditionHandle
- WaitCondition:
- type: AWS::CloudFormation::WaitCondition
- depends_on: Server
- properties:
- Handle: {get_resource: WaitHandleNeutron}
- Timeout: {get_param: timeout}
diff --git a/tempest/api/orchestration/stacks/templates/non_empty_stack.yaml b/tempest/api/orchestration/stacks/templates/non_empty_stack.yaml
deleted file mode 100644
index 4f9df91..0000000
--- a/tempest/api/orchestration/stacks/templates/non_empty_stack.yaml
+++ /dev/null
@@ -1,36 +0,0 @@
-HeatTemplateFormatVersion: '2012-12-12'
-Description: |
- Template which creates some simple resources
-Parameters:
- trigger:
- Type: String
- Default: not_yet
- image:
- Type: String
- flavor:
- Type: String
-Resources:
- fluffy:
- Type: AWS::AutoScaling::LaunchConfiguration
- Metadata:
- kittens:
- - Tom
- - Stinky
- Properties:
- ImageId: {Ref: image}
- InstanceType: {Ref: flavor}
- UserData:
- Fn::Replace:
- - variable_a: {Ref: trigger}
- variable_b: bee
- - |
- A == variable_a
- B == variable_b
-Outputs:
- fluffy:
- Description: "fluffies irc nick"
- Value:
- Fn::Replace:
- - nick: {Ref: fluffy}
- - |
- #nick
diff --git a/tempest/api/orchestration/stacks/templates/nova_keypair.json b/tempest/api/orchestration/stacks/templates/nova_keypair.json
deleted file mode 100644
index 63d3817..0000000
--- a/tempest/api/orchestration/stacks/templates/nova_keypair.json
+++ /dev/null
@@ -1,48 +0,0 @@
-{
- "AWSTemplateFormatVersion" : "2010-09-09",
- "Description" : "Template which create two key pairs.",
- "Parameters" : {
- "KeyPairName1": {
- "Type": "String",
- "Default": "testkey1"
- },
- "KeyPairName2": {
- "Type": "String",
- "Default": "testkey2"
- }
- },
- "Resources" : {
- "KeyPairSavePrivate": {
- "Type": "OS::Nova::KeyPair",
- "Properties": {
- "name" : { "Ref" : "KeyPairName1" },
- "save_private_key": true
- }
- },
- "KeyPairDontSavePrivate": {
- "Type": "OS::Nova::KeyPair",
- "Properties": {
- "name" : { "Ref" : "KeyPairName2" },
- "save_private_key": false
- }
- }
- },
- "Outputs": {
- "KeyPair_PublicKey": {
- "Description": "Public Key of generated keypair.",
- "Value": { "Fn::GetAtt" : ["KeyPairSavePrivate", "public_key"] }
- },
- "KeyPair_PrivateKey": {
- "Description": "Private Key of generated keypair.",
- "Value": { "Fn::GetAtt" : ["KeyPairSavePrivate", "private_key"] }
- },
- "KeyPairDontSavePrivate_PublicKey": {
- "Description": "Public Key of generated keypair.",
- "Value": { "Fn::GetAtt" : ["KeyPairDontSavePrivate", "public_key"] }
- },
- "KeyPairDontSavePrivate_PrivateKey": {
- "Description": "Private Key of generated keypair.",
- "Value": { "Fn::GetAtt" : ["KeyPairDontSavePrivate", "private_key"] }
- }
- }
-}
diff --git a/tempest/api/orchestration/stacks/templates/nova_keypair.yaml b/tempest/api/orchestration/stacks/templates/nova_keypair.yaml
deleted file mode 100644
index 81ad99c..0000000
--- a/tempest/api/orchestration/stacks/templates/nova_keypair.yaml
+++ /dev/null
@@ -1,43 +0,0 @@
-heat_template_version: 2013-05-23
-
-description: >
- Template which creates two key pairs.
-
-parameters:
- KeyPairName1:
- type: string
- default: testkey
-
- KeyPairName2:
- type: string
- default: testkey2
-
-resources:
- KeyPairSavePrivate:
- type: OS::Nova::KeyPair
- properties:
- name: { get_param: KeyPairName1 }
- save_private_key: true
-
- KeyPairDontSavePrivate:
- type: OS::Nova::KeyPair
- properties:
- name: { get_param: KeyPairName2 }
- save_private_key: false
-
-outputs:
- KeyPair_PublicKey:
- description: Public Key of generated keypair
- value: { get_attr: [KeyPairSavePrivate, public_key] }
-
- KeyPair_PrivateKey:
- description: Private Key of generated keypair
- value: { get_attr: [KeyPairSavePrivate, private_key] }
-
- KeyPairDontSavePrivate_PublicKey:
- description: Public Key of generated keypair
- value: { get_attr: [KeyPairDontSavePrivate, public_key] }
-
- KeyPairDontSavePrivate_PrivateKey:
- description: Private Key of generated keypair
- value: { get_attr: [KeyPairDontSavePrivate, private_key] }
diff --git a/tempest/api/orchestration/stacks/templates/random_string.yaml b/tempest/api/orchestration/stacks/templates/random_string.yaml
deleted file mode 100644
index dfd2342..0000000
--- a/tempest/api/orchestration/stacks/templates/random_string.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
-heat_template_version: 2013-05-23
-
-parameters:
- random_length:
- type: number
- default: 10
-
-resources:
- random:
- type: OS::Heat::RandomString
- properties:
- length: {get_param: random_length}
-
-outputs:
- random_length:
- value: {get_param: random_length}
- random_value:
- value: {get_attr: [random, value]}
diff --git a/tempest/api/orchestration/stacks/templates/swift_basic.yaml b/tempest/api/orchestration/stacks/templates/swift_basic.yaml
deleted file mode 100644
index 713f8bc..0000000
--- a/tempest/api/orchestration/stacks/templates/swift_basic.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-heat_template_version: 2013-05-23
-description: Template which creates a Swift container resource
-
-resources:
- SwiftContainerWebsite:
- deletion_policy: "Delete"
- type: OS::Swift::Container
- properties:
- X-Container-Read: ".r:*"
- X-Container-Meta:
- web-index: "index.html"
- web-error: "error.html"
-
- SwiftContainer:
- type: OS::Swift::Container
-
-outputs:
- WebsiteURL:
- description: "URL for website hosted on S3"
- value: { get_attr: [SwiftContainer, WebsiteURL] }
- DomainName:
- description: "Domain of Swift host"
- value: { get_attr: [SwiftContainer, DomainName] }
diff --git a/tempest/api/orchestration/stacks/test_environment.py b/tempest/api/orchestration/stacks/test_environment.py
deleted file mode 100644
index 0416bc7..0000000
--- a/tempest/api/orchestration/stacks/test_environment.py
+++ /dev/null
@@ -1,99 +0,0 @@
-# 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 logging
-
-from tempest.api.orchestration import base
-from tempest.common.utils import data_utils
-from tempest import config
-from tempest import test
-
-
-CONF = config.CONF
-LOG = logging.getLogger(__name__)
-
-
-class StackEnvironmentTest(base.BaseOrchestrationTest):
-
- @test.idempotent_id('37d4346b-1abd-4442-b7b1-2a4e5749a1e3')
- def test_environment_parameter(self):
- """Test passing a stack parameter via the environment."""
- stack_name = data_utils.rand_name('heat')
- template = self.read_template('random_string')
- environment = {'parameters': {'random_length': 20}}
-
- stack_identifier = self.create_stack(stack_name, template,
- environment=environment)
- self.client.wait_for_stack_status(stack_identifier, 'CREATE_COMPLETE')
-
- random_len = self.get_stack_output(stack_identifier, 'random_length')
- self.assertEqual(20, random_len)
-
- random_value = self.get_stack_output(stack_identifier, 'random_value')
- self.assertEqual(20, len(random_value))
-
- @test.idempotent_id('73bce717-ad22-4853-bbef-6ed89b632701')
- def test_environment_provider_resource(self):
- """Test passing resource_registry defining a provider resource."""
- stack_name = data_utils.rand_name('heat')
- template = '''
-heat_template_version: 2013-05-23
-resources:
- random:
- type: My:Random::String
-outputs:
- random_value:
- value: {get_attr: [random, random_value]}
-'''
- environment = {'resource_registry':
- {'My:Random::String': 'my_random.yaml'}}
- files = {'my_random.yaml': self.read_template('random_string')}
-
- stack_identifier = self.create_stack(stack_name, template,
- environment=environment,
- files=files)
- self.client.wait_for_stack_status(stack_identifier, 'CREATE_COMPLETE')
-
- # random_string.yaml specifies a length of 10
- random_value = self.get_stack_output(stack_identifier, 'random_value')
- random_string_template = self.load_template('random_string')
- expected_length = random_string_template['parameters'][
- 'random_length']['default']
- self.assertEqual(expected_length, len(random_value))
-
- @test.idempotent_id('9d682e5a-f4bb-47d5-8472-9d3cacb855df')
- def test_files_provider_resource(self):
- """Test untyped defining of a provider resource via "files"."""
- # It's also possible to specify the filename directly in the template.
- # without adding the type alias to resource_registry
- stack_name = data_utils.rand_name('heat')
- template = '''
-heat_template_version: 2013-05-23
-resources:
- random:
- type: my_random.yaml
-outputs:
- random_value:
- value: {get_attr: [random, random_value]}
-'''
- files = {'my_random.yaml': self.read_template('random_string')}
-
- stack_identifier = self.create_stack(stack_name, template,
- files=files)
- self.client.wait_for_stack_status(stack_identifier, 'CREATE_COMPLETE')
-
- # random_string.yaml specifies a length of 10
- random_value = self.get_stack_output(stack_identifier, 'random_value')
- random_string_template = self.load_template('random_string')
- expected_length = random_string_template['parameters'][
- 'random_length']['default']
- self.assertEqual(expected_length, len(random_value))
diff --git a/tempest/api/orchestration/stacks/test_limits.py b/tempest/api/orchestration/stacks/test_limits.py
deleted file mode 100644
index bb5b89d..0000000
--- a/tempest/api/orchestration/stacks/test_limits.py
+++ /dev/null
@@ -1,54 +0,0 @@
-# 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 logging
-
-from tempest_lib import exceptions as lib_exc
-
-from tempest.api.orchestration import base
-from tempest.common.utils import data_utils
-from tempest import config
-from tempest import test
-
-CONF = config.CONF
-
-LOG = logging.getLogger(__name__)
-
-
-class TestServerStackLimits(base.BaseOrchestrationTest):
-
- @test.idempotent_id('ec9bed71-c460-45c9-ab98-295caa9fd76b')
- def test_exceed_max_template_size_fails(self):
- stack_name = data_utils.rand_name('heat')
- fill = 'A' * CONF.orchestration.max_template_size
- template = '''
-HeatTemplateFormatVersion: '2012-12-12'
-Description: '%s'
-Outputs:
- Foo: bar''' % fill
- ex = self.assertRaises(lib_exc.BadRequest, self.create_stack,
- stack_name, template)
- self.assertIn('Template exceeds maximum allowed size', str(ex))
-
- @test.idempotent_id('d1b83e73-7cad-4a22-9839-036548c5387c')
- def test_exceed_max_resources_per_stack(self):
- stack_name = data_utils.rand_name('heat')
- # Create a big template, one resource more than the limit
- template = 'heat_template_version: \'2013-05-23\'\nresources:\n'
- rsrc_snippet = ' random%s:\n type: \'OS::Heat::RandomString\'\n'
- num_resources = CONF.orchestration.max_resources_per_stack + 1
- for i in range(num_resources):
- template += rsrc_snippet % i
-
- ex = self.assertRaises(lib_exc.BadRequest, self.create_stack,
- stack_name, template)
- self.assertIn('Maximum resources per stack exceeded', str(ex))
diff --git a/tempest/api/orchestration/stacks/test_neutron_resources.py b/tempest/api/orchestration/stacks/test_neutron_resources.py
deleted file mode 100644
index 8466e11..0000000
--- a/tempest/api/orchestration/stacks/test_neutron_resources.py
+++ /dev/null
@@ -1,195 +0,0 @@
-# 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 logging
-
-import netaddr
-
-from tempest.api.orchestration import base
-from tempest.common import credentials_factory as credentials
-from tempest.common.utils import data_utils
-from tempest import config
-from tempest import exceptions
-from tempest import test
-
-CONF = config.CONF
-
-LOG = logging.getLogger(__name__)
-
-
-class NeutronResourcesTestJSON(base.BaseOrchestrationTest):
-
- @classmethod
- def skip_checks(cls):
- super(NeutronResourcesTestJSON, cls).skip_checks()
- if not CONF.service_available.neutron:
- raise cls.skipException("Neutron support is required")
-
- @classmethod
- def setup_credentials(cls):
- super(NeutronResourcesTestJSON, cls).setup_credentials()
- cls.os = credentials.ConfiguredUserManager()
-
- @classmethod
- def setup_clients(cls):
- super(NeutronResourcesTestJSON, cls).setup_clients()
- cls.network_client = cls.os.network_client
- cls.subnets_client = cls.os.subnets_client
- cls.ports_client = cls.os.ports_client
-
- @classmethod
- def resource_setup(cls):
- super(NeutronResourcesTestJSON, cls).resource_setup()
- cls.neutron_basic_template = cls.load_template('neutron_basic')
- cls.stack_name = data_utils.rand_name('heat')
- template = cls.read_template('neutron_basic')
- cls.keypair_name = (CONF.orchestration.keypair_name or
- cls._create_keypair()['name'])
- cls.external_network_id = CONF.network.public_network_id
-
- tenant_cidr = netaddr.IPNetwork(CONF.network.tenant_network_cidr)
- mask_bits = CONF.network.tenant_network_mask_bits
- cls.subnet_cidr = tenant_cidr.subnet(mask_bits).next()
-
- # create the stack
- cls.stack_identifier = cls.create_stack(
- cls.stack_name,
- template,
- parameters={
- 'KeyName': cls.keypair_name,
- 'InstanceType': CONF.orchestration.instance_type,
- 'ImageId': CONF.compute.image_ref,
- 'ExternalNetworkId': cls.external_network_id,
- 'timeout': CONF.orchestration.build_timeout,
- 'DNSServers': CONF.network.dns_servers,
- 'SubNetCidr': str(cls.subnet_cidr)
- })
- cls.stack_id = cls.stack_identifier.split('/')[1]
- try:
- cls.client.wait_for_stack_status(cls.stack_id, 'CREATE_COMPLETE')
- resources = (cls.client.list_resources(cls.stack_identifier)
- ['resources'])
- except exceptions.TimeoutException as e:
- if CONF.compute_feature_enabled.console_output:
- # attempt to log the server console to help with debugging
- # the cause of the server not signalling the waitcondition
- # to heat.
- body = cls.client.show_resource(cls.stack_identifier,
- 'Server')
- server_id = body['physical_resource_id']
- LOG.debug('Console output for %s', server_id)
- output = cls.servers_client.get_console_output(
- server_id)['output']
- LOG.debug(output)
- raise e
-
- cls.test_resources = {}
- for resource in resources:
- cls.test_resources[resource['logical_resource_id']] = resource
-
- @test.idempotent_id('f9e2664c-bc44-4eef-98b6-495e4f9d74b3')
- def test_created_resources(self):
- """Verifies created neutron resources."""
- resources = [('Network', self.neutron_basic_template['resources'][
- 'Network']['type']),
- ('Subnet', self.neutron_basic_template['resources'][
- 'Subnet']['type']),
- ('RouterInterface', self.neutron_basic_template[
- 'resources']['RouterInterface']['type']),
- ('Server', self.neutron_basic_template['resources'][
- 'Server']['type'])]
- for resource_name, resource_type in resources:
- resource = self.test_resources.get(resource_name, None)
- self.assertIsInstance(resource, dict)
- self.assertEqual(resource_name, resource['logical_resource_id'])
- self.assertEqual(resource_type, resource['resource_type'])
- self.assertEqual('CREATE_COMPLETE', resource['resource_status'])
-
- @test.idempotent_id('c572b915-edb1-4e90-b196-c7199a6848c0')
- @test.services('network')
- def test_created_network(self):
- """Verifies created network."""
- network_id = self.test_resources.get('Network')['physical_resource_id']
- body = self.networks_client.show_network(network_id)
- network = body['network']
- self.assertIsInstance(network, dict)
- self.assertEqual(network_id, network['id'])
- self.assertEqual(self.neutron_basic_template['resources'][
- 'Network']['properties']['name'], network['name'])
-
- @test.idempotent_id('e8f84b96-f9d7-4684-ad5f-340203e9f2c2')
- @test.services('network')
- def test_created_subnet(self):
- """Verifies created subnet."""
- subnet_id = self.test_resources.get('Subnet')['physical_resource_id']
- body = self.subnets_client.show_subnet(subnet_id)
- subnet = body['subnet']
- network_id = self.test_resources.get('Network')['physical_resource_id']
- self.assertEqual(subnet_id, subnet['id'])
- self.assertEqual(network_id, subnet['network_id'])
- self.assertEqual(self.neutron_basic_template['resources'][
- 'Subnet']['properties']['name'], subnet['name'])
- self.assertEqual(sorted(CONF.network.dns_servers),
- sorted(subnet['dns_nameservers']))
- self.assertEqual(self.neutron_basic_template['resources'][
- 'Subnet']['properties']['ip_version'], subnet['ip_version'])
- self.assertEqual(str(self.subnet_cidr), subnet['cidr'])
-
- @test.idempotent_id('96af4c7f-5069-44bc-bdcf-c0390f8a67d1')
- @test.services('network')
- def test_created_router(self):
- """Verifies created router."""
- router_id = self.test_resources.get('Router')['physical_resource_id']
- body = self.network_client.show_router(router_id)
- router = body['router']
- self.assertEqual(self.neutron_basic_template['resources'][
- 'Router']['properties']['name'], router['name'])
- self.assertEqual(self.external_network_id,
- router['external_gateway_info']['network_id'])
- self.assertEqual(True, router['admin_state_up'])
-
- @test.idempotent_id('89f605bd-153e-43ee-a0ed-9919b63423c5')
- @test.services('network')
- def test_created_router_interface(self):
- """Verifies created router interface."""
- router_id = self.test_resources.get('Router')['physical_resource_id']
- network_id = self.test_resources.get('Network')['physical_resource_id']
- subnet_id = self.test_resources.get('Subnet')['physical_resource_id']
- body = self.ports_client.list_ports()
- ports = body['ports']
- router_ports = filter(lambda port: port['device_id'] ==
- router_id, ports)
- created_network_ports = filter(lambda port: port['network_id'] ==
- network_id, router_ports)
- self.assertEqual(1, len(created_network_ports))
- router_interface = created_network_ports[0]
- fixed_ips = router_interface['fixed_ips']
- subnet_fixed_ips = filter(lambda port: port['subnet_id'] ==
- subnet_id, fixed_ips)
- self.assertEqual(1, len(subnet_fixed_ips))
- router_interface_ip = subnet_fixed_ips[0]['ip_address']
- self.assertEqual(str(self.subnet_cidr.iter_hosts().next()),
- router_interface_ip)
-
- @test.idempotent_id('75d85316-4ac2-4c0e-a1a9-edd2148fc10e')
- @test.services('compute', 'network')
- def test_created_server(self):
- """Verifies created sever."""
- server_id = self.test_resources.get('Server')['physical_resource_id']
- server = self.servers_client.show_server(server_id)['server']
- self.assertEqual(self.keypair_name, server['key_name'])
- self.assertEqual('ACTIVE', server['status'])
- network = server['addresses'][self.neutron_basic_template['resources'][
- 'Network']['properties']['name']][0]
- self.assertEqual(4, network['version'])
- self.assertIn(netaddr.IPAddress(network['addr']), self.subnet_cidr)
diff --git a/tempest/api/orchestration/stacks/test_non_empty_stack.py b/tempest/api/orchestration/stacks/test_non_empty_stack.py
deleted file mode 100644
index 4bc2c17..0000000
--- a/tempest/api/orchestration/stacks/test_non_empty_stack.py
+++ /dev/null
@@ -1,154 +0,0 @@
-# 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 logging
-
-from tempest.api.orchestration import base
-from tempest.common.utils import data_utils
-from tempest import config
-from tempest import test
-
-CONF = config.CONF
-
-LOG = logging.getLogger(__name__)
-
-
-class StacksTestJSON(base.BaseOrchestrationTest):
-
- @classmethod
- def resource_setup(cls):
- super(StacksTestJSON, cls).resource_setup()
- cls.stack_name = data_utils.rand_name('heat')
- template = cls.read_template('non_empty_stack')
- image_id = (CONF.compute.image_ref or
- cls._create_image()['id'])
- flavor = CONF.orchestration.instance_type
- # create the stack
- cls.stack_identifier = cls.create_stack(
- cls.stack_name,
- template,
- parameters={
- 'trigger': 'start',
- 'image': image_id,
- 'flavor': flavor
- })
- cls.stack_id = cls.stack_identifier.split('/')[1]
- cls.resource_name = 'fluffy'
- cls.resource_type = 'AWS::AutoScaling::LaunchConfiguration'
- cls.client.wait_for_stack_status(cls.stack_id, 'CREATE_COMPLETE')
-
- def _list_stacks(self, expected_num=None, **filter_kwargs):
- stacks = self.client.list_stacks(params=filter_kwargs)['stacks']
- self.assertIsInstance(stacks, list)
- if expected_num is not None:
- self.assertEqual(expected_num, len(stacks))
- return stacks
-
- @test.idempotent_id('065c652a-720d-4760-9132-06aedeb8e3ab')
- def test_stack_list(self):
- """Created stack should be in the list of existing stacks."""
- stacks = self._list_stacks()
- stacks_names = map(lambda stack: stack['stack_name'], stacks)
- self.assertIn(self.stack_name, stacks_names)
-
- @test.idempotent_id('992f96e3-41ee-4ff6-91c7-bcfb670c0919')
- def test_stack_show(self):
- """Getting details about created stack should be possible."""
- stack = self.client.show_stack(self.stack_name)['stack']
- self.assertIsInstance(stack, dict)
- self.assert_fields_in_dict(stack, 'stack_name', 'id', 'links',
- 'parameters', 'outputs', 'disable_rollback',
- 'stack_status_reason', 'stack_status',
- 'creation_time', 'updated_time',
- 'capabilities', 'notification_topics',
- 'timeout_mins', 'template_description')
- self.assert_fields_in_dict(stack['parameters'], 'AWS::StackId',
- 'trigger', 'AWS::Region', 'AWS::StackName')
- self.assertEqual(True, stack['disable_rollback'],
- 'disable_rollback should default to True')
- self.assertEqual(self.stack_name, stack['stack_name'])
- self.assertEqual(self.stack_id, stack['id'])
- self.assertEqual('fluffy', stack['outputs'][0]['output_key'])
-
- @test.idempotent_id('fe719f7a-305a-44d8-bbb5-c91e93d9da17')
- def test_suspend_resume_stack(self):
- """Suspend and resume a stack."""
- self.client.suspend_stack(self.stack_identifier)
- self.client.wait_for_stack_status(self.stack_identifier,
- 'SUSPEND_COMPLETE')
- self.client.resume_stack(self.stack_identifier)
- self.client.wait_for_stack_status(self.stack_identifier,
- 'RESUME_COMPLETE')
-
- @test.idempotent_id('c951d55e-7cce-4c1f-83a0-bad735437fa6')
- def test_list_resources(self):
- """Get list of created resources for the stack should be possible."""
- resources = self.list_resources(self.stack_identifier)
- self.assertEqual({self.resource_name: self.resource_type}, resources)
-
- @test.idempotent_id('2aba03b3-392f-4237-900b-1f5a5e9bd962')
- def test_show_resource(self):
- """Getting details about created resource should be possible."""
- resource = self.client.show_resource(self.stack_identifier,
- self.resource_name)['resource']
- self.assertIsInstance(resource, dict)
- self.assert_fields_in_dict(resource, 'resource_name', 'description',
- 'links', 'logical_resource_id',
- 'resource_status', 'updated_time',
- 'required_by', 'resource_status_reason',
- 'physical_resource_id', 'resource_type')
- self.assertEqual(self.resource_name, resource['logical_resource_id'])
- self.assertEqual(self.resource_type, resource['resource_type'])
-
- @test.idempotent_id('898070a9-eba5-4fae-b7d6-cf3ffa03090f')
- def test_resource_metadata(self):
- """Getting metadata for created resources should be possible."""
- metadata = self.client.show_resource_metadata(
- self.stack_identifier,
- self.resource_name)['metadata']
- self.assertIsInstance(metadata, dict)
- self.assertEqual(['Tom', 'Stinky'], metadata.get('kittens', None))
-
- @test.idempotent_id('46567533-0a7f-483b-8942-fa19e0f17839')
- def test_list_events(self):
- """Getting list of created events for the stack should be possible."""
- events = self.client.list_events(self.stack_identifier)['events']
- self.assertIsInstance(events, list)
-
- for event in events:
- self.assert_fields_in_dict(event, 'logical_resource_id', 'id',
- 'resource_status_reason',
- 'resource_status', 'event_time')
-
- resource_statuses = map(lambda event: event['resource_status'], events)
- self.assertIn('CREATE_IN_PROGRESS', resource_statuses)
- self.assertIn('CREATE_COMPLETE', resource_statuses)
-
- @test.idempotent_id('92465723-1673-400a-909d-4773757a3f21')
- def test_show_event(self):
- """Getting details about an event should be possible."""
- events = self.client.list_resource_events(self.stack_identifier,
- self.resource_name)['events']
- self.assertNotEqual([], events)
- events.sort(key=lambda event: event['event_time'])
- event_id = events[0]['id']
- event = self.client.show_event(self.stack_identifier,
- self.resource_name, event_id)['event']
- self.assertIsInstance(event, dict)
- self.assert_fields_in_dict(event, 'resource_name', 'event_time',
- 'links', 'logical_resource_id',
- 'resource_status', 'resource_status_reason',
- 'physical_resource_id', 'id',
- 'resource_properties', 'resource_type')
- self.assertEqual(self.resource_name, event['resource_name'])
- self.assertEqual('state changed', event['resource_status_reason'])
- self.assertEqual(self.resource_name, event['logical_resource_id'])
diff --git a/tempest/api/orchestration/stacks/test_nova_keypair_resources.py b/tempest/api/orchestration/stacks/test_nova_keypair_resources.py
deleted file mode 100644
index b4d7fa0..0000000
--- a/tempest/api/orchestration/stacks/test_nova_keypair_resources.py
+++ /dev/null
@@ -1,98 +0,0 @@
-# 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 logging
-
-from tempest.api.orchestration import base
-from tempest.common.utils import data_utils
-from tempest import test
-
-
-LOG = logging.getLogger(__name__)
-
-
-class NovaKeyPairResourcesYAMLTest(base.BaseOrchestrationTest):
- _tpl_type = 'yaml'
- _resource = 'resources'
- _type = 'type'
-
- @classmethod
- def resource_setup(cls):
- super(NovaKeyPairResourcesYAMLTest, cls).resource_setup()
- cls.stack_name = data_utils.rand_name('heat')
- template = cls.read_template('nova_keypair', ext=cls._tpl_type)
-
- # create the stack, avoid any duplicated key.
- cls.stack_identifier = cls.create_stack(
- cls.stack_name,
- template,
- parameters={
- 'KeyPairName1': cls.stack_name + '_1',
- 'KeyPairName2': cls.stack_name + '_2'
- })
-
- cls.stack_id = cls.stack_identifier.split('/')[1]
- cls.client.wait_for_stack_status(cls.stack_id, 'CREATE_COMPLETE')
- resources = (cls.client.list_resources(cls.stack_identifier)
- ['resources'])
- cls.test_resources = {}
- for resource in resources:
- cls.test_resources[resource['logical_resource_id']] = resource
-
- @test.idempotent_id('b476eac2-a302-4815-961f-18c410a2a537')
- def test_created_resources(self):
- """Verifies created keypair resource."""
-
- nova_keypair_template = self.load_template('nova_keypair',
- ext=self._tpl_type)
- resources = [('KeyPairSavePrivate',
- nova_keypair_template[self._resource][
- 'KeyPairSavePrivate'][self._type]),
- ('KeyPairDontSavePrivate',
- nova_keypair_template[self._resource][
- 'KeyPairDontSavePrivate'][self._type])]
-
- for resource_name, resource_type in resources:
- resource = self.test_resources.get(resource_name, None)
- self.assertIsInstance(resource, dict)
- self.assertEqual(resource_name, resource['logical_resource_id'])
- self.assertEqual(resource_type, resource['resource_type'])
- self.assertEqual('CREATE_COMPLETE', resource['resource_status'])
-
- @test.idempotent_id('8d77dec7-91fd-45a6-943d-5abd45e338a4')
- def test_stack_keypairs_output(self):
- stack = self.client.show_stack(self.stack_name)['stack']
- self.assertIsInstance(stack, dict)
-
- output_map = {}
- for outputs in stack['outputs']:
- output_map[outputs['output_key']] = outputs['output_value']
- # Test that first key generated public and private keys
- self.assertTrue('KeyPair_PublicKey' in output_map)
- self.assertTrue("Generated" in output_map['KeyPair_PublicKey'])
- self.assertTrue('KeyPair_PrivateKey' in output_map)
- self.assertTrue('-----BEGIN' in output_map['KeyPair_PrivateKey'])
- # Test that second key generated public key, and private key is not
- # in the output due to save_private_key = false
- self.assertTrue('KeyPairDontSavePrivate_PublicKey' in output_map)
- self.assertTrue('Generated' in
- output_map['KeyPairDontSavePrivate_PublicKey'])
- self.assertTrue(u'KeyPairDontSavePrivate_PrivateKey' in output_map)
- private_key = output_map['KeyPairDontSavePrivate_PrivateKey']
- self.assertTrue(len(private_key) == 0)
-
-
-class NovaKeyPairResourcesAWSTest(NovaKeyPairResourcesYAMLTest):
- _tpl_type = 'json'
- _resource = 'Resources'
- _type = 'Type'
diff --git a/tempest/api/orchestration/stacks/test_resource_types.py b/tempest/api/orchestration/stacks/test_resource_types.py
deleted file mode 100644
index 8cf40de..0000000
--- a/tempest/api/orchestration/stacks/test_resource_types.py
+++ /dev/null
@@ -1,49 +0,0 @@
-# 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.orchestration import base
-from tempest import test
-
-
-class ResourceTypesTest(base.BaseOrchestrationTest):
-
- @test.attr(type='smoke')
- @test.idempotent_id('7123d082-3577-4a30-8f00-f805327c4ffd')
- def test_resource_type_list(self):
- """Verify it is possible to list resource types."""
- resource_types = self.client.list_resource_types()['resource_types']
- self.assertIsInstance(resource_types, list)
- self.assertIn('OS::Nova::Server', resource_types)
-
- @test.attr(type='smoke')
- @test.idempotent_id('0e85a483-828b-4a28-a0e3-f0a21809192b')
- def test_resource_type_show(self):
- """Verify it is possible to get schema about resource types."""
- resource_types = self.client.list_resource_types()['resource_types']
- self.assertNotEmpty(resource_types)
-
- for resource_type in resource_types:
- type_schema = self.client.show_resource_type(resource_type)
- self.assert_fields_in_dict(type_schema, 'properties',
- 'attributes', 'resource_type')
- self.assertEqual(resource_type, type_schema['resource_type'])
-
- @test.attr(type='smoke')
- @test.idempotent_id('8401821d-65fe-4d43-9fa3-57d5ce3a35c7')
- def test_resource_type_template(self):
- """Verify it is possible to get template about resource types."""
- type_template = self.client.show_resource_type_template(
- 'OS::Nova::Server')
- self.assert_fields_in_dict(
- type_template,
- 'Outputs',
- 'Parameters', 'Resources')
diff --git a/tempest/api/orchestration/stacks/test_soft_conf.py b/tempest/api/orchestration/stacks/test_soft_conf.py
deleted file mode 100644
index 34d93e4..0000000
--- a/tempest/api/orchestration/stacks/test_soft_conf.py
+++ /dev/null
@@ -1,175 +0,0 @@
-# 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_log import log as logging
-from tempest_lib import exceptions as lib_exc
-
-from tempest.api.orchestration import base
-from tempest.common.utils import data_utils
-from tempest import config
-from tempest import test
-
-LOG = logging.getLogger(__name__)
-CONF = config.CONF
-
-
-class TestSoftwareConfig(base.BaseOrchestrationTest):
-
- def setUp(self):
- super(TestSoftwareConfig, self).setUp()
- self.configs = []
- # Add 2 sets of software configuration
- self.configs.append(self._config_create('a'))
- self.configs.append(self._config_create('b'))
- # Create a deployment using config a's id
- self._deployment_create(self.configs[0]['id'])
-
- def _config_create(self, suffix):
- configuration = {'group': 'script',
- 'inputs': [],
- 'outputs': [],
- 'options': {}}
- configuration['name'] = 'heat_soft_config_%s' % suffix
- configuration['config'] = '#!/bin/bash echo init-%s' % suffix
- api_config = self.client.create_software_config(**configuration)
- configuration['id'] = api_config['software_config']['id']
- self.addCleanup(self._config_delete, configuration['id'])
- self._validate_config(configuration, api_config)
- return configuration
-
- def _validate_config(self, configuration, api_config):
- # Assert all expected keys are present with matching data
- for k in configuration.keys():
- self.assertEqual(configuration[k],
- api_config['software_config'][k])
-
- def _deployment_create(self, config_id):
- self.server_id = data_utils.rand_name('dummy-server')
- self.action = 'ACTION_0'
- self.status = 'STATUS_0'
- self.input_values = {}
- self.output_values = []
- self.status_reason = 'REASON_0'
- self.signal_transport = 'NO_SIGNAL'
- self.deployment = self.client.create_software_deploy(
- self.server_id, config_id, self.action, self.status,
- self.input_values, self.output_values, self.status_reason,
- self.signal_transport)
- self.deployment_id = self.deployment['software_deployment']['id']
- self.addCleanup(self._deployment_delete, self.deployment_id)
-
- def _deployment_delete(self, deploy_id):
- self.client.delete_software_deploy(deploy_id)
- # Testing that it is really gone
- self.assertRaises(
- lib_exc.NotFound, self.client.show_software_deployment,
- self.deployment_id)
-
- def _config_delete(self, config_id):
- self.client.delete_software_config(config_id)
- # Testing that it is really gone
- self.assertRaises(
- lib_exc.NotFound, self.client.show_software_config, config_id)
-
- @test.attr(type='smoke')
- @test.idempotent_id('136162ed-9445-4b9c-b7fc-306af8b5da99')
- def test_get_software_config(self):
- """Testing software config get."""
- for conf in self.configs:
- api_config = self.client.show_software_config(conf['id'])
- self._validate_config(conf, api_config)
-
- @test.attr(type='smoke')
- @test.idempotent_id('1275c835-c967-4a2c-8d5d-ad533447ed91')
- def test_get_deployment_list(self):
- """Getting a list of all deployments"""
- deploy_list = self.client.list_software_deployments()
- deploy_ids = [deploy['id'] for deploy in
- deploy_list['software_deployments']]
- self.assertIn(self.deployment_id, deploy_ids)
-
- @test.attr(type='smoke')
- @test.idempotent_id('fe7cd9f9-54b1-429c-a3b7-7df8451db913')
- def test_get_deployment_metadata(self):
- """Testing deployment metadata get"""
- metadata = self.client.show_software_deployment_metadata(
- self.server_id)
- conf_ids = [conf['id'] for conf in metadata['metadata']]
- self.assertIn(self.configs[0]['id'], conf_ids)
-
- def _validate_deployment(self, action, status, reason, config_id):
- deployment = self.client.show_software_deployment(self.deployment_id)
- self.assertEqual(action, deployment['software_deployment']['action'])
- self.assertEqual(status, deployment['software_deployment']['status'])
- self.assertEqual(reason,
- deployment['software_deployment']['status_reason'])
- self.assertEqual(config_id,
- deployment['software_deployment']['config_id'])
-
- @test.attr(type='smoke')
- @test.idempotent_id('f29d21f3-ed75-47cf-8cdc-ef1bdeb4c674')
- def test_software_deployment_create_validate(self):
- """Testing software deployment was created as expected."""
- # Asserting that all fields were created
- self.assert_fields_in_dict(
- self.deployment['software_deployment'], 'action', 'config_id',
- 'id', 'input_values', 'output_values', 'server_id', 'status',
- 'status_reason')
- # Testing get for this deployment and verifying parameters
- self._validate_deployment(self.action, self.status,
- self.status_reason, self.configs[0]['id'])
-
- @test.attr(type='smoke')
- @test.idempotent_id('2ac43ab3-34f2-415d-be2e-eabb4d14ee32')
- def test_software_deployment_update_no_metadata_change(self):
- """Testing software deployment update without metadata change."""
- metadata = self.client.show_software_deployment_metadata(
- self.server_id)
- # Updating values without changing the configuration ID
- new_action = 'ACTION_1'
- new_status = 'STATUS_1'
- new_reason = 'REASON_1'
- self.client.update_software_deploy(
- self.deployment_id, self.server_id, self.configs[0]['id'],
- new_action, new_status, self.input_values, self.output_values,
- new_reason, self.signal_transport)
- # Verifying get and that the deployment was updated as expected
- self._validate_deployment(new_action, new_status,
- new_reason, self.configs[0]['id'])
-
- # Metadata should not be changed at this point
- test_metadata = self.client.show_software_deployment_metadata(
- self.server_id)
- for key in metadata['metadata'][0]:
- self.assertEqual(
- metadata['metadata'][0][key],
- test_metadata['metadata'][0][key])
-
- @test.attr(type='smoke')
- @test.idempotent_id('92c48944-d79d-4595-a840-8e1a581c1a72')
- def test_software_deployment_update_with_metadata_change(self):
- """Testing software deployment update with metadata change."""
- metadata = self.client.show_software_deployment_metadata(
- self.server_id)
- self.client.update_software_deploy(
- self.deployment_id, self.server_id, self.configs[1]['id'],
- self.action, self.status, self.input_values,
- self.output_values, self.status_reason, self.signal_transport)
- self._validate_deployment(self.action, self.status,
- self.status_reason, self.configs[1]['id'])
- # Metadata should now be changed
- new_metadata = self.client.show_software_deployment_metadata(
- self.server_id)
- # Its enough to test the ID in this case
- meta_id = metadata['metadata'][0]['id']
- test_id = new_metadata['metadata'][0]['id']
- self.assertNotEqual(meta_id, test_id)
diff --git a/tempest/api/orchestration/stacks/test_stacks.py b/tempest/api/orchestration/stacks/test_stacks.py
deleted file mode 100644
index f766b00..0000000
--- a/tempest/api/orchestration/stacks/test_stacks.py
+++ /dev/null
@@ -1,68 +0,0 @@
-# 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_log import log as logging
-
-from tempest.api.orchestration import base
-from tempest.common.utils import data_utils
-from tempest import test
-
-
-LOG = logging.getLogger(__name__)
-
-
-class StacksTestJSON(base.BaseOrchestrationTest):
- empty_template = "HeatTemplateFormatVersion: '2012-12-12'\n"
-
- @classmethod
- def resource_setup(cls):
- super(StacksTestJSON, cls).resource_setup()
-
- @test.attr(type='smoke')
- @test.idempotent_id('d35d628c-07f6-4674-85a1-74db9919e986')
- def test_stack_list_responds(self):
- stacks = self.client.list_stacks()['stacks']
- self.assertIsInstance(stacks, list)
-
- @test.attr(type='smoke')
- @test.idempotent_id('10498bd5-a83e-4b62-a817-ce24afe938fe')
- def test_stack_crud_no_resources(self):
- stack_name = data_utils.rand_name('heat')
-
- # create the stack
- stack_identifier = self.create_stack(
- stack_name, self.empty_template)
- stack_id = stack_identifier.split('/')[1]
-
- # wait for create complete (with no resources it should be instant)
- self.client.wait_for_stack_status(stack_identifier, 'CREATE_COMPLETE')
-
- # check for stack in list
- stacks = self.client.list_stacks()['stacks']
- list_ids = list([stack['id'] for stack in stacks])
- self.assertIn(stack_id, list_ids)
-
- # fetch the stack
- stack = self.client.show_stack(stack_identifier)['stack']
- self.assertEqual('CREATE_COMPLETE', stack['stack_status'])
-
- # fetch the stack by name
- stack = self.client.show_stack(stack_name)['stack']
- self.assertEqual('CREATE_COMPLETE', stack['stack_status'])
-
- # fetch the stack by id
- stack = self.client.show_stack(stack_id)['stack']
- self.assertEqual('CREATE_COMPLETE', stack['stack_status'])
-
- # delete the stack
- self.client.delete_stack(stack_identifier)
- self.client.wait_for_stack_status(stack_identifier, 'DELETE_COMPLETE')
diff --git a/tempest/api/orchestration/stacks/test_swift_resources.py b/tempest/api/orchestration/stacks/test_swift_resources.py
deleted file mode 100644
index c0f1c4b..0000000
--- a/tempest/api/orchestration/stacks/test_swift_resources.py
+++ /dev/null
@@ -1,120 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (C) 2013 eNovance SAS <licensing@enovance.com>
-#
-# 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.orchestration import base
-from tempest.common.utils import data_utils
-from tempest import config
-from tempest import test
-
-
-CONF = config.CONF
-
-
-class SwiftResourcesTestJSON(base.BaseOrchestrationTest):
- @classmethod
- def skip_checks(cls):
- super(SwiftResourcesTestJSON, cls).skip_checks()
- if not CONF.service_available.swift:
- raise cls.skipException("Swift support is required")
-
- @classmethod
- def setup_credentials(cls):
- super(SwiftResourcesTestJSON, cls).setup_credentials()
- stack_owner_role = CONF.orchestration.stack_owner_role
- operator_role = CONF.object_storage.operator_role
- cls.os = cls.get_client_manager(
- roles=[stack_owner_role, operator_role])
-
- @classmethod
- def setup_clients(cls):
- super(SwiftResourcesTestJSON, cls).setup_clients()
- cls.account_client = cls.os.account_client
- cls.container_client = cls.os.container_client
-
- @classmethod
- def resource_setup(cls):
- super(SwiftResourcesTestJSON, cls).resource_setup()
- cls.stack_name = data_utils.rand_name('heat')
- template = cls.read_template('swift_basic')
- # create the stack
- cls.stack_identifier = cls.create_stack(
- cls.stack_name,
- template)
- cls.stack_id = cls.stack_identifier.split('/')[1]
- cls.client.wait_for_stack_status(cls.stack_id, 'CREATE_COMPLETE')
- cls.test_resources = {}
- resources = (cls.client.list_resources(cls.stack_identifier)
- ['resources'])
- for resource in resources:
- cls.test_resources[resource['logical_resource_id']] = resource
-
- @test.idempotent_id('1a6fe69e-4be4-4990-9a7a-84b6f18019cb')
- def test_created_resources(self):
- """Created stack should be in the list of existing stacks."""
- swift_basic_template = self.load_template('swift_basic')
- resources = [('SwiftContainer', swift_basic_template['resources'][
- 'SwiftContainer']['type']),
- ('SwiftContainerWebsite', swift_basic_template[
- 'resources']['SwiftContainerWebsite']['type'])]
- for resource_name, resource_type in resources:
- resource = self.test_resources.get(resource_name)
- self.assertIsInstance(resource, dict)
- self.assertEqual(resource_type, resource['resource_type'])
- self.assertEqual(resource_name, resource['logical_resource_id'])
- self.assertEqual('CREATE_COMPLETE', resource['resource_status'])
-
- @test.idempotent_id('bd438b18-5494-4d5a-9ce6-d2a942ec5060')
- @test.services('object_storage')
- def test_created_containers(self):
- params = {'format': 'json'}
- _, container_list = \
- self.account_client.list_account_containers(params=params)
- created_containers = [cont for cont in container_list
- if cont['name'].startswith(self.stack_name)]
- self.assertEqual(2, len(created_containers))
-
- @test.idempotent_id('73d0c093-9922-44a0-8b1d-1fc092dee367')
- @test.services('object_storage')
- def test_acl(self):
- acl_headers = ('x-container-meta-web-index', 'x-container-read')
-
- swcont = self.test_resources.get(
- 'SwiftContainer')['physical_resource_id']
- swcont_website = self.test_resources.get(
- 'SwiftContainerWebsite')['physical_resource_id']
-
- headers, _ = self.container_client.list_container_metadata(swcont)
- for h in acl_headers:
- self.assertNotIn(h, headers)
- headers, _ = self.container_client.list_container_metadata(
- swcont_website)
- for h in acl_headers:
- self.assertIn(h, headers)
-
- @test.idempotent_id('fda06135-6777-4594-aefa-0f6107169698')
- @test.services('object_storage')
- def test_metadata(self):
- swift_basic_template = self.load_template('swift_basic')
- metadatas = swift_basic_template['resources']['SwiftContainerWebsite'][
- 'properties']['X-Container-Meta']
- swcont_website = self.test_resources.get(
- 'SwiftContainerWebsite')['physical_resource_id']
- headers, _ = self.container_client.list_container_metadata(
- swcont_website)
-
- for meta in metadatas:
- header_meta = "x-container-meta-%s" % meta
- self.assertIn(header_meta, headers)
- self.assertEqual(headers[header_meta], metadatas[meta])
diff --git a/tempest/api/orchestration/stacks/test_templates.py b/tempest/api/orchestration/stacks/test_templates.py
deleted file mode 100644
index 9154175..0000000
--- a/tempest/api/orchestration/stacks/test_templates.py
+++ /dev/null
@@ -1,61 +0,0 @@
-# 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.orchestration import base
-from tempest.common.utils import data_utils
-from tempest import test
-
-
-class TemplateYAMLTestJSON(base.BaseOrchestrationTest):
- template = """
-HeatTemplateFormatVersion: '2012-12-12'
-Description: |
- Template which creates only a new user
-Resources:
- CfnUser:
- Type: AWS::IAM::User
-"""
-
- @classmethod
- def resource_setup(cls):
- super(TemplateYAMLTestJSON, cls).resource_setup()
- cls.stack_name = data_utils.rand_name('heat')
- cls.stack_identifier = cls.create_stack(cls.stack_name, cls.template)
- cls.client.wait_for_stack_status(cls.stack_identifier,
- 'CREATE_COMPLETE')
- cls.stack_id = cls.stack_identifier.split('/')[1]
- cls.parameters = {}
-
- @test.idempotent_id('47430699-c368-495e-a1db-64c26fd967d7')
- def test_show_template(self):
- """Getting template used to create the stack."""
- self.client.show_template(self.stack_identifier)
-
- @test.idempotent_id('ed53debe-8727-46c5-ab58-eba6090ec4de')
- def test_validate_template(self):
- """Validating template passing it content."""
- self.client.validate_template(self.template,
- self.parameters)
-
-
-class TemplateAWSTestJSON(TemplateYAMLTestJSON):
- template = """
-{
- "AWSTemplateFormatVersion" : "2010-09-09",
- "Description" : "Template which creates only a new user",
- "Resources" : {
- "CfnUser" : {
- "Type" : "AWS::IAM::User"
- }
- }
-}
-"""
diff --git a/tempest/api/orchestration/stacks/test_templates_negative.py b/tempest/api/orchestration/stacks/test_templates_negative.py
deleted file mode 100644
index 4bd0f33..0000000
--- a/tempest/api/orchestration/stacks/test_templates_negative.py
+++ /dev/null
@@ -1,61 +0,0 @@
-# Copyright 2014 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 import exceptions as lib_exc
-
-from tempest.api.orchestration import base
-from tempest import test
-
-
-class TemplateYAMLNegativeTestJSON(base.BaseOrchestrationTest):
- template = """
-HeatTemplateFormatVersion: '2012-12-12'
-Description: |
- Template which creates only a new user
-Resources:
- CfnUser:
- Type: AWS::IAM::User
-"""
-
- invalid_template_url = 'http://www.example.com/template.yaml'
-
- @classmethod
- def resource_setup(cls):
- super(TemplateYAMLNegativeTestJSON, cls).resource_setup()
- cls.parameters = {}
-
- @test.attr(type=['negative'])
- @test.idempotent_id('5586cbca-ddc4-4152-9db8-fa1ce5fc1876')
- def test_validate_template_url(self):
- """Validating template passing url to it."""
- self.assertRaises(lib_exc.BadRequest,
- self.client.validate_template_url,
- template_url=self.invalid_template_url,
- parameters=self.parameters)
-
-
-class TemplateAWSNegativeTestJSON(TemplateYAMLNegativeTestJSON):
- template = """
-{
- "AWSTemplateFormatVersion" : "2010-09-09",
- "Description" : "Template which creates only a new user",
- "Resources" : {
- "CfnUser" : {
- "Type" : "AWS::IAM::User"
- }
- }
-}
-"""
-
- invalid_template_url = 'http://www.example.com/template.template'
diff --git a/tempest/api/orchestration/stacks/test_volumes.py b/tempest/api/orchestration/stacks/test_volumes.py
deleted file mode 100644
index ae9a411..0000000
--- a/tempest/api/orchestration/stacks/test_volumes.py
+++ /dev/null
@@ -1,118 +0,0 @@
-# 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 logging
-
-from tempest_lib import exceptions as lib_exc
-
-from tempest.api.orchestration import base
-from tempest.common.utils import data_utils
-from tempest import config
-from tempest import test
-
-
-CONF = config.CONF
-LOG = logging.getLogger(__name__)
-
-
-class CinderResourcesTest(base.BaseOrchestrationTest):
-
- @classmethod
- def skip_checks(cls):
- super(CinderResourcesTest, cls).skip_checks()
- if not CONF.service_available.cinder:
- raise cls.skipException('Cinder support is required')
-
- def _cinder_verify(self, volume_id, template):
- self.assertIsNotNone(volume_id)
- volume = self.volumes_client.show_volume(volume_id)['volume']
- self.assertEqual('available', volume.get('status'))
- self.assertEqual(template['resources']['volume']['properties'][
- 'size'], volume.get('size'))
-
- # Some volume properties have been renamed with Cinder v2
- if CONF.volume_feature_enabled.api_v2:
- description_field = 'description'
- name_field = 'name'
- else:
- description_field = 'display_description'
- name_field = 'display_name'
-
- self.assertEqual(template['resources']['volume']['properties'][
- 'description'], volume.get(description_field))
- self.assertEqual(template['resources']['volume']['properties'][
- 'name'], volume.get(name_field))
-
- def _outputs_verify(self, stack_identifier, template):
- self.assertEqual('available',
- self.get_stack_output(stack_identifier, 'status'))
- self.assertEqual(str(template['resources']['volume']['properties'][
- 'size']), self.get_stack_output(stack_identifier, 'size'))
- self.assertEqual(template['resources']['volume']['properties'][
- 'description'], self.get_stack_output(stack_identifier,
- 'display_description'))
- self.assertEqual(template['resources']['volume']['properties'][
- 'name'], self.get_stack_output(stack_identifier, 'display_name'))
-
- @test.idempotent_id('c3243329-7bdd-4730-b402-4d19d50c41d8')
- @test.services('volume')
- def test_cinder_volume_create_delete(self):
- """Create and delete a volume via OS::Cinder::Volume."""
- stack_name = data_utils.rand_name('heat')
- template = self.read_template('cinder_basic')
- stack_identifier = self.create_stack(stack_name, template)
- self.client.wait_for_stack_status(stack_identifier, 'CREATE_COMPLETE')
-
- # Verify with cinder that the volume exists, with matching details
- volume_id = self.get_stack_output(stack_identifier, 'volume_id')
- cinder_basic_template = self.load_template('cinder_basic')
- self._cinder_verify(volume_id, cinder_basic_template)
-
- # Verify the stack outputs are as expected
- self._outputs_verify(stack_identifier, cinder_basic_template)
-
- # Delete the stack and ensure the volume is gone
- self.client.delete_stack(stack_identifier)
- self.client.wait_for_stack_status(stack_identifier, 'DELETE_COMPLETE')
- self.assertRaises(lib_exc.NotFound,
- self.volumes_client.show_volume,
- volume_id)
-
- def _cleanup_volume(self, volume_id):
- """Cleanup the volume direct with cinder."""
- self.volumes_client.delete_volume(volume_id)
- self.volumes_client.wait_for_resource_deletion(volume_id)
-
- @test.idempotent_id('ea8b3a46-b932-4c18-907a-fe23f00b33f8')
- @test.services('volume')
- def test_cinder_volume_create_delete_retain(self):
- """Ensure the 'Retain' deletion policy is respected."""
- stack_name = data_utils.rand_name('heat')
- template = self.read_template('cinder_basic_delete_retain')
- stack_identifier = self.create_stack(stack_name, template)
- self.client.wait_for_stack_status(stack_identifier, 'CREATE_COMPLETE')
-
- # Verify with cinder that the volume exists, with matching details
- volume_id = self.get_stack_output(stack_identifier, 'volume_id')
- self.addCleanup(self._cleanup_volume, volume_id)
- retain_template = self.load_template('cinder_basic_delete_retain')
- self._cinder_verify(volume_id, retain_template)
-
- # Verify the stack outputs are as expected
- self._outputs_verify(stack_identifier, retain_template)
-
- # Delete the stack and ensure the volume is *not* gone
- self.client.delete_stack(stack_identifier)
- self.client.wait_for_stack_status(stack_identifier, 'DELETE_COMPLETE')
- self._cinder_verify(volume_id, retain_template)
-
- # Volume cleanup happens via addCleanup calling _cleanup_volume
diff --git a/tempest/api/telemetry/__init__.py b/tempest/api/telemetry/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/api/telemetry/__init__.py
+++ /dev/null
diff --git a/tempest/api/telemetry/base.py b/tempest/api/telemetry/base.py
deleted file mode 100644
index bbf6db2..0000000
--- a/tempest/api/telemetry/base.py
+++ /dev/null
@@ -1,189 +0,0 @@
-# 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 time
-
-from oslo_utils import timeutils
-from tempest_lib import exceptions as lib_exc
-
-from tempest.common import compute
-from tempest.common.utils import data_utils
-from tempest import config
-from tempest import exceptions
-import tempest.test
-
-CONF = config.CONF
-
-
-class BaseTelemetryTest(tempest.test.BaseTestCase):
-
- """Base test case class for all Telemetry API tests."""
-
- credentials = ['primary']
-
- @classmethod
- def skip_checks(cls):
- super(BaseTelemetryTest, cls).skip_checks()
- if not CONF.service_available.ceilometer:
- raise cls.skipException("Ceilometer support is required")
-
- @classmethod
- def setup_credentials(cls):
- cls.set_network_resources()
- super(BaseTelemetryTest, cls).setup_credentials()
-
- @classmethod
- def setup_clients(cls):
- super(BaseTelemetryTest, cls).setup_clients()
- cls.telemetry_client = cls.os.telemetry_client
- cls.servers_client = cls.os.servers_client
- cls.flavors_client = cls.os.flavors_client
- cls.image_client = cls.os.image_client
- cls.image_client_v2 = cls.os.image_client_v2
-
- @classmethod
- def resource_setup(cls):
- super(BaseTelemetryTest, cls).resource_setup()
- cls.nova_notifications = ['memory', 'vcpus', 'disk.root.size',
- 'disk.ephemeral.size']
-
- cls.glance_notifications = ['image.size']
-
- cls.glance_v2_notifications = ['image.download', 'image.serve']
-
- cls.server_ids = []
- cls.image_ids = []
-
- @classmethod
- def create_server(cls):
- tenant_network = cls.get_tenant_network()
- body, server = compute.create_test_server(
- cls.os,
- tenant_network=tenant_network,
- name=data_utils.rand_name('ceilometer-instance'),
- wait_until='ACTIVE')
- cls.server_ids.append(body['id'])
- return body
-
- @classmethod
- def create_image(cls, client, **kwargs):
- body = client.create_image(name=data_utils.rand_name('image'),
- container_format='bare',
- disk_format='raw',
- **kwargs)
- # TODO(jswarren) Move ['image'] up to initial body value assignment
- # once both v1 and v2 glance clients include the full response
- # object.
- if 'image' in body:
- body = body['image']
- cls.image_ids.append(body['id'])
- return body
-
- @staticmethod
- def cleanup_resources(method, list_of_ids):
- for resource_id in list_of_ids:
- try:
- method(resource_id)
- except lib_exc.NotFound:
- pass
-
- @classmethod
- def resource_cleanup(cls):
- cls.cleanup_resources(cls.servers_client.delete_server, cls.server_ids)
- cls.cleanup_resources(cls.image_client.delete_image, cls.image_ids)
- super(BaseTelemetryTest, cls).resource_cleanup()
-
- def await_samples(self, metric, query):
- """This method is to wait for sample to add it to database.
-
- There are long time delays when using Postgresql (or Mysql)
- database as ceilometer backend
- """
- timeout = CONF.compute.build_timeout
- start = timeutils.utcnow()
- while timeutils.delta_seconds(start, timeutils.utcnow()) < timeout:
- body = self.telemetry_client.list_samples(metric, query)
- if body:
- return body
- time.sleep(CONF.compute.build_interval)
-
- raise exceptions.TimeoutException(
- 'Sample for metric:%s with query:%s has not been added to the '
- 'database within %d seconds' % (metric, query,
- CONF.compute.build_timeout))
-
-
-class BaseTelemetryAdminTest(BaseTelemetryTest):
- """Base test case class for admin Telemetry API tests."""
-
- credentials = ['primary', 'admin']
-
- @classmethod
- def setup_clients(cls):
- super(BaseTelemetryAdminTest, cls).setup_clients()
- cls.telemetry_admin_client = cls.os_adm.telemetry_client
-
- def await_events(self, query):
- timeout = CONF.compute.build_timeout
- start = timeutils.utcnow()
- while timeutils.delta_seconds(start, timeutils.utcnow()) < timeout:
- body = self.telemetry_admin_client.list_events(query)
- if body:
- return body
- time.sleep(CONF.compute.build_interval)
-
- raise exceptions.TimeoutException(
- 'Event with query:%s has not been added to the '
- 'database within %d seconds' % (query, CONF.compute.build_timeout))
-
-
-class BaseAlarmingTest(tempest.test.BaseTestCase):
- """Base test case class for all Alarming API tests."""
-
- credentials = ['primary']
-
- @classmethod
- def skip_checks(cls):
- super(BaseAlarmingTest, cls).skip_checks()
- if not CONF.service_available.aodh:
- raise cls.skipException("Aodh support is required")
-
- @classmethod
- def setup_clients(cls):
- super(BaseAlarmingTest, cls).setup_clients()
- cls.alarming_client = cls.os.alarming_client
-
- @classmethod
- def resource_setup(cls):
- super(BaseAlarmingTest, cls).resource_setup()
- cls.alarm_ids = []
-
- @classmethod
- def create_alarm(cls, **kwargs):
- body = cls.alarming_client.create_alarm(
- name=data_utils.rand_name('telemetry_alarm'),
- type='threshold', **kwargs)
- cls.alarm_ids.append(body['alarm_id'])
- return body
-
- @staticmethod
- def cleanup_resources(method, list_of_ids):
- for resource_id in list_of_ids:
- try:
- method(resource_id)
- except lib_exc.NotFound:
- pass
-
- @classmethod
- def resource_cleanup(cls):
- cls.cleanup_resources(cls.alarming_client.delete_alarm, cls.alarm_ids)
- super(BaseAlarmingTest, cls).resource_cleanup()
diff --git a/tempest/api/telemetry/test_alarming_api.py b/tempest/api/telemetry/test_alarming_api.py
deleted file mode 100644
index daa0939..0000000
--- a/tempest/api/telemetry/test_alarming_api.py
+++ /dev/null
@@ -1,111 +0,0 @@
-# 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 import exceptions as lib_exc
-
-from tempest.api.telemetry import base
-from tempest.common.utils import data_utils
-from tempest import test
-
-
-class TelemetryAlarmingAPITestJSON(base.BaseAlarmingTest):
-
- @classmethod
- def resource_setup(cls):
- super(TelemetryAlarmingAPITestJSON, cls).resource_setup()
- cls.rule = {'meter_name': 'cpu_util',
- 'comparison_operator': 'gt',
- 'threshold': 80.0,
- 'period': 70}
- for i in range(2):
- cls.create_alarm(threshold_rule=cls.rule)
-
- @test.idempotent_id('1c918e06-210b-41eb-bd45-14676dd77cd6')
- def test_alarm_list(self):
- # List alarms
- alarm_list = self.alarming_client.list_alarms()
-
- # Verify created alarm in the list
- fetched_ids = [a['alarm_id'] for a in alarm_list]
- missing_alarms = [a for a in self.alarm_ids if a not in fetched_ids]
- self.assertEqual(0, len(missing_alarms),
- "Failed to find the following created alarm(s)"
- " in a fetched list: %s" %
- ', '.join(str(a) for a in missing_alarms))
-
- @test.idempotent_id('1297b095-39c1-4e74-8a1f-4ae998cedd67')
- def test_create_update_get_delete_alarm(self):
- # Create an alarm
- alarm_name = data_utils.rand_name('telemetry_alarm')
- body = self.alarming_client.create_alarm(
- name=alarm_name, type='threshold', threshold_rule=self.rule)
- self.assertEqual(alarm_name, body['name'])
- alarm_id = body['alarm_id']
- self.assertDictContainsSubset(self.rule, body['threshold_rule'])
- # Update alarm with new rule and new name
- new_rule = {'meter_name': 'cpu',
- 'comparison_operator': 'eq',
- 'threshold': 70.0,
- 'period': 60}
- alarm_name_updated = data_utils.rand_name('telemetry-alarm-update')
- body = self.alarming_client.update_alarm(
- alarm_id,
- threshold_rule=new_rule,
- name=alarm_name_updated,
- type='threshold')
- self.assertEqual(alarm_name_updated, body['name'])
- self.assertDictContainsSubset(new_rule, body['threshold_rule'])
- # Get and verify details of an alarm after update
- body = self.alarming_client.show_alarm(alarm_id)
- self.assertEqual(alarm_name_updated, body['name'])
- self.assertDictContainsSubset(new_rule, body['threshold_rule'])
- # Get history for the alarm and verify the same
- body = self.alarming_client.show_alarm_history(alarm_id)
- self.assertEqual("rule change", body[0]['type'])
- self.assertIn(alarm_name_updated, body[0]['detail'])
- self.assertEqual("creation", body[1]['type'])
- self.assertIn(alarm_name, body[1]['detail'])
- # Delete alarm and verify if deleted
- self.alarming_client.delete_alarm(alarm_id)
- self.assertRaises(lib_exc.NotFound,
- self.alarming_client.show_alarm, alarm_id)
-
- @test.idempotent_id('aca49486-70bb-4016-87e0-f6131374f741')
- def test_set_get_alarm_state(self):
- alarm_states = ['ok', 'alarm', 'insufficient data']
- alarm = self.create_alarm(threshold_rule=self.rule)
- # Set alarm state and verify
- new_state =\
- [elem for elem in alarm_states if elem != alarm['state']][0]
- state = self.alarming_client.alarm_set_state(alarm['alarm_id'],
- new_state)
- self.assertEqual(new_state, state.data)
- # Get alarm state and verify
- state = self.alarming_client.show_alarm_state(alarm['alarm_id'])
- self.assertEqual(new_state, state.data)
-
- @test.idempotent_id('08d7e45a-1344-4e5c-ba6f-f6cbb77f55b9')
- def test_create_delete_alarm_with_combination_rule(self):
- rule = {"alarm_ids": self.alarm_ids,
- "operator": "or"}
- # Verifies alarm create
- alarm_name = data_utils.rand_name('combination_alarm')
- body = self.alarming_client.create_alarm(name=alarm_name,
- combination_rule=rule,
- type='combination')
- self.assertEqual(alarm_name, body['name'])
- alarm_id = body['alarm_id']
- self.assertDictContainsSubset(rule, body['combination_rule'])
- # Verify alarm delete
- self.alarming_client.delete_alarm(alarm_id)
- self.assertRaises(lib_exc.NotFound,
- self.alarming_client.show_alarm, alarm_id)
diff --git a/tempest/api/telemetry/test_alarming_api_negative.py b/tempest/api/telemetry/test_alarming_api_negative.py
deleted file mode 100644
index e945556..0000000
--- a/tempest/api/telemetry/test_alarming_api_negative.py
+++ /dev/null
@@ -1,71 +0,0 @@
-# Copyright 2015 GlobalLogic. 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.telemetry import base
-from tempest.common.utils import data_utils
-from tempest import test
-from tempest_lib import exceptions as lib_exc
-
-import uuid
-
-
-class TelemetryAlarmingNegativeTest(base.BaseAlarmingTest):
- """Negative tests for show_alarm, update_alarm, show_alarm_history tests
-
- ** show non-existent alarm
- ** show the deleted alarm
- ** delete deleted alarm
- ** update deleted alarm
- """
-
- @test.attr(type=['negative'])
- @test.idempotent_id('668743d5-08ad-4480-b2b8-15da34f81e7d')
- def test_get_non_existent_alarm(self):
- # get the non-existent alarm
- non_existent_id = str(uuid.uuid4())
- self.assertRaises(lib_exc.NotFound, self.alarming_client.show_alarm,
- non_existent_id)
-
- @test.attr(type=['negative'])
- @test.idempotent_id('ef45000d-0a72-4781-866d-4cb7bf2582ad')
- def test_get_update_show_history_delete_deleted_alarm(self):
- # get, update and delete the deleted alarm
- alarm_name = data_utils.rand_name('telemetry_alarm')
- rule = {'meter_name': 'cpu',
- 'comparison_operator': 'eq',
- 'threshold': 100.0,
- 'period': 90}
- body = self.alarming_client.create_alarm(
- name=alarm_name,
- type='threshold',
- threshold_rule=rule)
- alarm_id = body['alarm_id']
- self.alarming_client.delete_alarm(alarm_id)
- # get the deleted alarm
- self.assertRaises(lib_exc.NotFound, self.alarming_client.show_alarm,
- alarm_id)
-
- # update the deleted alarm
- updated_alarm_name = data_utils.rand_name('telemetry_alarm_updated')
- updated_rule = {'meter_name': 'cpu_new',
- 'comparison_operator': 'eq',
- 'threshold': 70,
- 'period': 50}
- self.assertRaises(lib_exc.NotFound, self.alarming_client.update_alarm,
- alarm_id, threshold_rule=updated_rule,
- name=updated_alarm_name,
- type='threshold')
- # delete the deleted alarm
- self.assertRaises(lib_exc.NotFound, self.alarming_client.delete_alarm,
- alarm_id)
diff --git a/tempest/api/telemetry/test_telemetry_notification_api.py b/tempest/api/telemetry/test_telemetry_notification_api.py
deleted file mode 100644
index a575125..0000000
--- a/tempest/api/telemetry/test_telemetry_notification_api.py
+++ /dev/null
@@ -1,84 +0,0 @@
-# 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 import decorators
-import testtools
-
-from tempest.api.telemetry import base
-from tempest import config
-from tempest import test
-
-CONF = config.CONF
-
-
-class TelemetryNotificationAPITestJSON(base.BaseTelemetryTest):
-
- @test.idempotent_id('d7f8c1c8-d470-4731-8604-315d3956caad')
- @test.services('compute')
- def test_check_nova_notification(self):
-
- body = self.create_server()
-
- query = ('resource', 'eq', body['id'])
-
- for metric in self.nova_notifications:
- self.await_samples(metric, query)
-
- @test.attr(type="smoke")
- @test.idempotent_id('04b10bfe-a5dc-47af-b22f-0460426bf498')
- @test.services("image")
- @testtools.skipIf(not CONF.image_feature_enabled.api_v1,
- "Glance api v1 is disabled")
- def test_check_glance_v1_notifications(self):
- body = self.create_image(self.image_client, is_public=False)
- self.image_client.update_image(body['id'], data='data')
-
- query = 'resource', 'eq', body['id']
-
- self.image_client.delete_image(body['id'])
-
- for metric in self.glance_notifications:
- self.await_samples(metric, query)
-
- @test.attr(type="smoke")
- @test.idempotent_id('c240457d-d943-439b-8aea-85e26d64fe8e')
- @test.services("image")
- @testtools.skipIf(not CONF.image_feature_enabled.api_v2,
- "Glance api v2 is disabled")
- def test_check_glance_v2_notifications(self):
- body = self.create_image(self.image_client_v2, visibility='private')
-
- self.image_client_v2.store_image_file(body['id'], "file")
- self.image_client_v2.show_image_file(body['id'])
-
- query = 'resource', 'eq', body['id']
-
- for metric in self.glance_v2_notifications:
- self.await_samples(metric, query)
-
-
-class TelemetryNotificationAdminAPITestJSON(base.BaseTelemetryAdminTest):
-
- @test.idempotent_id('29604198-8b45-4fc0-8af8-1cae4f94ebe9')
- @test.services('compute')
- @decorators.skip_because(bug='1480490')
- def test_check_nova_notification_event_and_meter(self):
-
- body = self.create_server()
-
- if CONF.telemetry_feature_enabled.events:
- query = ('instance_id', 'eq', body['id'])
- self.await_events(query)
-
- query = ('resource', 'eq', body['id'])
- for metric in self.nova_notifications:
- self.await_samples(metric, query)
diff --git a/tempest/api/utils.py b/tempest/api/utils.py
deleted file mode 100644
index 00c93b7..0000000
--- a/tempest/api/utils.py
+++ /dev/null
@@ -1,37 +0,0 @@
-# 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.
-
-"""Common utilities used in testing."""
-
-from tempest import test
-
-
-class skip_unless_attr(object):
- """Decorator that skips a test if a specified attr exists and is True."""
- def __init__(self, attr, msg=None):
- self.attr = attr
- self.message = msg or ("Test case attribute %s not found "
- "or False") % attr
-
- def __call__(self, func):
- def _skipper(*args, **kw):
- """Wrapped skipper function."""
- testobj = args[0]
- if not getattr(testobj, self.attr, False):
- raise test.BaseTestCase.skipException(self.message)
- func(*args, **kw)
- _skipper.__name__ = func.__name__
- _skipper.__doc__ = func.__doc__
- return _skipper
diff --git a/tempest/api/volume/admin/test_backends_capabilities.py b/tempest/api/volume/admin/test_backends_capabilities.py
new file mode 100644
index 0000000..607fc43
--- /dev/null
+++ b/tempest/api/volume/admin/test_backends_capabilities.py
@@ -0,0 +1,79 @@
+# Copyright 2016 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import operator
+
+from tempest.api.volume import base
+from tempest.lib import decorators
+
+
+class BackendsCapabilitiesAdminTestsJSON(base.BaseVolumeAdminTest):
+
+ CAPABILITIES = ('namespace',
+ 'vendor_name',
+ 'volume_backend_name',
+ 'pool_name',
+ 'driver_version',
+ 'storage_protocol',
+ 'display_name',
+ 'description',
+ 'visibility',
+ 'properties')
+
+ @classmethod
+ def resource_setup(cls):
+ super(BackendsCapabilitiesAdminTestsJSON, cls).resource_setup()
+ # Get host list, formation: host@backend-name
+ cls.hosts = [
+ pool['name'] for pool in
+ cls.admin_scheduler_stats_client.list_pools()['pools']
+ ]
+
+ @decorators.idempotent_id('3750af44-5ea2-4cd4-bc3e-56e7e6caf854')
+ def test_get_capabilities_backend(self):
+ # Test backend properties
+ backend = self.admin_capabilities_client.show_backend_capabilities(
+ self.hosts[0])
+
+ # Verify getting capabilities parameters from a backend
+ for key in self.CAPABILITIES:
+ self.assertIn(key, backend)
+
+ @decorators.idempotent_id('a9035743-d46a-47c5-9cb7-3c80ea16dea0')
+ def test_compare_volume_stats_values(self):
+ # Test values comparison between show_backend_capabilities
+ # to show_pools
+ VOLUME_STATS = ('vendor_name',
+ 'volume_backend_name',
+ 'storage_protocol')
+
+ # Get list backend capabilities using show_pools
+ cinder_pools = [
+ pool['capabilities'] for pool in
+ self.admin_scheduler_stats_client.list_pools(detail=True)['pools']
+ ]
+
+ # Get list backends capabilities using show_backend_capabilities
+ capabilities = [
+ self.admin_capabilities_client.show_backend_capabilities(
+ host=host) for host in self.hosts
+ ]
+
+ # Returns a tuple of VOLUME_STATS values
+ expected_list = list(map(operator.itemgetter(*VOLUME_STATS),
+ cinder_pools))
+ observed_list = list(map(operator.itemgetter(*VOLUME_STATS),
+ capabilities))
+ self.assertEqual(expected_list, observed_list)
diff --git a/tempest/api/volume/admin/test_group_snapshots.py b/tempest/api/volume/admin/test_group_snapshots.py
new file mode 100644
index 0000000..731a055
--- /dev/null
+++ b/tempest/api/volume/admin/test_group_snapshots.py
@@ -0,0 +1,253 @@
+# Copyright 2017 FiberHome Telecommunication Technologies CO.,LTD
+# Copyright (C) 2017 Dell Inc. or its subsidiaries.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from tempest.api.volume import base
+from tempest.common import waiters
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+
+CONF = config.CONF
+
+
+class BaseGroupSnapshotsTest(base.BaseVolumeAdminTest):
+
+ @classmethod
+ def skip_checks(cls):
+ super(BaseGroupSnapshotsTest, cls).skip_checks()
+ if not CONF.volume_feature_enabled.snapshot:
+ raise cls.skipException("Cinder volume snapshots are disabled")
+
+ def _create_group_snapshot(self, **kwargs):
+ if 'name' not in kwargs:
+ kwargs['name'] = data_utils.rand_name(
+ self.__class__.__name__ + '-Group_Snapshot')
+
+ group_snapshot = self.group_snapshots_client.create_group_snapshot(
+ **kwargs)['group_snapshot']
+ group_snapshot['group_id'] = kwargs['group_id']
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self._delete_group_snapshot, group_snapshot)
+ waiters.wait_for_volume_resource_status(
+ self.group_snapshots_client, group_snapshot['id'], 'available')
+ return group_snapshot
+
+ def _delete_group_snapshot(self, group_snapshot):
+ self.group_snapshots_client.delete_group_snapshot(group_snapshot['id'])
+ vols = self.volumes_client.list_volumes(detail=True)['volumes']
+ snapshots = self.snapshots_client.list_snapshots(
+ detail=True)['snapshots']
+ for vol in vols:
+ for snap in snapshots:
+ if (vol['group_id'] == group_snapshot['group_id'] and
+ vol['id'] == snap['volume_id']):
+ self.snapshots_client.wait_for_resource_deletion(
+ snap['id'])
+ self.group_snapshots_client.wait_for_resource_deletion(
+ group_snapshot['id'])
+
+
+class GroupSnapshotsTest(BaseGroupSnapshotsTest):
+ _api_version = 3
+ min_microversion = '3.14'
+ max_microversion = 'latest'
+
+ @decorators.idempotent_id('1298e537-f1f0-47a3-a1dd-8adec8168897')
+ def test_group_snapshot_create_show_list_delete(self):
+ # Create volume type
+ volume_type = self.create_volume_type()
+
+ # Create group type
+ group_type = self.create_group_type()
+
+ # Create group
+ grp = self.create_group(group_type=group_type['id'],
+ volume_types=[volume_type['id']])
+
+ # Create volume
+ vol = self.create_volume(volume_type=volume_type['id'],
+ group_id=grp['id'])
+
+ # Create group snapshot
+ group_snapshot_name = data_utils.rand_name('group_snapshot')
+ group_snapshot = self._create_group_snapshot(
+ group_id=grp['id'], name=group_snapshot_name)
+ snapshots = self.snapshots_client.list_snapshots(
+ detail=True)['snapshots']
+ for snap in snapshots:
+ if vol['id'] == snap['volume_id']:
+ waiters.wait_for_volume_resource_status(
+ self.snapshots_client, snap['id'], 'available')
+ self.assertEqual(group_snapshot_name, group_snapshot['name'])
+
+ # Get a given group snapshot
+ group_snapshot = self.group_snapshots_client.show_group_snapshot(
+ group_snapshot['id'])['group_snapshot']
+ self.assertEqual(group_snapshot_name, group_snapshot['name'])
+
+ # Get all group snapshots with details, check some detail-specific
+ # elements, and look for the created group snapshot
+ group_snapshots = self.group_snapshots_client.list_group_snapshots(
+ detail=True)['group_snapshots']
+ for grp_snapshot in group_snapshots:
+ self.assertIn('created_at', grp_snapshot)
+ self.assertIn('group_id', grp_snapshot)
+ self.assertIn((group_snapshot['name'], group_snapshot['id']),
+ [(m['name'], m['id']) for m in group_snapshots])
+
+ # Delete group snapshot
+ self._delete_group_snapshot(group_snapshot)
+ group_snapshots = self.group_snapshots_client.list_group_snapshots()[
+ 'group_snapshots']
+ self.assertEmpty(group_snapshots)
+
+ @decorators.idempotent_id('eff52c70-efc7-45ed-b47a-4ad675d09b81')
+ def test_create_group_from_group_snapshot(self):
+ # Create volume type
+ volume_type = self.create_volume_type()
+
+ # Create group type
+ group_type = self.create_group_type()
+
+ # Create Group
+ grp = self.create_group(group_type=group_type['id'],
+ volume_types=[volume_type['id']])
+
+ # Create volume
+ vol = self.create_volume(volume_type=volume_type['id'],
+ group_id=grp['id'])
+
+ # Create group_snapshot
+ group_snapshot_name = data_utils.rand_name('group_snapshot')
+ group_snapshot = self._create_group_snapshot(
+ group_id=grp['id'], name=group_snapshot_name)
+ self.assertEqual(group_snapshot_name, group_snapshot['name'])
+ snapshots = self.snapshots_client.list_snapshots(
+ detail=True)['snapshots']
+ for snap in snapshots:
+ if vol['id'] == snap['volume_id']:
+ waiters.wait_for_volume_resource_status(
+ self.snapshots_client, snap['id'], 'available')
+
+ # Create Group from Group snapshot
+ grp_name2 = data_utils.rand_name('Group_from_snap')
+ grp2 = self.groups_client.create_group_from_source(
+ group_snapshot_id=group_snapshot['id'], name=grp_name2)['group']
+ self.addCleanup(self.delete_group, grp2['id'])
+ self.assertEqual(grp_name2, grp2['name'])
+ vols = self.volumes_client.list_volumes(detail=True)['volumes']
+ for vol in vols:
+ if vol['group_id'] == grp2['id']:
+ waiters.wait_for_volume_resource_status(
+ self.volumes_client, vol['id'], 'available')
+ waiters.wait_for_volume_resource_status(
+ self.groups_client, grp2['id'], 'available')
+
+ @decorators.idempotent_id('7d7fc000-0b4c-4376-a372-544116d2e127')
+ @decorators.related_bug('1739031')
+ def test_delete_group_snapshots_following_updated_volumes(self):
+ volume_type = self.create_volume_type()
+
+ group_type = self.create_group_type()
+
+ # Create a volume group
+ grp = self.create_group(group_type=group_type['id'],
+ volume_types=[volume_type['id']])
+
+ # Note: When dealing with consistency groups all volumes must
+ # reside on the same backend. Adding volumes to the same consistency
+ # group from multiple backends isn't supported. In order to ensure all
+ # volumes share the same backend, all volumes must share same
+ # volume-type and group id.
+ volume_list = []
+ for _ in range(2):
+ volume = self.create_volume(volume_type=volume_type['id'],
+ group_id=grp['id'])
+ volume_list.append(volume['id'])
+
+ for vol in volume_list:
+ self.groups_client.update_group(grp['id'],
+ remove_volumes=vol)
+ waiters.wait_for_volume_resource_status(
+ self.groups_client, grp['id'], 'available')
+
+ self.groups_client.update_group(grp['id'],
+ add_volumes=vol)
+ waiters.wait_for_volume_resource_status(
+ self.groups_client, grp['id'], 'available')
+
+ # Verify the created volumes are associated with consistency group
+ vols = self.volumes_client.list_volumes(detail=True)['volumes']
+ grp_vols = [v for v in vols if v['group_id'] == grp['id']]
+ self.assertEqual(2, len(grp_vols))
+
+ # Create a snapshot group
+ group_snapshot = self._create_group_snapshot(group_id=grp['id'])
+ snapshots = self.snapshots_client.list_snapshots(
+ detail=True)['snapshots']
+
+ for snap in snapshots:
+ if snap['volume_id'] in volume_list:
+ waiters.wait_for_volume_resource_status(
+ self.snapshots_client, snap['id'], 'available')
+
+ # Delete a snapshot group
+ self._delete_group_snapshot(group_snapshot)
+
+
+class GroupSnapshotsV319Test(BaseGroupSnapshotsTest):
+ _api_version = 3
+ min_microversion = '3.19'
+ max_microversion = 'latest'
+
+ @decorators.idempotent_id('3b42c9b9-c984-4444-816e-ca2e1ed30b40')
+ def test_reset_group_snapshot_status(self):
+ # Create volume type
+ volume_type = self.create_volume_type()
+
+ # Create group type
+ group_type = self.create_group_type()
+
+ # Create group
+ group = self.create_group(group_type=group_type['id'],
+ volume_types=[volume_type['id']])
+
+ # Create volume
+ volume = self.create_volume(volume_type=volume_type['id'],
+ group_id=group['id'])
+
+ # Create group snapshot
+ group_snapshot = self._create_group_snapshot(group_id=group['id'])
+ snapshots = self.snapshots_client.list_snapshots(
+ detail=True)['snapshots']
+ for snap in snapshots:
+ if volume['id'] == snap['volume_id']:
+ waiters.wait_for_volume_resource_status(
+ self.snapshots_client, snap['id'], 'available')
+
+ # Reset group snapshot status
+ self.addCleanup(waiters.wait_for_volume_resource_status,
+ self.group_snapshots_client,
+ group_snapshot['id'], 'available')
+ self.addCleanup(
+ self.admin_group_snapshots_client.reset_group_snapshot_status,
+ group_snapshot['id'], 'available')
+ for status in ['creating', 'available', 'error']:
+ self.admin_group_snapshots_client.reset_group_snapshot_status(
+ group_snapshot['id'], status)
+ waiters.wait_for_volume_resource_status(
+ self.group_snapshots_client, group_snapshot['id'], status)
diff --git a/tempest/api/volume/admin/test_group_type_specs.py b/tempest/api/volume/admin/test_group_type_specs.py
new file mode 100644
index 0000000..c5e6d1a
--- /dev/null
+++ b/tempest/api/volume/admin/test_group_type_specs.py
@@ -0,0 +1,80 @@
+# 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 tempest.api.volume import base
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+
+class GroupTypeSpecsTest(base.BaseVolumeAdminTest):
+ _api_version = 3
+ min_microversion = '3.11'
+ max_microversion = 'latest'
+
+ @decorators.idempotent_id('bb4e30d0-de6e-4f4d-866c-dcc48d023b4e')
+ def test_group_type_specs_create_show_update_list_delete(self):
+ # Create new group type
+ group_type = self.create_group_type()
+
+ # Create new group type specs
+ create_specs = {
+ "key1": "value1",
+ "key2": "value2"
+ }
+ body = self.admin_group_types_client.create_or_update_group_type_specs(
+ group_type['id'], create_specs)['group_specs']
+ self.assertEqual(create_specs, body)
+
+ # Create a new group type spec and update an existing group type spec
+ update_specs = {
+ "key2": "value2-updated",
+ "key3": "value3"
+ }
+ body = self.admin_group_types_client.create_or_update_group_type_specs(
+ group_type['id'], update_specs)['group_specs']
+ self.assertEqual(update_specs, body)
+
+ # Show specified item of group type specs
+ spec_keys = ['key2', 'key3']
+ for key in spec_keys:
+ body = self.admin_group_types_client.show_group_type_specs_item(
+ group_type['id'], key)
+ self.assertIn(key, body)
+ self.assertEqual(update_specs[key], body[key])
+
+ # Update specified item of group type specs
+ update_key = 'key3'
+ update_spec = {update_key: "value3-updated"}
+ body = self.admin_group_types_client.update_group_type_specs_item(
+ group_type['id'], update_key, update_spec)
+ self.assertEqual(update_spec, body)
+
+ # List all group type specs that created or updated above
+ list_specs = {}
+ list_specs.update(create_specs)
+ list_specs.update(update_specs)
+ list_specs.update(update_spec)
+ body = self.admin_group_types_client.list_group_type_specs(
+ group_type['id'])['group_specs']
+ self.assertEqual(list_specs, body)
+
+ # Delete specified item of group type specs
+ delete_key = 'key1'
+ self.admin_group_types_client.delete_group_type_specs_item(
+ group_type['id'], delete_key)
+ self.assertRaises(
+ lib_exc.NotFound,
+ self.admin_group_types_client.show_group_type_specs_item,
+ group_type['id'], delete_key)
diff --git a/tempest/api/volume/admin/test_group_types.py b/tempest/api/volume/admin/test_group_types.py
new file mode 100644
index 0000000..6723207
--- /dev/null
+++ b/tempest/api/volume/admin/test_group_types.py
@@ -0,0 +1,65 @@
+# Copyright (C) 2017 Dell Inc. or its subsidiaries.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from tempest.api.volume import base
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+
+
+class GroupTypesTest(base.BaseVolumeAdminTest):
+ _api_version = 3
+ min_microversion = '3.11'
+ max_microversion = 'latest'
+
+ @decorators.idempotent_id('dd71e5f9-393e-4d4f-90e9-fa1b8d278864')
+ def test_group_type_create_list_update_show(self):
+ # Create/list/show group type.
+ name = data_utils.rand_name(self.__class__.__name__ + '-group-type')
+ description = data_utils.rand_name("group-type-description")
+ group_specs = {"consistent_group_snapshot_enabled": "<is> False"}
+ params = {'name': name,
+ 'description': description,
+ 'group_specs': group_specs,
+ 'is_public': True}
+ body = self.create_group_type(**params)
+ self.assertIn('name', body)
+ err_msg = ("The created group_type %(var)s is not equal to the "
+ "requested %(var)s")
+ self.assertEqual(name, body['name'], err_msg % {"var": "name"})
+ self.assertEqual(description, body['description'],
+ err_msg % {"var": "description"})
+
+ group_list = (
+ self.admin_group_types_client.list_group_types()['group_types'])
+ self.assertIsInstance(group_list, list)
+ self.assertNotEmpty(group_list)
+
+ update_params = {
+ 'name': data_utils.rand_name(
+ self.__class__.__name__ + '-updated-group-type'),
+ 'description': 'updated-group-type-desc'
+ }
+ updated_group_type = self.admin_group_types_client.update_group_type(
+ body['id'], **update_params)['group_type']
+ for key, expected_val in update_params.items():
+ self.assertEqual(expected_val, updated_group_type[key])
+
+ fetched_group_type = self.admin_group_types_client.show_group_type(
+ body['id'])['group_type']
+ params.update(update_params) # Add updated params to original params.
+ for key in params.keys():
+ self.assertEqual(params[key], fetched_group_type[key],
+ '%s of the fetched group_type is different '
+ 'from the created group_type' % key)
diff --git a/tempest/api/volume/admin/test_groups.py b/tempest/api/volume/admin/test_groups.py
new file mode 100644
index 0000000..2f6eb6b
--- /dev/null
+++ b/tempest/api/volume/admin/test_groups.py
@@ -0,0 +1,212 @@
+# Copyright (C) 2017 Dell Inc. or its subsidiaries.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from tempest.api.volume import base
+from tempest.common import waiters
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+
+CONF = config.CONF
+
+
+class GroupsTest(base.BaseVolumeAdminTest):
+ _api_version = 3
+ min_microversion = '3.13'
+ max_microversion = 'latest'
+
+ @decorators.idempotent_id('4b111d28-b73d-4908-9bd2-03dc2992e4d4')
+ def test_group_create_show_list_delete(self):
+ # Create volume type
+ volume_type = self.create_volume_type()
+
+ # Create group type
+ group_type = self.create_group_type()
+
+ # Create group
+ grp1_name = data_utils.rand_name('Group1')
+ grp1 = self.create_group(group_type=group_type['id'],
+ volume_types=[volume_type['id']],
+ name=grp1_name)
+ grp1_id = grp1['id']
+
+ grp2_name = data_utils.rand_name('Group2')
+ grp2 = self.create_group(group_type=group_type['id'],
+ volume_types=[volume_type['id']],
+ name=grp2_name)
+ grp2_id = grp2['id']
+
+ # Create volume
+ vol1_name = data_utils.rand_name("volume")
+ params = {'name': vol1_name,
+ 'volume_type': volume_type['id'],
+ 'group_id': grp1['id'],
+ 'size': CONF.volume.volume_size}
+ vol1 = self.volumes_client.create_volume(**params)['volume']
+ self.assertEqual(grp1['id'], vol1['group_id'])
+ waiters.wait_for_volume_resource_status(
+ self.volumes_client, vol1['id'], 'available')
+ vol1_id = vol1['id']
+
+ # Get a given group
+ grp1 = self.groups_client.show_group(grp1['id'])['group']
+ self.assertEqual(grp1_name, grp1['name'])
+ self.assertEqual(grp1_id, grp1['id'])
+
+ grp2 = self.groups_client.show_group(grp2['id'])['group']
+ self.assertEqual(grp2_name, grp2['name'])
+ self.assertEqual(grp2_id, grp2['id'])
+
+ # Get all groups with detail
+ grps = self.groups_client.list_groups(detail=True)['groups']
+ for grp_id in [grp1_id, grp2_id]:
+ filtered_grps = [g for g in grps if g['id'] == grp_id]
+ self.assertEqual(1, len(filtered_grps))
+ self.assertEqual([volume_type['id']],
+ filtered_grps[0]['volume_types'])
+ self.assertEqual(group_type['id'],
+ filtered_grps[0]['group_type'])
+
+ vols = self.volumes_client.list_volumes(detail=True)['volumes']
+ filtered_vols = [v for v in vols if v['id'] in [vol1_id]]
+ self.assertEqual(1, len(filtered_vols))
+ for vol in filtered_vols:
+ self.assertEqual(grp1_id, vol['group_id'])
+
+ # Delete group
+ # grp1 has a volume so delete_volumes flag is set to True by default
+ self.delete_group(grp1_id)
+ # grp2 is empty so delete_volumes flag can be set to False
+ self.delete_group(grp2_id, delete_volumes=False)
+ grps = self.groups_client.list_groups(detail=True)['groups']
+ self.assertEmpty(grps)
+
+ @decorators.idempotent_id('4a8a6fd2-8b3b-4641-8f54-6a6f99320006')
+ def test_group_update(self):
+ # Create volume type
+ volume_type = self.create_volume_type()
+
+ # Create group type
+ group_type = self.create_group_type()
+
+ # Create Group
+ grp = self.create_group(group_type=group_type['id'],
+ volume_types=[volume_type['id']])
+
+ # Create volumes
+ grp_vols = []
+ for _ in range(2):
+ vol = self.create_volume(volume_type=volume_type['id'],
+ group_id=grp['id'])
+ grp_vols.append(vol)
+ vol2 = grp_vols[1]
+
+ # Remove a volume from group and update name and description
+ new_grp_name = 'new_group'
+ new_desc = 'This is a new group'
+ grp_params = {'name': new_grp_name,
+ 'description': new_desc,
+ 'remove_volumes': vol2['id']}
+ self.groups_client.update_group(grp['id'], **grp_params)
+
+ # Wait for group status to become available
+ waiters.wait_for_volume_resource_status(
+ self.groups_client, grp['id'], 'available')
+
+ # Get the updated Group
+ grp = self.groups_client.show_group(grp['id'])['group']
+ self.assertEqual(new_grp_name, grp['name'])
+ self.assertEqual(new_desc, grp['description'])
+
+ # Get volumes in the group
+ vols = self.volumes_client.list_volumes(detail=True)['volumes']
+ grp_vols = [v for v in vols if v['group_id'] == grp['id']]
+ self.assertEqual(1, len(grp_vols))
+
+ # Add a volume to the group
+ grp_params = {'add_volumes': vol2['id']}
+ self.groups_client.update_group(grp['id'], **grp_params)
+
+ # Wait for group status to become available
+ waiters.wait_for_volume_resource_status(
+ self.groups_client, grp['id'], 'available')
+
+ # Get volumes in the group
+ vols = self.volumes_client.list_volumes(detail=True)['volumes']
+ grp_vols = [v for v in vols if v['group_id'] == grp['id']]
+ self.assertEqual(2, len(grp_vols))
+
+
+class GroupsV314Test(base.BaseVolumeAdminTest):
+ _api_version = 3
+ min_microversion = '3.14'
+ max_microversion = 'latest'
+
+ @decorators.idempotent_id('2424af8c-7851-4888-986a-794b10c3210e')
+ def test_create_group_from_group(self):
+ # Create volume type
+ volume_type = self.create_volume_type()
+
+ # Create group type
+ group_type = self.create_group_type()
+
+ # Create Group
+ grp = self.create_group(group_type=group_type['id'],
+ volume_types=[volume_type['id']])
+
+ # Create volume
+ self.create_volume(volume_type=volume_type['id'], group_id=grp['id'])
+
+ # Create Group from Group
+ grp_name2 = data_utils.rand_name('Group_from_grp')
+ grp2 = self.groups_client.create_group_from_source(
+ source_group_id=grp['id'], name=grp_name2)['group']
+ self.addCleanup(self.delete_group, grp2['id'])
+ self.assertEqual(grp_name2, grp2['name'])
+ vols = self.volumes_client.list_volumes(detail=True)['volumes']
+ for vol in vols:
+ if vol['group_id'] == grp2['id']:
+ waiters.wait_for_volume_resource_status(
+ self.volumes_client, vol['id'], 'available')
+ waiters.wait_for_volume_resource_status(
+ self.groups_client, grp2['id'], 'available')
+
+
+class GroupsV320Test(base.BaseVolumeAdminTest):
+ _api_version = 3
+ min_microversion = '3.20'
+ max_microversion = 'latest'
+
+ @decorators.idempotent_id('b20c696b-0cbc-49a5-8b3a-b1fb9338f45c')
+ def test_reset_group_status(self):
+ # Create volume type
+ volume_type = self.create_volume_type()
+
+ # Create group type
+ group_type = self.create_group_type()
+
+ # Create group
+ group = self.create_group(group_type=group_type['id'],
+ volume_types=[volume_type['id']])
+
+ # Reset group status
+ self.addCleanup(waiters.wait_for_volume_resource_status,
+ self.groups_client, group['id'], 'available')
+ self.addCleanup(self.admin_groups_client.reset_group_status,
+ group['id'], 'available')
+ for status in ['creating', 'available', 'error']:
+ self.admin_groups_client.reset_group_status(group['id'], status)
+ waiters.wait_for_volume_resource_status(
+ self.groups_client, group['id'], status)
diff --git a/tempest/api/volume/admin/test_multi_backend.py b/tempest/api/volume/admin/test_multi_backend.py
index 8e43b00..c5c70d2 100644
--- a/tempest/api/volume/admin/test_multi_backend.py
+++ b/tempest/api/volume/admin/test_multi_backend.py
@@ -10,120 +10,90 @@
# License for the specific language governing permissions and limitations
# under the License.
-from oslo_log import log as logging
import six
from tempest.api.volume import base
-from tempest.common.utils import data_utils
+from tempest.common import waiters
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
CONF = config.CONF
-LOG = logging.getLogger(__name__)
-
-class VolumeMultiBackendV2Test(base.BaseVolumeAdminTest):
+class VolumeMultiBackendTest(base.BaseVolumeAdminTest):
@classmethod
def skip_checks(cls):
- super(VolumeMultiBackendV2Test, cls).skip_checks()
+ super(VolumeMultiBackendTest, cls).skip_checks()
if not CONF.volume_feature_enabled.multi_backend:
raise cls.skipException("Cinder multi-backend feature disabled")
+ if len(set(CONF.volume.backend_names)) < 2:
+ raise cls.skipException("Requires at least two different "
+ "backend names")
+
@classmethod
def resource_setup(cls):
- super(VolumeMultiBackendV2Test, cls).resource_setup()
- # support 2 backends names, deprecated_for_removal.
- # keep support 2 backend names, in case they are not empty
- if CONF.volume.backend1_name and CONF.volume.backend2_name:
- cls.backend_names = {CONF.volume.backend1_name,
- CONF.volume.backend2_name}
- else:
- # read backend name from a list .
- cls.backend_names = set(CONF.volume.backend_names)
+ super(VolumeMultiBackendTest, cls).resource_setup()
- cls.name_field = cls.special_fields['name_field']
- cls.volume_type_id_list = []
+ # read backend name from a list .
+ backend_names = set(CONF.volume.backend_names)
+
cls.volume_id_list_with_prefix = []
cls.volume_id_list_without_prefix = []
# Volume/Type creation (uses volume_backend_name)
# It is not allowed to create the same backend name twice
- if len(cls.backend_names) < 2:
- raise cls.skipException("Requires at least two different "
- "backend names")
- for backend_name in cls.backend_names:
+ for backend_name in backend_names:
# Volume/Type creation (uses backend_name)
cls._create_type_and_volume(backend_name, False)
# Volume/Type creation (uses capabilities:volume_backend_name)
cls._create_type_and_volume(backend_name, True)
@classmethod
- def _create_type_and_volume(self, backend_name_key, with_prefix):
+ def _create_type_and_volume(cls, backend_name_key, with_prefix):
# Volume/Type creation
- type_name = data_utils.rand_name('Type')
- vol_name = data_utils.rand_name('Volume')
+ type_name = data_utils.rand_name(cls.__name__ + '-Type')
+ vol_name = data_utils.rand_name(cls.__name__ + '-Volume')
spec_key_with_prefix = "capabilities:volume_backend_name"
spec_key_without_prefix = "volume_backend_name"
if with_prefix:
extra_specs = {spec_key_with_prefix: backend_name_key}
else:
extra_specs = {spec_key_without_prefix: backend_name_key}
- self.type = self.volume_types_client.create_volume_type(
- name=type_name, extra_specs=extra_specs)['volume_type']
- self.volume_type_id_list.append(self.type['id'])
+ cls.create_volume_type(name=type_name,
+ extra_specs=extra_specs)
- params = {self.name_field: vol_name, 'volume_type': type_name}
-
- self.volume = self.admin_volume_client.create_volume(
- **params)['volume']
+ params = {'name': vol_name, 'volume_type': type_name,
+ 'size': CONF.volume.volume_size}
+ cls.volume = cls.create_volume(**params)
if with_prefix:
- self.volume_id_list_with_prefix.append(self.volume['id'])
+ cls.volume_id_list_with_prefix.append(cls.volume['id'])
else:
- self.volume_id_list_without_prefix.append(
- self.volume['id'])
- self.admin_volume_client.wait_for_volume_status(
- self.volume['id'], 'available')
+ cls.volume_id_list_without_prefix.append(
+ cls.volume['id'])
+ waiters.wait_for_volume_resource_status(cls.admin_volume_client,
+ cls.volume['id'], 'available')
- @classmethod
- def resource_cleanup(cls):
- # volumes deletion
- vid_prefix = getattr(cls, 'volume_id_list_with_prefix', [])
- for volume_id in vid_prefix:
- cls.admin_volume_client.delete_volume(volume_id)
- cls.admin_volume_client.wait_for_resource_deletion(volume_id)
-
- vid_no_pre = getattr(cls, 'volume_id_list_without_prefix', [])
- for volume_id in vid_no_pre:
- cls.admin_volume_client.delete_volume(volume_id)
- cls.admin_volume_client.wait_for_resource_deletion(volume_id)
-
- # volume types deletion
- volume_type_id_list = getattr(cls, 'volume_type_id_list', [])
- for volume_type_id in volume_type_id_list:
- cls.volume_types_client.delete_volume_type(volume_type_id)
-
- super(VolumeMultiBackendV2Test, cls).resource_cleanup()
-
- @test.idempotent_id('c1a41f3f-9dad-493e-9f09-3ff197d477cc')
+ @decorators.idempotent_id('c1a41f3f-9dad-493e-9f09-3ff197d477cc')
def test_backend_name_reporting(self):
# get volume id which created by type without prefix
for volume_id in self.volume_id_list_without_prefix:
self._test_backend_name_reporting_by_volume_id(volume_id)
- @test.idempotent_id('f38e647f-ab42-4a31-a2e7-ca86a6485215')
+ @decorators.idempotent_id('f38e647f-ab42-4a31-a2e7-ca86a6485215')
def test_backend_name_reporting_with_prefix(self):
# get volume id which created by type with prefix
for volume_id in self.volume_id_list_with_prefix:
self._test_backend_name_reporting_by_volume_id(volume_id)
- @test.idempotent_id('46435ab1-a0af-4401-8373-f14e66b0dd58')
+ @decorators.idempotent_id('46435ab1-a0af-4401-8373-f14e66b0dd58')
def test_backend_name_distinction(self):
# get volume ids which created by type without prefix
self._test_backend_name_distinction(self.volume_id_list_without_prefix)
- @test.idempotent_id('4236305b-b65a-4bfc-a9d2-69cb5b2bf2ed')
+ @decorators.idempotent_id('4236305b-b65a-4bfc-a9d2-69cb5b2bf2ed')
def test_backend_name_distinction_with_prefix(self):
# get volume ids which created by type without prefix
self._test_backend_name_distinction(self.volume_id_list_with_prefix)
@@ -142,7 +112,7 @@
volume1_host = volume['os-vol-host-attr:host']
msg = ("multi-backend reporting incorrect values for volume %s" %
volume_id)
- self.assertTrue(len(volume1_host.split("@")) > 1, msg)
+ self.assertGreater(len(volume1_host.split("@")), 1, msg)
def _test_backend_name_distinction(self, volume_id_list):
# this test checks that the volumes created at setUp don't
@@ -154,7 +124,3 @@
msg = ("volumes %s were created in the same backend" % ", "
.join(volume_hosts))
six.assertCountEqual(self, volume_hosts, set(volume_hosts), msg)
-
-
-class VolumeMultiBackendV1Test(VolumeMultiBackendV2Test):
- _api_version = 1
diff --git a/tempest/api/volume/admin/test_qos.py b/tempest/api/volume/admin/test_qos.py
new file mode 100644
index 0000000..e31c0ef
--- /dev/null
+++ b/tempest/api/volume/admin/test_qos.py
@@ -0,0 +1,159 @@
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from tempest.api.volume import base
+from tempest.common import waiters
+from tempest.lib.common.utils import data_utils as utils
+from tempest.lib import decorators
+
+
+class QosSpecsTestJSON(base.BaseVolumeAdminTest):
+ """Test the Cinder QoS-specs.
+
+ Tests for create, list, delete, show, associate,
+ disassociate, set/unset key APIs.
+ """
+
+ @classmethod
+ def resource_setup(cls):
+ super(QosSpecsTestJSON, cls).resource_setup()
+ # Create admin qos client
+ # Create a test shared qos-specs for tests
+ cls.qos_name = utils.rand_name(cls.__name__ + '-QoS')
+ cls.qos_consumer = 'front-end'
+
+ cls.created_qos = cls.create_test_qos_specs(cls.qos_name,
+ cls.qos_consumer,
+ read_iops_sec='2000')
+
+ def _create_delete_test_qos_with_given_consumer(self, consumer):
+ name = utils.rand_name(self.__class__.__name__ + '-qos')
+ qos = {'name': name, 'consumer': consumer}
+ body = self.create_test_qos_specs(name, consumer)
+ for key in ['name', 'consumer']:
+ self.assertEqual(qos[key], body[key])
+
+ self.admin_volume_qos_client.delete_qos(body['id'])
+ self.admin_volume_qos_client.wait_for_resource_deletion(body['id'])
+
+ # validate the deletion
+ list_qos = self.admin_volume_qos_client.list_qos()['qos_specs']
+ self.assertNotIn(body, list_qos)
+
+ def _test_associate_qos(self, vol_type_id):
+ self.admin_volume_qos_client.associate_qos(
+ self.created_qos['id'], vol_type_id)
+
+ @decorators.idempotent_id('7e15f883-4bef-49a9-95eb-f94209a1ced1')
+ def test_create_delete_qos_with_front_end_consumer(self):
+ """Tests the creation and deletion of QoS specs
+
+ With consumer as front end
+ """
+ self._create_delete_test_qos_with_given_consumer('front-end')
+
+ @decorators.idempotent_id('b115cded-8f58-4ee4-aab5-9192cfada08f')
+ def test_create_delete_qos_with_back_end_consumer(self):
+ """Tests the creation and deletion of QoS specs
+
+ With consumer as back-end
+ """
+ self._create_delete_test_qos_with_given_consumer('back-end')
+
+ @decorators.idempotent_id('f88d65eb-ea0d-487d-af8d-71f4011575a4')
+ def test_create_delete_qos_with_both_consumer(self):
+ """Tests the creation and deletion of QoS specs
+
+ With consumer as both front end and back end
+ """
+ self._create_delete_test_qos_with_given_consumer('both')
+
+ @decorators.idempotent_id('7aa214cc-ac1a-4397-931f-3bb2e83bb0fd')
+ def test_get_qos(self):
+ """Tests the detail of a given qos-specs"""
+ body = self.admin_volume_qos_client.show_qos(
+ self.created_qos['id'])['qos_specs']
+ self.assertEqual(self.qos_name, body['name'])
+ self.assertEqual(self.qos_consumer, body['consumer'])
+
+ @decorators.idempotent_id('75e04226-bcf7-4595-a34b-fdf0736f38fc')
+ def test_list_qos(self):
+ """Tests the list of all qos-specs"""
+ body = self.admin_volume_qos_client.list_qos()['qos_specs']
+ self.assertIn(self.created_qos, body)
+
+ @decorators.idempotent_id('ed00fd85-4494-45f2-8ceb-9e2048919aed')
+ def test_set_unset_qos_key(self):
+ """Test the addition of a specs key to qos-specs"""
+ args = {'iops_bytes': '500'}
+ body = self.admin_volume_qos_client.set_qos_key(
+ self.created_qos['id'],
+ iops_bytes='500')['qos_specs']
+ self.assertEqual(args, body)
+ body = self.admin_volume_qos_client.show_qos(
+ self.created_qos['id'])['qos_specs']
+ self.assertEqual(args['iops_bytes'], body['specs']['iops_bytes'])
+
+ # test the deletion of a specs key from qos-specs
+ keys = ['iops_bytes']
+ self.admin_volume_qos_client.unset_qos_key(self.created_qos['id'],
+ keys)
+ operation = 'qos-key-unset'
+ waiters.wait_for_qos_operations(self.admin_volume_qos_client,
+ self.created_qos['id'],
+ operation, keys)
+ body = self.admin_volume_qos_client.show_qos(
+ self.created_qos['id'])['qos_specs']
+ self.assertNotIn(keys[0], body['specs'])
+
+ @decorators.idempotent_id('1dd93c76-6420-485d-a771-874044c416ac')
+ def test_associate_disassociate_qos(self):
+ """Test the following operations :
+
+ 1. associate_qos
+ 2. get_association_qos
+ 3. disassociate_qos
+ 4. disassociate_all_qos
+ """
+
+ # create a test volume-type
+ vol_type = []
+ for _ in range(0, 3):
+ vol_type.append(self.create_volume_type())
+
+ # associate the qos-specs with volume-types
+ for i in range(0, 3):
+ self._test_associate_qos(vol_type[i]['id'])
+
+ # get the association of the qos-specs
+ body = self.admin_volume_qos_client.show_association_qos(
+ self.created_qos['id'])['qos_associations']
+ associations = [association['id'] for association in body]
+ for i in range(0, 3):
+ self.assertIn(vol_type[i]['id'], associations)
+
+ # disassociate a volume-type with qos-specs
+ self.admin_volume_qos_client.disassociate_qos(
+ self.created_qos['id'], vol_type[0]['id'])
+ operation = 'disassociate'
+ waiters.wait_for_qos_operations(self.admin_volume_qos_client,
+ self.created_qos['id'], operation,
+ vol_type[0]['id'])
+
+ # disassociate all volume-types from qos-specs
+ self.admin_volume_qos_client.disassociate_all_qos(
+ self.created_qos['id'])
+ operation = 'disassociate-all'
+ waiters.wait_for_qos_operations(self.admin_volume_qos_client,
+ self.created_qos['id'], operation)
diff --git a/tempest/api/volume/admin/test_snapshot_manage.py b/tempest/api/volume/admin/test_snapshot_manage.py
new file mode 100644
index 0000000..37a47ec
--- /dev/null
+++ b/tempest/api/volume/admin/test_snapshot_manage.py
@@ -0,0 +1,101 @@
+# 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 tempest.api.volume import base
+from tempest.common import waiters
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions
+
+CONF = config.CONF
+
+
+class SnapshotManageAdminTest(base.BaseVolumeAdminTest):
+ """Unmanage & manage snapshots
+
+ This feature provides the ability to import/export volume snapshot
+ from one Cinder to another and to import snapshots that have not been
+ managed by Cinder from a storage back end to Cinder
+ """
+
+ @classmethod
+ def skip_checks(cls):
+ super(SnapshotManageAdminTest, cls).skip_checks()
+
+ if not CONF.volume_feature_enabled.snapshot:
+ raise cls.skipException("Cinder volume snapshots are disabled")
+
+ if not CONF.volume_feature_enabled.manage_snapshot:
+ raise cls.skipException("Manage snapshot tests are disabled")
+
+ if len(CONF.volume.manage_snapshot_ref) != 2:
+ msg = ("Manage snapshot ref is not correctly configured, "
+ "it should be a list of two elements")
+ raise exceptions.InvalidConfiguration(msg)
+
+ @decorators.idempotent_id('0132f42d-0147-4b45-8501-cc504bbf7810')
+ def test_unmanage_manage_snapshot(self):
+ # Create a volume
+ volume = self.create_volume()
+
+ # Create a snapshot
+ snapshot = self.create_snapshot(volume_id=volume['id'])
+
+ # Unmanage the snapshot
+ # Unmanage snapshot function works almost the same as delete snapshot,
+ # but it does not delete the snapshot data
+ self.admin_snapshots_client.unmanage_snapshot(snapshot['id'])
+ self.admin_snapshots_client.wait_for_resource_deletion(snapshot['id'])
+
+ # Verify the original snapshot does not exist in snapshot list
+ params = {'all_tenants': 1}
+ all_snapshots = self.admin_snapshots_client.list_snapshots(
+ detail=True, **params)['snapshots']
+ self.assertNotIn(snapshot['id'], [v['id'] for v in all_snapshots])
+
+ # Manage the snapshot
+ name = data_utils.rand_name(self.__class__.__name__ +
+ '-Managed-Snapshot')
+ description = data_utils.rand_name(self.__class__.__name__ +
+ '-Managed-Snapshot-Description')
+ metadata = {"manage-snap-meta1": "value1",
+ "manage-snap-meta2": "value2",
+ "manage-snap-meta3": "value3"}
+ snapshot_ref = {
+ 'volume_id': volume['id'],
+ 'ref': {CONF.volume.manage_snapshot_ref[0]:
+ CONF.volume.manage_snapshot_ref[1] % snapshot['id']},
+ 'name': name,
+ 'description': description,
+ 'metadata': metadata
+ }
+ new_snapshot = self.admin_snapshot_manage_client.manage_snapshot(
+ **snapshot_ref)['snapshot']
+ self.addCleanup(self.delete_snapshot, new_snapshot['id'],
+ self.admin_snapshots_client)
+
+ # Wait for the snapshot to be available after manage operation
+ waiters.wait_for_volume_resource_status(self.admin_snapshots_client,
+ new_snapshot['id'],
+ 'available')
+
+ # Verify the managed snapshot has the expected parent volume
+ # and the expected field values.
+ new_snapshot_info = self.admin_snapshots_client.show_snapshot(
+ new_snapshot['id'])['snapshot']
+ self.assertEqual(snapshot['size'], new_snapshot_info['size'])
+ for key in ['volume_id', 'name', 'description', 'metadata']:
+ self.assertEqual(snapshot_ref[key], new_snapshot_info[key])
diff --git a/tempest/api/volume/admin/test_snapshots_actions.py b/tempest/api/volume/admin/test_snapshots_actions.py
index aa6bfdf..41849bc 100644
--- a/tempest/api/volume/admin/test_snapshots_actions.py
+++ b/tempest/api/volume/admin/test_snapshots_actions.py
@@ -14,57 +14,29 @@
# under the License.
from tempest.api.volume import base
-from tempest.common.utils import data_utils
+from tempest.common import waiters
from tempest import config
-from tempest import test
+from tempest.lib import decorators
CONF = config.CONF
-class SnapshotsActionsV2Test(base.BaseVolumeAdminTest):
+class SnapshotsActionsTest(base.BaseVolumeAdminTest):
@classmethod
def skip_checks(cls):
- super(SnapshotsActionsV2Test, cls).skip_checks()
+ super(SnapshotsActionsTest, cls).skip_checks()
if not CONF.volume_feature_enabled.snapshot:
raise cls.skipException("Cinder snapshot feature disabled")
@classmethod
- def setup_clients(cls):
- super(SnapshotsActionsV2Test, cls).setup_clients()
- cls.client = cls.snapshots_client
-
- @classmethod
def resource_setup(cls):
- super(SnapshotsActionsV2Test, cls).resource_setup()
+ super(SnapshotsActionsTest, cls).resource_setup()
# Create a test shared volume for tests
- vol_name = data_utils.rand_name(cls.__name__ + '-Volume')
- cls.name_field = cls.special_fields['name_field']
- params = {cls.name_field: vol_name}
- cls.volume = \
- cls.volumes_client.create_volume(**params)['volume']
- cls.volumes_client.wait_for_volume_status(cls.volume['id'],
- 'available')
+ cls.volume = cls.create_volume()
# Create a test shared snapshot for tests
- snap_name = data_utils.rand_name(cls.__name__ + '-Snapshot')
- params = {cls.name_field: snap_name}
- cls.snapshot = cls.client.create_snapshot(
- cls.volume['id'], **params)['snapshot']
- cls.client.wait_for_snapshot_status(cls.snapshot['id'],
- 'available')
-
- @classmethod
- def resource_cleanup(cls):
- # Delete the test snapshot
- cls.client.delete_snapshot(cls.snapshot['id'])
- cls.client.wait_for_resource_deletion(cls.snapshot['id'])
-
- # Delete the test volume
- cls.volumes_client.delete_volume(cls.volume['id'])
- cls.volumes_client.wait_for_resource_deletion(cls.volume['id'])
-
- super(SnapshotsActionsV2Test, cls).resource_cleanup()
+ cls.snapshot = cls.create_snapshot(volume_id=cls.volume['id'])
def tearDown(self):
# Set snapshot's status to available after test
@@ -72,70 +44,71 @@
snapshot_id = self.snapshot['id']
self.admin_snapshots_client.reset_snapshot_status(snapshot_id,
status)
- super(SnapshotsActionsV2Test, self).tearDown()
+ waiters.wait_for_volume_resource_status(self.snapshots_client,
+ snapshot_id, status)
+ super(SnapshotsActionsTest, self).tearDown()
def _create_reset_and_force_delete_temp_snapshot(self, status=None):
# Create snapshot, reset snapshot status,
# and force delete temp snapshot
- temp_snapshot = self.create_snapshot(self.volume['id'])
+ temp_snapshot = self.create_snapshot(volume_id=self.volume['id'])
if status:
- self.admin_snapshots_client.\
- reset_snapshot_status(temp_snapshot['id'], status)
- self.admin_snapshots_client.\
- force_delete_snapshot(temp_snapshot['id'])
- self.client.wait_for_resource_deletion(temp_snapshot['id'])
+ self.admin_snapshots_client.reset_snapshot_status(
+ temp_snapshot['id'], status)
+ waiters.wait_for_volume_resource_status(
+ self.snapshots_client, temp_snapshot['id'], status)
+ self.admin_snapshots_client.force_delete_snapshot(temp_snapshot['id'])
+ self.snapshots_client.wait_for_resource_deletion(temp_snapshot['id'])
def _get_progress_alias(self):
return 'os-extended-snapshot-attributes:progress'
- @test.idempotent_id('3e13ca2f-48ea-49f3-ae1a-488e9180d535')
+ @decorators.idempotent_id('3e13ca2f-48ea-49f3-ae1a-488e9180d535')
def test_reset_snapshot_status(self):
# Reset snapshot status to creating
status = 'creating'
- self.admin_snapshots_client.\
- reset_snapshot_status(self.snapshot['id'], status)
- snapshot_get = self.admin_snapshots_client.show_snapshot(
- self.snapshot['id'])['snapshot']
- self.assertEqual(status, snapshot_get['status'])
+ self.admin_snapshots_client.reset_snapshot_status(
+ self.snapshot['id'], status)
+ waiters.wait_for_volume_resource_status(self.snapshots_client,
+ self.snapshot['id'], status)
- @test.idempotent_id('41288afd-d463-485e-8f6e-4eea159413eb')
+ @decorators.idempotent_id('41288afd-d463-485e-8f6e-4eea159413eb')
def test_update_snapshot_status(self):
# Reset snapshot status to creating
status = 'creating'
- self.admin_snapshots_client.\
- reset_snapshot_status(self.snapshot['id'], status)
+ self.admin_snapshots_client.reset_snapshot_status(
+ self.snapshot['id'], status)
+ waiters.wait_for_volume_resource_status(self.snapshots_client,
+ self.snapshot['id'], status)
# Update snapshot status to error
progress = '80%'
status = 'error'
progress_alias = self._get_progress_alias()
- self.client.update_snapshot_status(self.snapshot['id'],
- status, progress)
+ self.snapshots_client.update_snapshot_status(self.snapshot['id'],
+ status=status,
+ progress=progress)
snapshot_get = self.admin_snapshots_client.show_snapshot(
self.snapshot['id'])['snapshot']
self.assertEqual(status, snapshot_get['status'])
self.assertEqual(progress, snapshot_get[progress_alias])
- @test.idempotent_id('05f711b6-e629-4895-8103-7ca069f2073a')
+ @decorators.idempotent_id('05f711b6-e629-4895-8103-7ca069f2073a')
def test_snapshot_force_delete_when_snapshot_is_creating(self):
# test force delete when status of snapshot is creating
self._create_reset_and_force_delete_temp_snapshot('creating')
- @test.idempotent_id('92ce8597-b992-43a1-8868-6316b22a969e')
+ @decorators.idempotent_id('92ce8597-b992-43a1-8868-6316b22a969e')
def test_snapshot_force_delete_when_snapshot_is_deleting(self):
# test force delete when status of snapshot is deleting
self._create_reset_and_force_delete_temp_snapshot('deleting')
- @test.idempotent_id('645a4a67-a1eb-4e8e-a547-600abac1525d')
+ @decorators.idempotent_id('645a4a67-a1eb-4e8e-a547-600abac1525d')
def test_snapshot_force_delete_when_snapshot_is_error(self):
# test force delete when status of snapshot is error
self._create_reset_and_force_delete_temp_snapshot('error')
- @test.idempotent_id('bf89080f-8129-465e-9327-b2f922666ba5')
+ @decorators.idempotent_id('bf89080f-8129-465e-9327-b2f922666ba5')
def test_snapshot_force_delete_when_snapshot_is_error_deleting(self):
# test force delete when status of snapshot is error_deleting
self._create_reset_and_force_delete_temp_snapshot('error_deleting')
-
-
-class SnapshotsActionsV1Test(SnapshotsActionsV2Test):
- _api_version = 1
diff --git a/tempest/api/volume/admin/test_user_messages.py b/tempest/api/volume/admin/test_user_messages.py
new file mode 100644
index 0000000..20c3538
--- /dev/null
+++ b/tempest/api/volume/admin/test_user_messages.py
@@ -0,0 +1,88 @@
+# Copyright 2016 Andrew Kerr
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from tempest.api.volume import base
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+
+CONF = config.CONF
+
+MESSAGE_KEYS = [
+ 'created_at',
+ 'event_id',
+ 'guaranteed_until',
+ 'id',
+ 'message_level',
+ 'request_id',
+ 'resource_type',
+ 'resource_uuid',
+ 'user_message',
+ 'links']
+
+
+class UserMessagesTest(base.BaseVolumeAdminTest):
+ _api_version = 3
+ min_microversion = '3.3'
+ max_microversion = 'latest'
+
+ def _create_user_message(self):
+ """Trigger a 'no valid host' situation to generate a message."""
+ bad_protocol = data_utils.rand_name('storage_protocol')
+ bad_vendor = data_utils.rand_name('vendor_name')
+ extra_specs = {'storage_protocol': bad_protocol,
+ 'vendor_name': bad_vendor}
+ vol_type_name = data_utils.rand_name(
+ self.__class__.__name__ + '-volume-type')
+ bogus_type = self.create_volume_type(
+ name=vol_type_name, extra_specs=extra_specs)
+ params = {'volume_type': bogus_type['id'],
+ 'size': CONF.volume.volume_size}
+ volume = self.create_volume(wait_until="error", **params)
+ messages = self.messages_client.list_messages()['messages']
+ message_id = None
+ for message in messages:
+ if message['resource_uuid'] == volume['id']:
+ message_id = message['id']
+ break
+ self.assertIsNotNone(message_id, 'No user message generated for '
+ 'volume %s' % volume['id'])
+ return message_id
+
+ @decorators.idempotent_id('50f29e6e-f363-42e1-8ad1-f67ae7fd4d5a')
+ def test_list_messages(self):
+ self._create_user_message()
+ messages = self.messages_client.list_messages()['messages']
+ self.assertIsInstance(messages, list)
+ for message in messages:
+ for key in MESSAGE_KEYS:
+ self.assertIn(key, message.keys(),
+ 'Missing expected key %s' % key)
+
+ @decorators.idempotent_id('55a4a61e-c7b2-4ba0-a05d-b914bdef3070')
+ def test_show_message(self):
+ message_id = self._create_user_message()
+ self.addCleanup(self.messages_client.delete_message, message_id)
+
+ message = self.messages_client.show_message(message_id)['message']
+
+ for key in MESSAGE_KEYS:
+ self.assertIn(key, message.keys(), 'Missing expected key %s' % key)
+
+ @decorators.idempotent_id('c6eb6901-cdcc-490f-b735-4fe251842aed')
+ def test_delete_message(self):
+ message_id = self._create_user_message()
+ self.messages_client.delete_message(message_id)
+ self.messages_client.wait_for_resource_deletion(message_id)
diff --git a/tempest/api/volume/admin/test_volume_hosts.py b/tempest/api/volume/admin/test_volume_hosts.py
index b28488a..7e53ce8 100644
--- a/tempest/api/volume/admin/test_volume_hosts.py
+++ b/tempest/api/volume/admin/test_volume_hosts.py
@@ -14,17 +14,51 @@
# under the License.
from tempest.api.volume import base
-from tempest import test
+from tempest.lib import decorators
-class VolumeHostsAdminV2TestsJSON(base.BaseVolumeAdminTest):
+class VolumeHostsAdminTestsJSON(base.BaseVolumeAdminTest):
- @test.idempotent_id('d5f3efa2-6684-4190-9ced-1c2f526352ad')
+ @decorators.idempotent_id('d5f3efa2-6684-4190-9ced-1c2f526352ad')
def test_list_hosts(self):
- hosts = self.hosts_client.list_hosts()['hosts']
- self.assertTrue(len(hosts) >= 2, "No. of hosts are < 2,"
- "response of list hosts is: % s" % hosts)
+ hosts = self.admin_hosts_client.list_hosts()['hosts']
+ self.assertGreaterEqual(len(hosts), 2,
+ "The count of volume hosts is < 2, "
+ "response of list hosts is: %s" % hosts)
+ # Check elements in volume hosts list
+ host_list_keys = ['service', 'host_name', 'last-update',
+ 'zone', 'service-status', 'service-state']
+ for host in hosts:
+ for key in host_list_keys:
+ self.assertIn(key, host)
-class VolumeHostsAdminV1TestsJSON(VolumeHostsAdminV2TestsJSON):
- _api_version = 1
+ @decorators.idempotent_id('21168d57-b373-4b71-a3ac-f2c88f0c5d31')
+ def test_show_host(self):
+ hosts = self.admin_hosts_client.list_hosts()['hosts']
+ self.assertGreaterEqual(len(hosts), 2,
+ "The count of volume hosts is < 2, "
+ "response of list hosts is: %s" % hosts)
+
+ # Note(jeremyZ): The show host API is to show volume usage info on the
+ # specified cinder-volume host. If the host does not run cinder-volume
+ # service, or the cinder-volume service is disabled on the host, the
+ # 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')]
+ self.assertNotEmpty(c_vol_hosts,
+ "No available cinder-volume host is found, "
+ "all hosts that found are: %s" % hosts)
+
+ # Check each cinder-volume host.
+ host_detail_keys = ['project', 'volume_count', 'snapshot_count',
+ 'host', 'total_volume_gb', 'total_snapshot_gb']
+ for host in c_vol_hosts:
+ host_details = self.admin_hosts_client.show_host(host)['host']
+ self.assertNotEmpty(host_details)
+ for detail in host_details:
+ self.assertIn('resource', detail)
+ for key in host_detail_keys:
+ self.assertIn(key, detail['resource'])
diff --git a/tempest/api/volume/admin/test_volume_manage.py b/tempest/api/volume/admin/test_volume_manage.py
new file mode 100644
index 0000000..4b352e0
--- /dev/null
+++ b/tempest/api/volume/admin/test_volume_manage.py
@@ -0,0 +1,83 @@
+# 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 tempest.api.volume import base
+from tempest.common import waiters
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions
+
+CONF = config.CONF
+
+
+class VolumeManageAdminTest(base.BaseVolumeAdminTest):
+
+ @classmethod
+ def skip_checks(cls):
+ super(VolumeManageAdminTest, cls).skip_checks()
+
+ if not CONF.volume_feature_enabled.manage_volume:
+ raise cls.skipException("Manage volume tests are disabled")
+
+ if len(CONF.volume.manage_volume_ref) != 2:
+ msg = ("Manage volume ref is not correctly configured, "
+ "it should be a list of two elements")
+ raise exceptions.InvalidConfiguration(msg)
+
+ @decorators.idempotent_id('70076c71-0ce1-4208-a8ff-36a66e65cc1e')
+ def test_unmanage_manage_volume(self):
+ # Create original volume
+ org_vol_id = self.create_volume()['id']
+ org_vol_info = self.admin_volume_client.show_volume(
+ org_vol_id)['volume']
+
+ # Unmanage the original volume
+ self.admin_volume_client.unmanage_volume(org_vol_id)
+ self.admin_volume_client.wait_for_resource_deletion(org_vol_id)
+
+ # Verify the original volume does not exist in volume list
+ params = {'all_tenants': 1}
+ all_tenants_volumes = self.admin_volume_client.list_volumes(
+ detail=True, params=params)['volumes']
+ self.assertNotIn(org_vol_id, [v['id'] for v in all_tenants_volumes])
+
+ # Manage volume
+ new_vol_name = data_utils.rand_name(
+ self.__class__.__name__ + '-volume')
+ new_vol_ref = {
+ 'name': new_vol_name,
+ 'host': org_vol_info['os-vol-host-attr:host'],
+ 'ref': {CONF.volume.manage_volume_ref[0]:
+ CONF.volume.manage_volume_ref[1] % org_vol_id},
+ 'volume_type': org_vol_info['volume_type'],
+ 'availability_zone': org_vol_info['availability_zone']}
+ new_vol_id = self.admin_volume_manage_client.manage_volume(
+ **new_vol_ref)['volume']['id']
+ self.addCleanup(self.delete_volume,
+ self.admin_volume_client, new_vol_id)
+ waiters.wait_for_volume_resource_status(self.admin_volume_client,
+ new_vol_id, 'available')
+
+ # Compare the managed volume with the original
+ new_vol_info = self.admin_volume_client.show_volume(
+ new_vol_id)['volume']
+ self.assertNotIn(new_vol_id, [org_vol_id])
+ self.assertEqual(new_vol_info['name'], new_vol_name)
+ for key in ['size',
+ 'volume_type',
+ 'availability_zone',
+ 'os-vol-host-attr:host']:
+ self.assertEqual(new_vol_info[key], org_vol_info[key])
diff --git a/tempest/api/volume/admin/test_volume_pools.py b/tempest/api/volume/admin/test_volume_pools.py
new file mode 100644
index 0000000..d389c26
--- /dev/null
+++ b/tempest/api/volume/admin/test_volume_pools.py
@@ -0,0 +1,39 @@
+# Copyright 2016 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from tempest.api.volume import base
+from tempest import config
+from tempest.lib import decorators
+
+CONF = config.CONF
+
+
+class VolumePoolsAdminTestsJSON(base.BaseVolumeAdminTest):
+ def _assert_pools(self, with_detail=False):
+ cinder_pools = self.admin_scheduler_stats_client.list_pools(
+ detail=with_detail)['pools']
+ self.assertIn('name', cinder_pools[0])
+ if with_detail:
+ self.assertIn(CONF.volume.vendor_name,
+ [pool['capabilities']['vendor_name']
+ for pool in cinder_pools])
+
+ @decorators.idempotent_id('0248a46c-e226-4933-be10-ad6fca8227e7')
+ def test_get_pools_without_details(self):
+ self._assert_pools()
+
+ @decorators.idempotent_id('d4bb61f7-762d-4437-b8a4-5785759a0ced')
+ def test_get_pools_with_details(self):
+ self._assert_pools(with_detail=True)
diff --git a/tempest/api/volume/admin/test_volume_quota_classes.py b/tempest/api/volume/admin/test_volume_quota_classes.py
new file mode 100644
index 0000000..75dca41
--- /dev/null
+++ b/tempest/api/volume/admin/test_volume_quota_classes.py
@@ -0,0 +1,103 @@
+# 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.
+
+import random
+
+from oslo_log import log as logging
+from testtools import matchers
+
+from tempest.api.volume import base
+from tempest.common import identity
+from tempest.common import tempest_fixtures as fixtures
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+
+LOG = logging.getLogger(__name__)
+QUOTA_KEYS = ['gigabytes', 'snapshots', 'volumes', 'backups',
+ 'backup_gigabytes', 'per_volume_gigabytes']
+
+
+class VolumeQuotaClassesTest(base.BaseVolumeAdminTest):
+
+ def setUp(self):
+ # Note(jeremy.zhang): All test cases in this class need to externally
+ # lock on doing anything with default quota values.
+ self.useFixture(fixtures.LockFixture('volume_quotas'))
+ super(VolumeQuotaClassesTest, self).setUp()
+
+ def _restore_default_quotas(self, original_defaults):
+ LOG.debug("Restoring volume quota class defaults")
+ self.admin_quota_classes_client.update_quota_class_set(
+ 'default', **original_defaults)
+
+ @decorators.idempotent_id('abb9198e-67d0-4b09-859f-4f4a1418f176')
+ def test_show_default_quota(self):
+ default_quotas = self.admin_quota_classes_client.show_quota_class_set(
+ 'default')['quota_class_set']
+ self.assertIn('id', default_quotas)
+ self.assertEqual('default', default_quotas.pop('id'))
+ for key in QUOTA_KEYS:
+ self.assertIn(key, default_quotas)
+
+ @decorators.idempotent_id('a7644c63-2669-467a-b00e-452dd5c5397b')
+ def test_update_default_quota(self):
+ LOG.debug("Get the current default quota class values")
+ body = self.admin_quota_classes_client.show_quota_class_set(
+ 'default')['quota_class_set']
+
+ # Note(jeremyZ) Only include specified quota keys to avoid the conflict
+ # that other tests may create/delete volume types or update volume
+ # type's default quotas in concurrency running.
+ update_kwargs = {key: body[key] for key in body if key in QUOTA_KEYS}
+
+ # Restore the defaults when the test is done.
+ self.addCleanup(self._restore_default_quotas, update_kwargs.copy())
+
+ # Note(jeremyZ) Increment some of the values for updating the default
+ # quota class. For safety, only items with value >= 0 will be updated,
+ # and items with value < 0 (-1 means unlimited) will be ignored.
+ for quota, default in update_kwargs.items():
+ if default >= 0:
+ update_kwargs[quota] = default + 1
+
+ # Create a volume type for updating default quotas class.
+ volume_type_name = self.create_volume_type()['name']
+ for key in ['volumes', 'snapshots', 'gigabytes']:
+ update_kwargs['%s_%s' % (key, volume_type_name)] = \
+ random.randint(1, 10)
+
+ LOG.debug("Update limits for the default quota class set")
+ update_body = self.admin_quota_classes_client.update_quota_class_set(
+ 'default', **update_kwargs)['quota_class_set']
+ self.assertThat(update_body.items(),
+ matchers.ContainsAll(update_kwargs.items()))
+
+ # Verify current project's default quotas.
+ default_quotas = self.admin_quotas_client.show_default_quota_set(
+ self.os_admin.credentials.tenant_id)['quota_set']
+ self.assertThat(default_quotas.items(),
+ matchers.ContainsAll(update_kwargs.items()))
+
+ # Verify a new project's default quotas.
+ project_name = data_utils.rand_name('quota_class_tenant')
+ description = data_utils.rand_name('desc_')
+ project_id = identity.identity_utils(self.os_admin).create_project(
+ name=project_name, description=description)['id']
+ self.addCleanup(identity.identity_utils(self.os_admin).delete_project,
+ project_id)
+ default_quotas = self.admin_quotas_client.show_default_quota_set(
+ project_id)['quota_set']
+ self.assertThat(default_quotas.items(),
+ matchers.ContainsAll(update_kwargs.items()))
diff --git a/tempest/api/volume/admin/test_volume_quotas.py b/tempest/api/volume/admin/test_volume_quotas.py
index b2e52bb..e546bff 100644
--- a/tempest/api/volume/admin/test_volume_quotas.py
+++ b/tempest/api/volume/admin/test_volume_quotas.py
@@ -12,82 +12,125 @@
# License for the specific language governing permissions and limitations
# under the License.
-import six
-
from tempest.api.volume import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.common import tempest_fixtures as fixtures
+from tempest.common import waiters
+from tempest.lib import decorators
-QUOTA_KEYS = ['gigabytes', 'snapshots', 'volumes']
+QUOTA_KEYS = ['gigabytes', 'snapshots', 'volumes', 'backups',
+ 'backup_gigabytes', 'per_volume_gigabytes']
QUOTA_USAGE_KEYS = ['reserved', 'limit', 'in_use']
-class BaseVolumeQuotasAdminV2TestJSON(base.BaseVolumeAdminTest):
- force_tenant_isolation = True
+class BaseVolumeQuotasAdminTestJSON(base.BaseVolumeAdminTest):
+ credentials = ['primary', 'alt', 'admin']
+
+ def setUp(self):
+ # NOTE(jeremy.zhang): Avoid conflicts with volume quota class tests.
+ self.useFixture(fixtures.LockFixture('volume_quotas'))
+ super(BaseVolumeQuotasAdminTestJSON, self).setUp()
@classmethod
def setup_credentials(cls):
- super(BaseVolumeQuotasAdminV2TestJSON, cls).setup_credentials()
- cls.demo_tenant_id = cls.os.credentials.tenant_id
+ super(BaseVolumeQuotasAdminTestJSON, cls).setup_credentials()
+ cls.demo_tenant_id = cls.os_primary.credentials.tenant_id
- @test.idempotent_id('59eada70-403c-4cef-a2a3-a8ce2f1b07a0')
+ @classmethod
+ def setup_clients(cls):
+ super(BaseVolumeQuotasAdminTestJSON, cls).setup_clients()
+ cls.transfer_client = cls.os_primary.volume_transfers_v2_client
+ cls.alt_transfer_client = cls.os_alt.volume_transfers_v2_client
+
+ @classmethod
+ def resource_setup(cls):
+ super(BaseVolumeQuotasAdminTestJSON, cls).resource_setup()
+
+ # Save the current set of quotas so that some tests may use it
+ # to restore the quotas to their original values after they are
+ # done.
+ cls.original_quota_set = (cls.admin_quotas_client.show_quota_set(
+ cls.demo_tenant_id)['quota_set'])
+ cls.cleanup_quota_set = dict(
+ (k, v) for k, v in cls.original_quota_set.items()
+ if k in QUOTA_KEYS)
+
+ @decorators.idempotent_id('59eada70-403c-4cef-a2a3-a8ce2f1b07a0')
def test_list_quotas(self):
- quotas = (self.quotas_client.show_quota_set(self.demo_tenant_id)
+ quotas = (self.admin_quotas_client.show_quota_set(self.demo_tenant_id)
['quota_set'])
for key in QUOTA_KEYS:
self.assertIn(key, quotas)
- @test.idempotent_id('2be020a2-5fdd-423d-8d35-a7ffbc36e9f7')
+ @decorators.idempotent_id('2be020a2-5fdd-423d-8d35-a7ffbc36e9f7')
def test_list_default_quotas(self):
- quotas = self.quotas_client.show_default_quota_set(
+ quotas = self.admin_quotas_client.show_default_quota_set(
self.demo_tenant_id)['quota_set']
for key in QUOTA_KEYS:
self.assertIn(key, quotas)
- @test.idempotent_id('3d45c99e-cc42-4424-a56e-5cbd212b63a6')
+ @decorators.idempotent_id('3d45c99e-cc42-4424-a56e-5cbd212b63a6')
def test_update_all_quota_resources_for_tenant(self):
# Admin can update all the resource quota limits for a tenant
- default_quota_set = self.quotas_client.show_default_quota_set(
- self.demo_tenant_id)['quota_set']
new_quota_set = {'gigabytes': 1009,
'volumes': 11,
- 'snapshots': 11}
+ 'snapshots': 11,
+ 'backups': 11,
+ 'backup_gigabytes': 1009,
+ 'per_volume_gigabytes': 1009}
# Update limits for all quota resources
- quota_set = self.quotas_client.update_quota_set(
+ quota_set = self.admin_quotas_client.update_quota_set(
self.demo_tenant_id,
**new_quota_set)['quota_set']
- cleanup_quota_set = dict(
- (k, v) for k, v in six.iteritems(default_quota_set)
- if k in QUOTA_KEYS)
- self.addCleanup(self.quotas_client.update_quota_set,
- self.demo_tenant_id, **cleanup_quota_set)
+ self.addCleanup(self.admin_quotas_client.update_quota_set,
+ self.demo_tenant_id, **self.cleanup_quota_set)
+
# test that the specific values we set are actually in
# the final result. There is nothing here that ensures there
# would be no other values in there.
self.assertDictContainsSubset(new_quota_set, quota_set)
- @test.idempotent_id('18c51ae9-cb03-48fc-b234-14a19374dbed')
+ @decorators.idempotent_id('18c51ae9-cb03-48fc-b234-14a19374dbed')
def test_show_quota_usage(self):
- quota_usage = self.quotas_client.show_quota_usage(
- self.os_adm.credentials.tenant_id)['quota_set']
+ quota_usage = self.admin_quotas_client.show_quota_set(
+ self.os_admin.credentials.tenant_id,
+ params={'usage': True})['quota_set']
for key in QUOTA_KEYS:
self.assertIn(key, quota_usage)
for usage_key in QUOTA_USAGE_KEYS:
self.assertIn(usage_key, quota_usage[key])
- @test.idempotent_id('ae8b6091-48ad-4bfa-a188-bbf5cc02115f')
- def test_quota_usage(self):
- quota_usage = self.quotas_client.show_quota_usage(
+ @decorators.idempotent_id('874b35a9-51f1-4258-bec5-cd561b6690d3')
+ def test_delete_quota(self):
+ # Admin can delete the resource quota set for a project
+
+ self.addCleanup(self.admin_quotas_client.update_quota_set,
+ self.demo_tenant_id, **self.cleanup_quota_set)
+
+ quota_set_default = self.admin_quotas_client.show_default_quota_set(
self.demo_tenant_id)['quota_set']
+ volume_default = quota_set_default['volumes']
+
+ self.admin_quotas_client.update_quota_set(
+ self.demo_tenant_id, volumes=(volume_default + 5))
+
+ self.admin_quotas_client.delete_quota_set(self.demo_tenant_id)
+ quota_set_new = (self.admin_quotas_client.show_quota_set(
+ self.demo_tenant_id)['quota_set'])
+ self.assertEqual(volume_default, quota_set_new['volumes'])
+
+ @decorators.idempotent_id('ae8b6091-48ad-4bfa-a188-bbf5cc02115f')
+ def test_quota_usage(self):
+ quota_usage = self.admin_quotas_client.show_quota_set(
+ self.demo_tenant_id, params={'usage': True})['quota_set']
volume = self.create_volume()
- self.addCleanup(self.admin_volume_client.delete_volume,
- volume['id'])
+ self.addCleanup(self.delete_volume,
+ self.volumes_client, volume['id'])
- new_quota_usage = self.quotas_client.show_quota_usage(
- self.demo_tenant_id)['quota_set']
+ new_quota_usage = self.admin_quotas_client.show_quota_set(
+ self.demo_tenant_id, params={'usage': True})['quota_set']
self.assertEqual(quota_usage['volumes']['in_use'] + 1,
new_quota_usage['volumes']['in_use'])
@@ -96,27 +139,53 @@
volume["size"],
new_quota_usage['gigabytes']['in_use'])
- @test.idempotent_id('874b35a9-51f1-4258-bec5-cd561b6690d3')
- def test_delete_quota(self):
- # Admin can delete the resource quota set for a project
- project_name = data_utils.rand_name('quota_tenant')
- description = data_utils.rand_name('desc_')
- project = self.identity_utils.create_project(project_name,
- description=description)
- project_id = project['id']
- self.addCleanup(self.identity_utils.delete_project, project_id)
- quota_set_default = self.quotas_client.show_default_quota_set(
- project_id)['quota_set']
- volume_default = quota_set_default['volumes']
+ @decorators.idempotent_id('8911036f-9d54-4720-80cc-a1c9796a8805')
+ def test_quota_usage_after_volume_transfer(self):
+ # Create a volume for transfer
+ volume = self.create_volume()
+ self.addCleanup(self.delete_volume,
+ self.admin_volume_client, volume['id'])
- self.quotas_client.update_quota_set(project_id,
- volumes=(int(volume_default) + 5))
+ # List of tenants quota usage pre-transfer
+ primary_quota = self.admin_quotas_client.show_quota_set(
+ self.demo_tenant_id, params={'usage': True})['quota_set']
- self.quotas_client.delete_quota_set(project_id)
- quota_set_new = (self.quotas_client.show_quota_set(project_id)
- ['quota_set'])
- self.assertEqual(volume_default, quota_set_new['volumes'])
+ alt_quota = self.admin_quotas_client.show_quota_set(
+ self.os_alt.volumes_client_latest.tenant_id,
+ params={'usage': True})['quota_set']
+ # Creates a volume transfer
+ transfer = self.transfer_client.create_volume_transfer(
+ volume_id=volume['id'])['transfer']
+ transfer_id = transfer['id']
+ auth_key = transfer['auth_key']
-class VolumeQuotasAdminV1TestJSON(BaseVolumeQuotasAdminV2TestJSON):
- _api_version = 1
+ # Accepts a volume transfer
+ self.alt_transfer_client.accept_volume_transfer(
+ transfer_id, auth_key=auth_key)
+
+ # Verify volume transferred is available
+ waiters.wait_for_volume_resource_status(
+ self.os_alt.volumes_client_latest, volume['id'], 'available')
+
+ # List of tenants quota usage post transfer
+ new_primary_quota = self.admin_quotas_client.show_quota_set(
+ self.demo_tenant_id, params={'usage': True})['quota_set']
+
+ new_alt_quota = self.admin_quotas_client.show_quota_set(
+ self.os_alt.volumes_client_latest.tenant_id,
+ params={'usage': True})['quota_set']
+
+ # Verify tenants quota usage was updated
+ self.assertEqual(primary_quota['volumes']['in_use'] -
+ new_primary_quota['volumes']['in_use'],
+ new_alt_quota['volumes']['in_use'] -
+ alt_quota['volumes']['in_use'])
+
+ self.assertEqual(alt_quota['gigabytes']['in_use'] +
+ volume['size'],
+ new_alt_quota['gigabytes']['in_use'])
+
+ self.assertEqual(primary_quota['gigabytes']['in_use'] -
+ volume['size'],
+ new_primary_quota['gigabytes']['in_use'])
diff --git a/tempest/api/volume/admin/test_volume_quotas_negative.py b/tempest/api/volume/admin/test_volume_quotas_negative.py
index 9185553..f50f336 100644
--- a/tempest/api/volume/admin/test_volume_quotas_negative.py
+++ b/tempest/api/volume/admin/test_volume_quotas_negative.py
@@ -13,33 +13,44 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.volume import base
from tempest import config
-from tempest import test
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
CONF = config.CONF
+QUOTA_KEYS = ['gigabytes', 'snapshots', 'volumes', 'backups',
+ 'backup_gigabytes', 'per_volume_gigabytes']
-class BaseVolumeQuotasNegativeV2TestJSON(base.BaseVolumeAdminTest):
- force_tenant_isolation = True
+class BaseVolumeQuotasNegativeTestJSON(base.BaseVolumeAdminTest):
@classmethod
def setup_credentials(cls):
- super(BaseVolumeQuotasNegativeV2TestJSON, cls).setup_credentials()
- cls.demo_tenant_id = cls.os.credentials.tenant_id
+ super(BaseVolumeQuotasNegativeTestJSON, cls).setup_credentials()
+ cls.demo_tenant_id = cls.os_primary.credentials.tenant_id
@classmethod
def resource_setup(cls):
- super(BaseVolumeQuotasNegativeV2TestJSON, cls).resource_setup()
- cls.default_volume_size = cls.volumes_client.default_volume_size
- cls.shared_quota_set = {'gigabytes': 2 * cls.default_volume_size,
+ super(BaseVolumeQuotasNegativeTestJSON, cls).resource_setup()
+
+ # Save the current set of quotas, then set up the cleanup method
+ # to restore the quotas to their original values after the tests
+ # from this class are done. This is needed just in case Tempest is
+ # configured to use pre-provisioned projects/user accounts.
+ cls.original_quota_set = (cls.admin_quotas_client.show_quota_set(
+ cls.demo_tenant_id)['quota_set'])
+ cls.cleanup_quota_set = dict(
+ (k, v) for k, v in cls.original_quota_set.items()
+ if k in QUOTA_KEYS)
+ cls.addClassResourceCleanup(cls.admin_quotas_client.update_quota_set,
+ cls.demo_tenant_id,
+ **cls.cleanup_quota_set)
+
+ cls.shared_quota_set = {'gigabytes': 2 * CONF.volume.volume_size,
'volumes': 1}
- # NOTE(gfidente): no need to restore original quota set
- # after the tests as they only work with dynamic credentials.
- cls.quotas_client.update_quota_set(
+ cls.admin_quotas_client.update_quota_set(
cls.demo_tenant_id,
**cls.shared_quota_set)
@@ -47,29 +58,27 @@
# they are created using utility wrapper methods.
cls.volume = cls.create_volume()
- @test.attr(type='negative')
- @test.idempotent_id('bf544854-d62a-47f2-a681-90f7a47d86b6')
+ @decorators.attr(type='negative')
+ @decorators.idempotent_id('bf544854-d62a-47f2-a681-90f7a47d86b6')
def test_quota_volumes(self):
self.assertRaises(lib_exc.OverLimit,
- self.volumes_client.create_volume)
+ self.volumes_client.create_volume,
+ size=CONF.volume.volume_size)
- @test.attr(type='negative')
- @test.idempotent_id('2dc27eee-8659-4298-b900-169d71a91374')
+ @decorators.attr(type='negative')
+ @decorators.idempotent_id('2dc27eee-8659-4298-b900-169d71a91374')
def test_quota_volume_gigabytes(self):
# NOTE(gfidente): quota set needs to be changed for this test
# or we may be limited by the volumes or snaps quota number, not by
# actual gigs usage; next line ensures shared set is restored.
- self.addCleanup(self.quotas_client.update_quota_set,
+ self.addCleanup(self.admin_quotas_client.update_quota_set,
self.demo_tenant_id,
**self.shared_quota_set)
- new_quota_set = {'gigabytes': self.default_volume_size,
+ new_quota_set = {'gigabytes': CONF.volume.volume_size,
'volumes': 2, 'snapshots': 1}
- self.quotas_client.update_quota_set(
+ self.admin_quotas_client.update_quota_set(
self.demo_tenant_id,
**new_quota_set)
self.assertRaises(lib_exc.OverLimit,
- self.volumes_client.create_volume)
-
-
-class VolumeQuotasNegativeV1TestJSON(BaseVolumeQuotasNegativeV2TestJSON):
- _api_version = 1
+ self.volumes_client.create_volume,
+ size=CONF.volume.volume_size)
diff --git a/tempest/api/volume/admin/test_volume_retype.py b/tempest/api/volume/admin/test_volume_retype.py
new file mode 100644
index 0000000..423104b
--- /dev/null
+++ b/tempest/api/volume/admin/test_volume_retype.py
@@ -0,0 +1,176 @@
+# 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 abc
+
+from oslo_log import log as logging
+
+from tempest.api.volume import base
+from tempest.common import waiters
+from tempest import config
+from tempest.lib import decorators
+
+CONF = config.CONF
+
+LOG = logging.getLogger(__name__)
+
+
+class VolumeRetypeTest(base.BaseVolumeAdminTest):
+
+ 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
+ # before deleting the types we've created.
+
+ # This list should return 2 volumes until the copy and cleanup
+ # process is finished.
+ fetched_list = self.admin_volume_client.list_volumes(
+ params={'all_tenants': True,
+ 'display_name': vol['name']})['volumes']
+
+ for fetched_vol in fetched_list:
+ if fetched_vol['id'] != vol['id']:
+ # This is the Cinder internal volume
+ LOG.debug('Waiting for internal volume %s deletion',
+ fetched_vol['id'])
+ self.admin_volume_client.wait_for_resource_deletion(
+ fetched_vol['id'])
+ break
+
+ @abc.abstractmethod
+ def _verify_migration(self, source_vol, dest_vol):
+ pass
+
+ def _create_volume_from_snapshot(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'])
+
+ return src_vol
+
+ def _retype_volume(self, volume, migration_policy):
+
+ volume_source = self.admin_volume_client.show_volume(
+ volume['id'])['volume']
+
+ self.volumes_client.retype_volume(
+ volume['id'],
+ new_type=self.dst_vol_type['name'],
+ migration_policy=migration_policy)
+ 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(
+ volume['id'])['volume']
+
+ self._verify_migration(volume_source, volume_dest)
+
+
+class VolumeRetypeWithMigrationTest(VolumeRetypeTest):
+
+ @classmethod
+ def skip_checks(cls):
+ super(VolumeRetypeTest, cls).skip_checks()
+
+ if not CONF.volume_feature_enabled.multi_backend:
+ raise cls.skipException("Cinder multi-backend feature disabled.")
+
+ if len(set(CONF.volume.backend_names)) < 2:
+ raise cls.skipException("Requires at least two different "
+ "backend names")
+
+ @classmethod
+ def resource_setup(cls):
+ super(VolumeRetypeWithMigrationTest, cls).resource_setup()
+ # read backend name from a list.
+ backend_src = CONF.volume.backend_names[0]
+ backend_dst = CONF.volume.backend_names[1]
+
+ extra_specs_src = {"volume_backend_name": backend_src}
+ extra_specs_dst = {"volume_backend_name": backend_dst}
+
+ 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)
+
+ def _verify_migration(self, volume_source, volume_dest):
+
+ 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')
+
+ # Check the volume information after the migration.
+ self.assertEqual('success',
+ volume_dest['os-vol-mig-status-attr:migstat'])
+ self.assertEqual('success', volume_dest['migration_status'])
+
+ for key in keys_with_no_change:
+ self.assertEqual(volume_source[key], volume_dest[key])
+
+ 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, migration_policy='on-demand')
+
+ @decorators.idempotent_id('d0d9554f-e7a5-4104-8973-f35b27ccb60d')
+ def test_volume_from_snapshot_retype_with_migration(self):
+ src_vol = self._create_volume_from_snapshot()
+
+ # Migrate the volume from snapshot to the second backend
+ self._retype_volume(src_vol, migration_policy='on-demand')
+
+
+class VolumeRetypeWithoutMigrationTest(VolumeRetypeTest):
+
+ @classmethod
+ def resource_setup(cls):
+ super(VolumeRetypeWithoutMigrationTest, cls).resource_setup()
+ cls.src_vol_type = cls.create_volume_type('volume-type-1')
+ cls.dst_vol_type = cls.create_volume_type('volume-type-2')
+
+ def _verify_migration(self, volume_source, volume_dest):
+
+ keys_with_no_change = ('id', 'size', 'description', 'name',
+ 'user_id', 'os-vol-tenant-attr:tenant_id',
+ 'os-vol-host-attr:host')
+ keys_with_change = ('volume_type',)
+
+ # Check the volume information after the retype
+ self.assertIsNone(volume_dest['os-vol-mig-status-attr:migstat'])
+ self.assertIsNone(volume_dest['migration_status'])
+
+ for key in keys_with_no_change:
+ self.assertEqual(volume_source[key], volume_dest[key])
+
+ for key in keys_with_change:
+ self.assertNotEqual(volume_source[key], volume_dest[key])
+
+ @decorators.idempotent_id('b90412ee-465d-46e9-b249-ec84a47d5f25')
+ def test_available_volume_retype(self):
+ src_vol = self.create_volume(volume_type=self.src_vol_type['name'])
+
+ # Retype the volume from snapshot
+ self._retype_volume(src_vol, migration_policy='never')
diff --git a/tempest/api/volume/admin/test_volume_services.py b/tempest/api/volume/admin/test_volume_services.py
index 2b7ee45..293af81 100644
--- a/tempest/api/volume/admin/test_volume_services.py
+++ b/tempest/api/volume/admin/test_volume_services.py
@@ -12,12 +12,15 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
-
from tempest.api.volume import base
-from tempest import test
+from tempest.lib import decorators
-class VolumesServicesV2TestJSON(base.BaseVolumeAdminTest):
+def _get_host(host):
+ return host.split('@')[0]
+
+
+class VolumesServicesTestJSON(base.BaseVolumeAdminTest):
"""Tests Volume Services API.
volume service list requires admin privileges.
@@ -25,30 +28,33 @@
@classmethod
def resource_setup(cls):
- super(VolumesServicesV2TestJSON, cls).resource_setup()
+ super(VolumesServicesTestJSON, cls).resource_setup()
cls.services = (cls.admin_volume_services_client.list_services()
['services'])
- cls.host_name = cls.services[0]['host']
+ # NOTE: Cinder service-list API returns the list contains
+ # "<host name>@<driver name>" like "nova-compute01@lvmdriver-1".
+ # So here picks <host name> up as a host.
+ cls.host_name = _get_host(cls.services[0]['host'])
cls.binary_name = cls.services[0]['binary']
- @test.idempotent_id('e0218299-0a59-4f43-8b2b-f1c035b3d26d')
+ @decorators.idempotent_id('e0218299-0a59-4f43-8b2b-f1c035b3d26d')
def test_list_services(self):
services = (self.admin_volume_services_client.list_services()
['services'])
- self.assertNotEqual(0, len(services))
+ self.assertNotEmpty(services)
- @test.idempotent_id('63a3e1ca-37ee-4983-826d-83276a370d25')
+ @decorators.idempotent_id('63a3e1ca-37ee-4983-826d-83276a370d25')
def test_get_service_by_service_binary_name(self):
services = (self.admin_volume_services_client.list_services(
binary=self.binary_name)['services'])
- self.assertNotEqual(0, len(services))
+ self.assertNotEmpty(services)
for service in services:
self.assertEqual(self.binary_name, service['binary'])
- @test.idempotent_id('178710e4-7596-4e08-9333-745cb8bc4f8d')
+ @decorators.idempotent_id('178710e4-7596-4e08-9333-745cb8bc4f8d')
def test_get_service_by_host_name(self):
services_on_host = [service for service in self.services if
- service['host'] == self.host_name]
+ _get_host(service['host']) == self.host_name]
services = (self.admin_volume_services_client.list_services(
host=self.host_name)['services'])
@@ -61,16 +67,26 @@
# on order.
self.assertEqual(sorted(s1), sorted(s2))
- @test.idempotent_id('ffa6167c-4497-4944-a464-226bbdb53908')
+ @decorators.idempotent_id('67ec6902-f91d-4dec-91fa-338523208bbc')
+ def test_get_service_by_volume_host_name(self):
+ volume_id = self.create_volume()['id']
+ volume = self.admin_volume_client.show_volume(volume_id)['volume']
+ hostname = _get_host(volume['os-vol-host-attr:host'])
+
+ services = (self.admin_volume_services_client.list_services(
+ host=hostname, binary='cinder-volume')['services'])
+
+ self.assertNotEmpty(services,
+ 'cinder-volume not found on host %s' % hostname)
+ self.assertEqual(hostname, _get_host(services[0]['host']))
+ self.assertEqual('cinder-volume', services[0]['binary'])
+
+ @decorators.idempotent_id('ffa6167c-4497-4944-a464-226bbdb53908')
def test_get_service_by_service_and_host_name(self):
services = (self.admin_volume_services_client.list_services(
host=self.host_name, binary=self.binary_name))['services']
- self.assertEqual(1, len(services))
- self.assertEqual(self.host_name, services[0]['host'])
+ self.assertNotEmpty(services)
+ self.assertEqual(self.host_name, _get_host(services[0]['host']))
self.assertEqual(self.binary_name, services[0]['binary'])
-
-
-class VolumesServicesV1TestJSON(VolumesServicesV2TestJSON):
- _api_version = 1
diff --git a/tempest/api/volume/admin/test_volume_services_negative.py b/tempest/api/volume/admin/test_volume_services_negative.py
new file mode 100644
index 0000000..6f3dbc6
--- /dev/null
+++ b/tempest/api/volume/admin/test_volume_services_negative.py
@@ -0,0 +1,65 @@
+# Copyright 2018 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 tempest.api.volume import base
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+
+class VolumeServicesNegativeTest(base.BaseVolumeAdminTest):
+
+ @classmethod
+ def resource_setup(cls):
+ super(VolumeServicesNegativeTest, cls).resource_setup()
+ cls.services = cls.admin_volume_services_client.list_services()[
+ 'services']
+ cls.host = cls.services[0]['host']
+ cls.binary = cls.services[0]['binary']
+
+ @decorators.attr(type='negative')
+ @decorators.idempotent_id('3246ce65-ba70-4159-aa3b-082c28e4b484')
+ def test_enable_service_with_invalid_host(self):
+ self.assertRaises(lib_exc.NotFound,
+ self.admin_volume_services_client.enable_service,
+ host='invalid_host', binary=self.binary)
+
+ @decorators.attr(type='negative')
+ @decorators.idempotent_id('c571f179-c6e6-4c50-a0ab-368b628a8ac1')
+ def test_disable_service_with_invalid_binary(self):
+ self.assertRaises(lib_exc.NotFound,
+ self.admin_volume_services_client.disable_service,
+ host=self.host, binary='invalid_binary')
+
+ @decorators.attr(type='negative')
+ @decorators.idempotent_id('77767b36-5e8f-4c68-a0b5-2308cc21ec64')
+ def test_disable_log_reason_with_no_reason(self):
+ self.assertRaises(lib_exc.BadRequest,
+ self.admin_volume_services_client.disable_log_reason,
+ host=self.host, binary=self.binary,
+ disabled_reason=None)
+
+ @decorators.attr(type='negative')
+ @decorators.idempotent_id('712bfab8-1f44-4eb5-a632-fa70bf78f05e')
+ def test_freeze_host_with_invalid_host(self):
+ self.assertRaises(lib_exc.BadRequest,
+ self.admin_volume_services_client.freeze_host,
+ host='invalid_host')
+
+ @decorators.attr(type='negative')
+ @decorators.idempotent_id('7c6287c9-d655-47e1-9a11-76f6657a6dce')
+ def test_thaw_host_with_invalid_host(self):
+ self.assertRaises(lib_exc.BadRequest,
+ self.admin_volume_services_client.thaw_host,
+ host='invalid_host')
diff --git a/tempest/api/volume/admin/test_volume_snapshot_quotas_negative.py b/tempest/api/volume/admin/test_volume_snapshot_quotas_negative.py
index ce0b618..74eb792 100644
--- a/tempest/api/volume/admin/test_volume_snapshot_quotas_negative.py
+++ b/tempest/api/volume/admin/test_volume_snapshot_quotas_negative.py
@@ -13,69 +13,77 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.volume import base
from tempest import config
-from tempest import test
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
CONF = config.CONF
+QUOTA_KEYS = ['gigabytes', 'snapshots', 'volumes', 'backups',
+ 'backup_gigabytes', 'per_volume_gigabytes']
-class VolumeSnapshotQuotasNegativeV2TestJSON(base.BaseVolumeAdminTest):
- force_tenant_isolation = True
+class VolumeSnapshotQuotasNegativeTestJSON(base.BaseVolumeAdminTest):
@classmethod
def skip_checks(cls):
- super(VolumeSnapshotQuotasNegativeV2TestJSON, cls).skip_checks()
+ super(VolumeSnapshotQuotasNegativeTestJSON, cls).skip_checks()
if not CONF.volume_feature_enabled.snapshot:
raise cls.skipException('Cinder volume snapshots are disabled')
@classmethod
def setup_credentials(cls):
- super(VolumeSnapshotQuotasNegativeV2TestJSON, cls).setup_credentials()
- cls.demo_tenant_id = cls.os.credentials.tenant_id
+ super(VolumeSnapshotQuotasNegativeTestJSON, cls).setup_credentials()
+ cls.demo_tenant_id = cls.os_primary.credentials.tenant_id
@classmethod
def resource_setup(cls):
- super(VolumeSnapshotQuotasNegativeV2TestJSON, cls).resource_setup()
- cls.default_volume_size = cls.volumes_client.default_volume_size
+ super(VolumeSnapshotQuotasNegativeTestJSON, cls).resource_setup()
+
+ # Save the current set of quotas, then set up the cleanup method
+ # to restore the quotas to their original values after the tests
+ # from this class are done. This is needed just in case Tempest is
+ # configured to use pre-provisioned projects/user accounts.
+ cls.original_quota_set = (cls.admin_quotas_client.show_quota_set(
+ cls.demo_tenant_id)['quota_set'])
+ cls.cleanup_quota_set = dict(
+ (k, v) for k, v in cls.original_quota_set.items()
+ if k in QUOTA_KEYS)
+ cls.addClassResourceCleanup(cls.admin_quotas_client.update_quota_set,
+ cls.demo_tenant_id,
+ **cls.cleanup_quota_set)
+
+ cls.default_volume_size = CONF.volume.volume_size
cls.shared_quota_set = {'gigabytes': 3 * cls.default_volume_size,
'volumes': 1, 'snapshots': 1}
- # NOTE(gfidente): no need to restore original quota set
- # after the tests as they only work with tenant isolation.
- cls.quotas_client.update_quota_set(
+ cls.admin_quotas_client.update_quota_set(
cls.demo_tenant_id,
**cls.shared_quota_set)
# NOTE(gfidente): no need to delete in tearDown as
# they are created using utility wrapper methods.
cls.volume = cls.create_volume()
- cls.snapshot = cls.create_snapshot(cls.volume['id'])
+ cls.snapshot = cls.create_snapshot(volume_id=cls.volume['id'])
- @test.attr(type='negative')
- @test.idempotent_id('02bbf63f-6c05-4357-9d98-2926a94064ff')
+ @decorators.attr(type='negative')
+ @decorators.idempotent_id('02bbf63f-6c05-4357-9d98-2926a94064ff')
def test_quota_volume_snapshots(self):
self.assertRaises(lib_exc.OverLimit,
self.snapshots_client.create_snapshot,
- self.volume['id'])
+ volume_id=self.volume['id'])
- @test.attr(type='negative')
- @test.idempotent_id('c99a1ca9-6cdf-498d-9fdf-25832babef27')
+ @decorators.attr(type='negative')
+ @decorators.idempotent_id('c99a1ca9-6cdf-498d-9fdf-25832babef27')
def test_quota_volume_gigabytes_snapshots(self):
- self.addCleanup(self.quotas_client.update_quota_set,
+ self.addCleanup(self.admin_quotas_client.update_quota_set,
self.demo_tenant_id,
**self.shared_quota_set)
new_quota_set = {'gigabytes': 2 * self.default_volume_size,
'volumes': 1, 'snapshots': 2}
- self.quotas_client.update_quota_set(
+ self.admin_quotas_client.update_quota_set(
self.demo_tenant_id,
**new_quota_set)
self.assertRaises(lib_exc.OverLimit,
self.snapshots_client.create_snapshot,
- self.volume['id'])
-
-
-class VolumeSnapshotNegativeV1TestJSON(VolumeSnapshotQuotasNegativeV2TestJSON):
- _api_version = 1
+ volume_id=self.volume['id'])
diff --git a/tempest/api/volume/admin/test_volume_type_access.py b/tempest/api/volume/admin/test_volume_type_access.py
new file mode 100644
index 0000000..b64face
--- /dev/null
+++ b/tempest/api/volume/admin/test_volume_type_access.py
@@ -0,0 +1,88 @@
+# Copyright 2016 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import operator
+
+from tempest.api.volume import base
+from tempest import config
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+CONF = config.CONF
+
+
+class VolumeTypesAccessTest(base.BaseVolumeAdminTest):
+
+ credentials = ['primary', 'alt', 'admin']
+
+ @decorators.idempotent_id('d4dd0027-835f-4554-a6e5-50903fb79184')
+ def test_volume_type_access_add(self):
+ # Creating a NON public volume type
+ params = {'os-volume-type-access:is_public': False}
+ volume_type = self.create_volume_type(**params)
+
+ # Try creating a volume from volume type in primary tenant
+ self.assertRaises(lib_exc.NotFound, self.volumes_client.create_volume,
+ volume_type=volume_type['id'],
+ size=CONF.volume.volume_size)
+
+ # Adding volume type access for primary tenant
+ self.admin_volume_types_client.add_type_access(
+ volume_type['id'], project=self.volumes_client.tenant_id)
+ self.addCleanup(self.admin_volume_types_client.remove_type_access,
+ volume_type['id'],
+ project=self.volumes_client.tenant_id)
+
+ # Creating a volume from primary tenant
+ volume = self.create_volume(volume_type=volume_type['id'])
+ # Validating the created volume is based on the volume type
+ self.assertEqual(volume_type['name'], volume['volume_type'])
+
+ @decorators.idempotent_id('5220eb28-a435-43ce-baaf-ed46f0e95159')
+ def test_volume_type_access_list(self):
+ # Creating a NON public volume type
+ params = {'os-volume-type-access:is_public': False}
+ volume_type = self.create_volume_type(**params)
+
+ # Adding volume type access for primary tenant
+ self.admin_volume_types_client.add_type_access(
+ volume_type['id'], project=self.volumes_client.tenant_id)
+ self.addCleanup(self.admin_volume_types_client.remove_type_access,
+ volume_type['id'],
+ project=self.volumes_client.tenant_id)
+
+ # Adding volume type access for alt tenant
+ self.admin_volume_types_client.add_type_access(
+ volume_type['id'],
+ project=self.os_alt.volumes_client_latest.tenant_id)
+ self.addCleanup(self.admin_volume_types_client.remove_type_access,
+ volume_type['id'],
+ project=self.os_alt.volumes_client_latest.tenant_id)
+
+ # List tenant access for the given volume type
+ type_access_list = self.admin_volume_types_client.list_type_access(
+ volume_type['id'])['volume_type_access']
+ volume_type_ids = [
+ vol_type['volume_type_id'] for vol_type in type_access_list
+ ]
+
+ # Validating volume type available for only two tenants
+ self.assertEqual(2, volume_type_ids.count(volume_type['id']))
+
+ # Validating the permitted tenants are the expected tenants
+ self.assertIn(self.volumes_client.tenant_id,
+ map(operator.itemgetter('project_id'), type_access_list))
+ self.assertIn(self.os_alt.volumes_client_latest.tenant_id,
+ map(operator.itemgetter('project_id'), type_access_list))
diff --git a/tempest/api/volume/admin/test_volume_types.py b/tempest/api/volume/admin/test_volume_types.py
index acb591d..1077524 100644
--- a/tempest/api/volume/admin/test_volume_types.py
+++ b/tempest/api/volume/admin/test_volume_types.py
@@ -14,64 +14,55 @@
# under the License.
from tempest.api.volume import base
-from tempest.common.utils import data_utils
+from tempest.common import waiters
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
CONF = config.CONF
-class VolumeTypesV2Test(base.BaseVolumeAdminTest):
+class VolumeTypesTest(base.BaseVolumeAdminTest):
- def _delete_volume(self, volume_id):
- self.volumes_client.delete_volume(volume_id)
- self.volumes_client.wait_for_resource_deletion(volume_id)
-
- def _delete_volume_type(self, volume_type_id):
- self.volume_types_client.delete_volume_type(volume_type_id)
-
- @test.idempotent_id('9d9b28e3-1b2e-4483-a2cc-24aa0ea1de54')
+ @decorators.idempotent_id('9d9b28e3-1b2e-4483-a2cc-24aa0ea1de54')
def test_volume_type_list(self):
# List volume types.
- body = self.volume_types_client.list_volume_types()['volume_types']
+ body = \
+ self.admin_volume_types_client.list_volume_types()['volume_types']
self.assertIsInstance(body, list)
- @test.idempotent_id('c03cc62c-f4e9-4623-91ec-64ce2f9c1260')
+ @decorators.idempotent_id('c03cc62c-f4e9-4623-91ec-64ce2f9c1260')
def test_volume_crud_with_volume_type_and_extra_specs(self):
# Create/update/get/delete volume with volume_type and extra spec.
volume_types = list()
- vol_name = data_utils.rand_name("volume")
- self.name_field = self.special_fields['name_field']
+ vol_name = data_utils.rand_name(self.__class__.__name__ + '-volume')
proto = CONF.volume.storage_protocol
vendor = CONF.volume.vendor_name
extra_specs = {"storage_protocol": proto,
"vendor_name": vendor}
# Create two volume_types
- for i in range(2):
- vol_type_name = data_utils.rand_name("volume-type")
- vol_type = self.volume_types_client.create_volume_type(
- name=vol_type_name,
- extra_specs=extra_specs)['volume_type']
+ for _ in range(2):
+ vol_type = self.create_volume_type(
+ extra_specs=extra_specs)
volume_types.append(vol_type)
- self.addCleanup(self._delete_volume_type, vol_type['id'])
- params = {self.name_field: vol_name,
- 'volume_type': volume_types[0]['id']}
+ params = {'name': vol_name,
+ 'volume_type': volume_types[0]['id'],
+ 'size': CONF.volume.volume_size}
# Create volume
- volume = self.volumes_client.create_volume(**params)['volume']
- self.addCleanup(self._delete_volume, volume['id'])
+ volume = self.create_volume(**params)
self.assertEqual(volume_types[0]['name'], volume["volume_type"])
- self.assertEqual(volume[self.name_field], vol_name,
+ self.assertEqual(volume['name'], vol_name,
"The created volume name is not equal "
"to the requested name")
self.assertIsNotNone(volume['id'],
"Field volume id is empty or not found.")
- self.volumes_client.wait_for_volume_status(volume['id'], 'available')
# Update volume with new volume_type
self.volumes_client.retype_volume(volume['id'],
- volume_type=volume_types[1]['id'])
- self.volumes_client.wait_for_volume_status(volume['id'], 'available')
+ new_type=volume_types[1]['id'])
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume['id'], 'available')
# Get volume details and Verify
fetched_volume = self.volumes_client.show_volume(
@@ -80,34 +71,37 @@
fetched_volume['volume_type'],
'The fetched Volume type is different '
'from updated volume type')
- self.assertEqual(vol_name, fetched_volume[self.name_field],
+ self.assertEqual(vol_name, fetched_volume['name'],
'The fetched Volume is different '
'from the created Volume')
self.assertEqual(volume['id'], fetched_volume['id'],
'The fetched Volume is different '
'from the created Volume')
- @test.idempotent_id('4e955c3b-49db-4515-9590-0c99f8e471ad')
+ @decorators.idempotent_id('4e955c3b-49db-4515-9590-0c99f8e471ad')
def test_volume_type_create_get_delete(self):
# Create/get volume type.
- body = {}
- name = data_utils.rand_name("volume-type")
+ name = data_utils.rand_name(self.__class__.__name__ + '-volume-type')
+ description = data_utils.rand_name("volume-type-description")
proto = CONF.volume.storage_protocol
vendor = CONF.volume.vendor_name
extra_specs = {"storage_protocol": proto,
"vendor_name": vendor}
- body = self.volume_types_client.create_volume_type(
- name=name,
- extra_specs=extra_specs)['volume_type']
- self.assertIn('id', body)
- self.addCleanup(self._delete_volume_type, body['id'])
+ params = {'name': name,
+ 'description': description,
+ 'extra_specs': extra_specs,
+ 'os-volume-type-access:is_public': True}
+ body = self.create_volume_type(**params)
self.assertIn('name', body)
- self.assertEqual(body['name'], name,
+ self.assertEqual(name, body['name'],
"The created volume_type name is not equal "
"to the requested name")
- self.assertTrue(body['id'] is not None,
- "Field volume_type id is empty or not found.")
- fetched_volume_type = self.volume_types_client.show_volume_type(
+ self.assertEqual(description, body['description'],
+ "The created volume_type_description name is "
+ "not equal to the requested name")
+ self.assertIsNotNone(body['id'],
+ "Field volume_type id is empty or not found.")
+ fetched_volume_type = self.admin_volume_types_client.show_volume_type(
body['id'])['volume_type']
self.assertEqual(name, fetched_volume_type['name'],
'The fetched Volume_type is different '
@@ -118,53 +112,89 @@
self.assertEqual(extra_specs, fetched_volume_type['extra_specs'],
'The fetched Volume_type is different '
'from the created Volume_type')
+ self.assertEqual(description, fetched_volume_type['description'])
+ self.assertEqual(body['is_public'],
+ fetched_volume_type['is_public'])
+ self.assertEqual(
+ body['os-volume-type-access:is_public'],
+ fetched_volume_type['os-volume-type-access:is_public'])
- @test.idempotent_id('7830abd0-ff99-4793-a265-405684a54d46')
- def test_volume_type_encryption_create_get_delete(self):
- # Create/get/delete encryption type.
- provider = "LuksEncryptor"
- control_location = "front-end"
- name = data_utils.rand_name("volume-type")
- body = self.volume_types_client.create_volume_type(
- name=name)['volume_type']
- self.addCleanup(self._delete_volume_type, body['id'])
+ @decorators.idempotent_id('7830abd0-ff99-4793-a265-405684a54d46')
+ def test_volume_type_encryption_create_get_update_delete(self):
+ # Create/get/update/delete encryption type.
+ create_kwargs = {'provider': 'LuksEncryptor',
+ 'control_location': 'front-end'}
+ volume_type_id = self.create_volume_type()['id']
# Create encryption type
- encryption_type = self.volume_types_client.create_encryption_type(
- body['id'], provider=provider,
- control_location=control_location)['encryption']
+ encryption_type = \
+ self.admin_encryption_types_client.create_encryption_type(
+ volume_type_id, **create_kwargs)['encryption']
self.assertIn('volume_type_id', encryption_type)
- self.assertEqual(provider, encryption_type['provider'],
- "The created encryption_type provider is not equal "
- "to the requested provider")
- self.assertEqual(control_location, encryption_type['control_location'],
- "The created encryption_type control_location is not "
- "equal to the requested control_location")
+ for key in create_kwargs:
+ self.assertEqual(create_kwargs[key], encryption_type[key],
+ 'The created encryption_type %s is different '
+ 'from the requested encryption_type' % key)
# Get encryption type
+ encrypt_type_id = encryption_type['volume_type_id']
fetched_encryption_type = (
- self.volume_types_client.show_encryption_type(
- encryption_type['volume_type_id']))
- self.assertEqual(provider,
- fetched_encryption_type['provider'],
- 'The fetched encryption_type provider is different '
- 'from the created encryption_type')
- self.assertEqual(control_location,
- fetched_encryption_type['control_location'],
- 'The fetched encryption_type control_location is '
- 'different from the created encryption_type')
+ self.admin_encryption_types_client.show_encryption_type(
+ encrypt_type_id))
+ for key in create_kwargs:
+ self.assertEqual(create_kwargs[key], fetched_encryption_type[key],
+ 'The fetched encryption_type %s is different '
+ 'from the created encryption_type' % key)
+
+ # Update encryption type
+ update_kwargs = {'key_size': 128,
+ 'provider': 'SomeProvider',
+ 'cipher': 'aes-xts-plain64',
+ 'control_location': 'back-end'}
+ self.admin_encryption_types_client.update_encryption_type(
+ encrypt_type_id, **update_kwargs)
+ updated_encryption_type = (
+ self.admin_encryption_types_client.show_encryption_type(
+ encrypt_type_id))
+ for key in update_kwargs:
+ self.assertEqual(update_kwargs[key], updated_encryption_type[key],
+ 'The fetched encryption_type %s is different '
+ 'from the updated encryption_type' % key)
+
+ # Get encryption specs item
+ key = 'cipher'
+ item = self.admin_encryption_types_client.show_encryption_specs_item(
+ encrypt_type_id, key)
+ self.assertEqual(update_kwargs[key], item[key])
# Delete encryption type
- self.volume_types_client.delete_encryption_type(
- encryption_type['volume_type_id'])
- resource = {"id": encryption_type['volume_type_id'],
- "type": "encryption-type"}
- self.volume_types_client.wait_for_resource_deletion(resource)
+ self.admin_encryption_types_client.delete_encryption_type(
+ encrypt_type_id)
+ self.admin_encryption_types_client.wait_for_resource_deletion(
+ encrypt_type_id)
deleted_encryption_type = (
- self.volume_types_client.show_encryption_type(
- encryption_type['volume_type_id']))
+ self.admin_encryption_types_client.show_encryption_type(
+ encrypt_type_id))
self.assertEmpty(deleted_encryption_type)
+ @decorators.idempotent_id('cf9f07c6-db9e-4462-a243-5933ad65e9c8')
+ def test_volume_type_update(self):
+ # Create volume type
+ volume_type = self.create_volume_type()
-class VolumeTypesV1Test(VolumeTypesV2Test):
- _api_version = 1
+ # New volume type details
+ name = data_utils.rand_name("volume-type")
+ description = data_utils.rand_name("volume-type-description")
+ is_public = not volume_type['is_public']
+
+ # Update volume type details
+ kwargs = {'name': name,
+ 'description': description,
+ 'is_public': is_public}
+ updated_vol_type = self.admin_volume_types_client.update_volume_type(
+ volume_type['id'], **kwargs)['volume_type']
+
+ # Verify volume type details were updated
+ self.assertEqual(name, updated_vol_type['name'])
+ self.assertEqual(description, updated_vol_type['description'])
+ self.assertEqual(is_public, updated_vol_type['is_public'])
diff --git a/tempest/api/volume/admin/test_volume_types_extra_specs.py b/tempest/api/volume/admin/test_volume_types_extra_specs.py
index 502cd86..730acdf 100644
--- a/tempest/api/volume/admin/test_volume_types_extra_specs.py
+++ b/tempest/api/volume/admin/test_volume_types_extra_specs.py
@@ -14,75 +14,84 @@
# under the License.
from tempest.api.volume import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
-class VolumeTypesExtraSpecsV2Test(base.BaseVolumeAdminTest):
+class VolumeTypesExtraSpecsTest(base.BaseVolumeAdminTest):
@classmethod
def resource_setup(cls):
- super(VolumeTypesExtraSpecsV2Test, cls).resource_setup()
- vol_type_name = data_utils.rand_name('Volume-type')
- cls.volume_type = cls.volume_types_client.create_volume_type(
- name=vol_type_name)['volume_type']
+ super(VolumeTypesExtraSpecsTest, cls).resource_setup()
+ cls.volume_type = cls.create_volume_type()
- @classmethod
- def resource_cleanup(cls):
- cls.volume_types_client.delete_volume_type(cls.volume_type['id'])
- super(VolumeTypesExtraSpecsV2Test, cls).resource_cleanup()
-
- @test.idempotent_id('b42923e9-0452-4945-be5b-d362ae533e60')
+ @decorators.idempotent_id('b42923e9-0452-4945-be5b-d362ae533e60')
def test_volume_type_extra_specs_list(self):
# List Volume types extra specs.
extra_specs = {"spec1": "val1"}
- body = self.volume_types_client.create_volume_type_extra_specs(
+ body = self.admin_volume_types_client.create_volume_type_extra_specs(
self.volume_type['id'], extra_specs)['extra_specs']
self.assertEqual(extra_specs, body,
"Volume type extra spec incorrectly created")
- body = self.volume_types_client.list_volume_types_extra_specs(
+ body = self.admin_volume_types_client.list_volume_types_extra_specs(
self.volume_type['id'])['extra_specs']
self.assertIsInstance(body, dict)
self.assertIn('spec1', body)
- @test.idempotent_id('0806db36-b4a0-47a1-b6f3-c2e7f194d017')
+ @decorators.idempotent_id('0806db36-b4a0-47a1-b6f3-c2e7f194d017')
def test_volume_type_extra_specs_update(self):
# Update volume type extra specs
extra_specs = {"spec2": "val1"}
- body = self.volume_types_client.create_volume_type_extra_specs(
+ body = self.admin_volume_types_client.create_volume_type_extra_specs(
self.volume_type['id'], extra_specs)['extra_specs']
self.assertEqual(extra_specs, body,
"Volume type extra spec incorrectly created")
- extra_spec = {"spec2": "val2"}
- body = self.volume_types_client.update_volume_type_extra_specs(
- self.volume_type['id'],
- extra_spec.keys()[0],
- extra_spec)
- self.assertIn('spec2', body)
- self.assertEqual(extra_spec['spec2'], body['spec2'],
+ # Only update an extra spec
+ spec_key = "spec2"
+ extra_spec = {spec_key: "val2"}
+ body = self.admin_volume_types_client.update_volume_type_extra_specs(
+ self.volume_type['id'], spec_key, extra_spec)
+ self.assertIn(spec_key, body)
+ self.assertEqual(extra_spec[spec_key], body[spec_key])
+ body = self.admin_volume_types_client.show_volume_type_extra_specs(
+ self.volume_type['id'], spec_key)
+ self.assertIn(spec_key, body)
+ self.assertEqual(extra_spec[spec_key], body[spec_key],
"Volume type extra spec incorrectly updated")
- @test.idempotent_id('d4772798-601f-408a-b2a5-29e8a59d1220')
+ # Update an existing extra spec and create a new extra spec
+ extra_specs = {spec_key: "val3", "spec4": "val4"}
+ body = self.admin_volume_types_client.create_volume_type_extra_specs(
+ self.volume_type['id'], extra_specs)['extra_specs']
+ self.assertEqual(extra_specs, body)
+ body = self.admin_volume_types_client.list_volume_types_extra_specs(
+ self.volume_type['id'])['extra_specs']
+ for key in extra_specs:
+ self.assertIn(key, body)
+ self.assertEqual(extra_specs[key], body[key],
+ "Volume type extra spec incorrectly created")
+
+ @decorators.idempotent_id('d4772798-601f-408a-b2a5-29e8a59d1220')
def test_volume_type_extra_spec_create_get_delete(self):
# Create/Get/Delete volume type extra spec.
- extra_specs = {"spec3": "val1"}
- body = self.volume_types_client.create_volume_type_extra_specs(
+ spec_key = "spec3"
+ extra_specs = {spec_key: "val1"}
+ body = self.admin_volume_types_client.create_volume_type_extra_specs(
self.volume_type['id'],
extra_specs)['extra_specs']
self.assertEqual(extra_specs, body,
"Volume type extra spec incorrectly created")
- self.volume_types_client.show_volume_type_extra_specs(
+ body = self.admin_volume_types_client.show_volume_type_extra_specs(
self.volume_type['id'],
- extra_specs.keys()[0])
+ spec_key)
self.assertEqual(extra_specs, body,
"Volume type extra spec incorrectly fetched")
- self.volume_types_client.delete_volume_type_extra_specs(
- self.volume_type['id'],
- extra_specs.keys()[0])
-
-
-class VolumeTypesExtraSpecsV1Test(VolumeTypesExtraSpecsV2Test):
- _api_version = 1
+ self.admin_volume_types_client.delete_volume_type_extra_specs(
+ self.volume_type['id'], spec_key)
+ self.assertRaises(
+ lib_exc.NotFound,
+ self.admin_volume_types_client.show_volume_type_extra_specs,
+ self.volume_type['id'], spec_key)
diff --git a/tempest/api/volume/admin/test_volume_types_extra_specs_negative.py b/tempest/api/volume/admin/test_volume_types_extra_specs_negative.py
index 6483af3..fe249d6 100644
--- a/tempest/api/volume/admin/test_volume_types_extra_specs_negative.py
+++ b/tempest/api/volume/admin/test_volume_types_extra_specs_negative.py
@@ -13,132 +13,125 @@
# License for the specific language governing permissions and limitations
# under the License.
-import uuid
-
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.volume import base
-from tempest.common.utils import data_utils
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
-class ExtraSpecsNegativeV2Test(base.BaseVolumeAdminTest):
+class ExtraSpecsNegativeTest(base.BaseVolumeAdminTest):
@classmethod
def resource_setup(cls):
- super(ExtraSpecsNegativeV2Test, cls).resource_setup()
- vol_type_name = data_utils.rand_name('Volume-type')
- cls.extra_specs = {"spec1": "val1"}
- cls.volume_type = cls.volume_types_client.create_volume_type(
- name=vol_type_name,
- extra_specs=cls.extra_specs)['volume_type']
+ super(ExtraSpecsNegativeTest, cls).resource_setup()
+ extra_specs = {"spec1": "val1"}
+ cls.volume_type = cls.create_volume_type(extra_specs=extra_specs)
- @classmethod
- def resource_cleanup(cls):
- cls.volume_types_client.delete_volume_type(cls.volume_type['id'])
- super(ExtraSpecsNegativeV2Test, cls).resource_cleanup()
-
- @test.idempotent_id('08961d20-5cbb-4910-ac0f-89ad6dbb2da1')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('08961d20-5cbb-4910-ac0f-89ad6dbb2da1')
def test_update_no_body(self):
# Should not update volume type extra specs with no body
- extra_spec = {"spec1": "val2"}
self.assertRaises(
lib_exc.BadRequest,
- self.volume_types_client.update_volume_type_extra_specs,
- self.volume_type['id'], extra_spec.keys()[0], None)
+ self.admin_volume_types_client.update_volume_type_extra_specs,
+ self.volume_type['id'], "spec1", None)
- @test.idempotent_id('25e5a0ee-89b3-4c53-8310-236f76c75365')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('25e5a0ee-89b3-4c53-8310-236f76c75365')
def test_update_nonexistent_extra_spec_id(self):
# Should not update volume type extra specs with nonexistent id.
extra_spec = {"spec1": "val2"}
self.assertRaises(
lib_exc.BadRequest,
- self.volume_types_client.update_volume_type_extra_specs,
- self.volume_type['id'], str(uuid.uuid4()),
+ self.admin_volume_types_client.update_volume_type_extra_specs,
+ self.volume_type['id'], data_utils.rand_uuid(),
extra_spec)
- @test.idempotent_id('9bf7a657-b011-4aec-866d-81c496fbe5c8')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('9bf7a657-b011-4aec-866d-81c496fbe5c8')
def test_update_none_extra_spec_id(self):
# Should not update volume type extra specs with none id.
extra_spec = {"spec1": "val2"}
self.assertRaises(
lib_exc.BadRequest,
- self.volume_types_client.update_volume_type_extra_specs,
+ self.admin_volume_types_client.update_volume_type_extra_specs,
self.volume_type['id'], None, extra_spec)
- @test.idempotent_id('a77dfda2-9100-448e-9076-ed1711f4bdfc')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('a77dfda2-9100-448e-9076-ed1711f4bdfc')
def test_update_multiple_extra_spec(self):
# Should not update volume type extra specs with multiple specs as
- # body.
+ # body.
extra_spec = {"spec1": "val2", "spec2": "val1"}
self.assertRaises(
lib_exc.BadRequest,
- self.volume_types_client.update_volume_type_extra_specs,
- self.volume_type['id'], extra_spec.keys()[0],
+ self.admin_volume_types_client.update_volume_type_extra_specs,
+ self.volume_type['id'], list(extra_spec)[0],
extra_spec)
- @test.idempotent_id('49d5472c-a53d-4eab-a4d3-450c4db1c545')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('49d5472c-a53d-4eab-a4d3-450c4db1c545')
def test_create_nonexistent_type_id(self):
# Should not create volume type extra spec for nonexistent volume
- # type id.
+ # type id.
extra_specs = {"spec2": "val1"}
self.assertRaises(
lib_exc.NotFound,
- self.volume_types_client.create_volume_type_extra_specs,
- str(uuid.uuid4()), extra_specs)
+ self.admin_volume_types_client.create_volume_type_extra_specs,
+ data_utils.rand_uuid(), extra_specs)
- @test.idempotent_id('c821bdc8-43a4-4bf4-86c8-82f3858d5f7d')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('c821bdc8-43a4-4bf4-86c8-82f3858d5f7d')
def test_create_none_body(self):
# Should not create volume type extra spec for none POST body.
self.assertRaises(
lib_exc.BadRequest,
- self.volume_types_client.create_volume_type_extra_specs,
+ self.admin_volume_types_client.create_volume_type_extra_specs,
self.volume_type['id'], None)
- @test.idempotent_id('bc772c71-1ed4-4716-b945-8b5ed0f15e87')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('bc772c71-1ed4-4716-b945-8b5ed0f15e87')
def test_create_invalid_body(self):
# Should not create volume type extra spec for invalid POST body.
self.assertRaises(
lib_exc.BadRequest,
- self.volume_types_client.create_volume_type_extra_specs,
+ self.admin_volume_types_client.create_volume_type_extra_specs,
self.volume_type['id'], extra_specs=['invalid'])
- @test.idempotent_id('031cda8b-7d23-4246-8bf6-bbe73fd67074')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('031cda8b-7d23-4246-8bf6-bbe73fd67074')
def test_delete_nonexistent_volume_type_id(self):
# Should not delete volume type extra spec for nonexistent
- # type id.
- extra_specs = {"spec1": "val1"}
+ # type id.
self.assertRaises(
lib_exc.NotFound,
- self.volume_types_client.delete_volume_type_extra_specs,
- str(uuid.uuid4()), extra_specs.keys()[0])
+ self.admin_volume_types_client.delete_volume_type_extra_specs,
+ data_utils.rand_uuid(), "spec1")
- @test.idempotent_id('dee5cf0c-cdd6-4353-b70c-e847050d71fb')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('dee5cf0c-cdd6-4353-b70c-e847050d71fb')
def test_list_nonexistent_volume_type_id(self):
# Should not list volume type extra spec for nonexistent type id.
self.assertRaises(
lib_exc.NotFound,
- self.volume_types_client.list_volume_types_extra_specs,
- str(uuid.uuid4()))
+ self.admin_volume_types_client.list_volume_types_extra_specs,
+ data_utils.rand_uuid())
- @test.idempotent_id('9f402cbd-1838-4eb4-9554-126a6b1908c9')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('9f402cbd-1838-4eb4-9554-126a6b1908c9')
def test_get_nonexistent_volume_type_id(self):
# Should not get volume type extra spec for nonexistent type id.
- extra_specs = {"spec1": "val1"}
self.assertRaises(
lib_exc.NotFound,
- self.volume_types_client.show_volume_type_extra_specs,
- str(uuid.uuid4()), extra_specs.keys()[0])
+ self.admin_volume_types_client.show_volume_type_extra_specs,
+ data_utils.rand_uuid(), "spec1")
- @test.idempotent_id('c881797d-12ff-4f1a-b09d-9f6212159753')
- def test_get_nonexistent_extra_spec_id(self):
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('c881797d-12ff-4f1a-b09d-9f6212159753')
+ def test_get_nonexistent_extra_spec_name(self):
# Should not get volume type extra spec for nonexistent extra spec
- # id.
+ # name.
self.assertRaises(
lib_exc.NotFound,
- self.volume_types_client.show_volume_type_extra_specs,
- self.volume_type['id'], str(uuid.uuid4()))
-
-
-class ExtraSpecsNegativeV1Test(ExtraSpecsNegativeV2Test):
- _api_version = 1
+ self.admin_volume_types_client.show_volume_type_extra_specs,
+ self.volume_type['id'], "nonexistent_extra_spec_name")
diff --git a/tempest/api/volume/admin/test_volume_types_negative.py b/tempest/api/volume/admin/test_volume_types_negative.py
index bc32fc9..ae29049 100644
--- a/tempest/api/volume/admin/test_volume_types_negative.py
+++ b/tempest/api/volume/admin/test_volume_types_negative.py
@@ -13,45 +13,43 @@
# License for the specific language governing permissions and limitations
# under the License.
-import uuid
-
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.volume import base
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
-class VolumeTypesNegativeV2Test(base.BaseVolumeAdminTest):
+class VolumeTypesNegativeTest(base.BaseVolumeAdminTest):
- @test.idempotent_id('b48c98f2-e662-4885-9b71-032256906314')
- def test_create_with_nonexistent_volume_type(self):
- # Should not be able to create volume with nonexistent volume_type.
- self.name_field = self.special_fields['name_field']
- params = {self.name_field: str(uuid.uuid4()),
- 'volume_type': str(uuid.uuid4())}
- self.assertRaises(lib_exc.NotFound,
- self.volumes_client.create_volume, **params)
-
- @test.idempotent_id('878b4e57-faa2-4659-b0d1-ce740a06ae81')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('878b4e57-faa2-4659-b0d1-ce740a06ae81')
def test_create_with_empty_name(self):
# Should not be able to create volume type with an empty name.
- self.assertRaises(lib_exc.BadRequest,
- self.volume_types_client.create_volume_type, name='')
+ self.assertRaises(
+ lib_exc.BadRequest,
+ self.admin_volume_types_client.create_volume_type, name='')
- @test.idempotent_id('994610d6-0476-4018-a644-a2602ef5d4aa')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('994610d6-0476-4018-a644-a2602ef5d4aa')
def test_get_nonexistent_type_id(self):
# Should not be able to get volume type with nonexistent type id.
self.assertRaises(lib_exc.NotFound,
- self.volume_types_client.show_volume_type,
- str(uuid.uuid4()))
+ self.admin_volume_types_client.show_volume_type,
+ data_utils.rand_uuid())
- @test.idempotent_id('6b3926d2-7d73-4896-bc3d-e42dfd11a9f6')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('6b3926d2-7d73-4896-bc3d-e42dfd11a9f6')
def test_delete_nonexistent_type_id(self):
# Should not be able to delete volume type with nonexistent type id.
self.assertRaises(lib_exc.NotFound,
- self.volume_types_client.delete_volume_type,
- str(uuid.uuid4()))
+ self.admin_volume_types_client.delete_volume_type,
+ data_utils.rand_uuid())
-
-class VolumeTypesNegativeV1Test(VolumeTypesNegativeV2Test):
- _api_version = 1
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('8c09f849-f225-4d78-ba87-bffd9a5e0c6f')
+ def test_create_volume_with_private_volume_type(self):
+ # Should not be able to create volume with private volume type.
+ params = {'os-volume-type-access:is_public': False}
+ volume_type = self.create_volume_type(**params)
+ self.assertRaises(lib_exc.NotFound,
+ self.create_volume, volume_type=volume_type['id'])
diff --git a/tempest/api/volume/admin/test_volumes_actions.py b/tempest/api/volume/admin/test_volumes_actions.py
index 6c32321..3e0deef 100644
--- a/tempest/api/volume/admin/test_volumes_actions.py
+++ b/tempest/api/volume/admin/test_volumes_actions.py
@@ -14,88 +14,92 @@
# under the License.
from tempest.api.volume import base
-from tempest.common.utils import data_utils as utils
-from tempest import test
+from tempest.common import utils
+from tempest.common import waiters
+from tempest import config
+from tempest.lib import decorators
+
+CONF = config.CONF
-class VolumesActionsV2Test(base.BaseVolumeAdminTest):
-
- @classmethod
- def setup_clients(cls):
- super(VolumesActionsV2Test, cls).setup_clients()
- cls.client = cls.volumes_client
-
- @classmethod
- def resource_setup(cls):
- super(VolumesActionsV2Test, cls).resource_setup()
-
- # Create a test shared volume for tests
- vol_name = utils.rand_name(cls.__name__ + '-Volume')
- cls.name_field = cls.special_fields['name_field']
- params = {cls.name_field: vol_name}
-
- cls.volume = cls.client.create_volume(**params)['volume']
- cls.client.wait_for_volume_status(cls.volume['id'], 'available')
-
- @classmethod
- def resource_cleanup(cls):
- # Delete the test volume
- cls.client.delete_volume(cls.volume['id'])
- cls.client.wait_for_resource_deletion(cls.volume['id'])
-
- super(VolumesActionsV2Test, cls).resource_cleanup()
-
- def _reset_volume_status(self, volume_id, status):
- # Reset the volume status
- body = self.admin_volume_client.reset_volume_status(volume_id,
- status)
- return body
-
- def tearDown(self):
- # Set volume's status to available after test
- self._reset_volume_status(self.volume['id'], 'available')
- super(VolumesActionsV2Test, self).tearDown()
-
- def _create_temp_volume(self):
- # Create a temp volume for force delete tests
- vol_name = utils.rand_name('Volume')
- params = {self.name_field: vol_name}
- temp_volume = self.client.create_volume(**params)['volume']
- self.client.wait_for_volume_status(temp_volume['id'], 'available')
-
- return temp_volume
+class VolumesActionsTest(base.BaseVolumeAdminTest):
def _create_reset_and_force_delete_temp_volume(self, status=None):
# Create volume, reset volume status, and force delete temp volume
- temp_volume = self._create_temp_volume()
+ temp_volume = self.create_volume()
if status:
- self._reset_volume_status(temp_volume['id'], status)
+ self.admin_volume_client.reset_volume_status(
+ temp_volume['id'], status=status)
+ waiters.wait_for_volume_resource_status(
+ self.volumes_client, temp_volume['id'], status)
self.admin_volume_client.force_delete_volume(temp_volume['id'])
- self.client.wait_for_resource_deletion(temp_volume['id'])
+ self.volumes_client.wait_for_resource_deletion(temp_volume['id'])
- @test.idempotent_id('d063f96e-a2e0-4f34-8b8a-395c42de1845')
+ @decorators.idempotent_id('d063f96e-a2e0-4f34-8b8a-395c42de1845')
def test_volume_reset_status(self):
- # test volume reset status : available->error->available
- self._reset_volume_status(self.volume['id'], 'error')
- volume_get = self.admin_volume_client.show_volume(
- self.volume['id'])['volume']
- self.assertEqual('error', volume_get['status'])
+ # test volume reset status : available->error->available->maintenance
+ volume = self.create_volume()
+ self.addCleanup(waiters.wait_for_volume_resource_status,
+ self.volumes_client, volume['id'], 'available')
+ self.addCleanup(self.admin_volume_client.reset_volume_status,
+ volume['id'], status='available')
+ for status in ['error', 'available', 'maintenance']:
+ self.admin_volume_client.reset_volume_status(
+ volume['id'], status=status)
+ waiters.wait_for_volume_resource_status(
+ self.volumes_client, volume['id'], status)
- @test.idempotent_id('21737d5a-92f2-46d7-b009-a0cc0ee7a570')
+ @decorators.idempotent_id('21737d5a-92f2-46d7-b009-a0cc0ee7a570')
def test_volume_force_delete_when_volume_is_creating(self):
# test force delete when status of volume is creating
self._create_reset_and_force_delete_temp_volume('creating')
- @test.idempotent_id('db8d607a-aa2e-4beb-b51d-d4005c232011')
+ @decorators.idempotent_id('db8d607a-aa2e-4beb-b51d-d4005c232011')
def test_volume_force_delete_when_volume_is_attaching(self):
# test force delete when status of volume is attaching
self._create_reset_and_force_delete_temp_volume('attaching')
- @test.idempotent_id('3e33a8a8-afd4-4d64-a86b-c27a185c5a4a')
+ @decorators.idempotent_id('3e33a8a8-afd4-4d64-a86b-c27a185c5a4a')
def test_volume_force_delete_when_volume_is_error(self):
# test force delete when status of volume is error
self._create_reset_and_force_delete_temp_volume('error')
+ @decorators.idempotent_id('b957cabd-1486-4e21-90cf-a9ed3c39dfb2')
+ def test_volume_force_delete_when_volume_is_maintenance(self):
+ # test force delete when status of volume is maintenance
+ self._create_reset_and_force_delete_temp_volume('maintenance')
-class VolumesActionsV1Test(VolumesActionsV2Test):
- _api_version = 1
+ @decorators.idempotent_id('d38285d9-929d-478f-96a5-00e66a115b81')
+ @utils.services('compute')
+ def test_force_detach_volume(self):
+ # Create a server and a volume
+ server_id = self.create_server()['id']
+ volume_id = self.create_volume()['id']
+
+ # Attach volume
+ self.volumes_client.attach_volume(
+ volume_id,
+ instance_uuid=server_id,
+ mountpoint='/dev/%s' % CONF.compute.volume_device_name)
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume_id, 'in-use')
+ self.addCleanup(waiters.wait_for_volume_resource_status,
+ self.volumes_client, volume_id, 'available')
+ self.addCleanup(self.volumes_client.detach_volume, volume_id)
+ attachment = self.volumes_client.show_volume(
+ volume_id)['volume']['attachments'][0]
+
+ # Reset volume's status to error
+ self.admin_volume_client.reset_volume_status(volume_id, status='error')
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume_id, 'error')
+
+ # Force detach volume
+ self.admin_volume_client.force_detach_volume(
+ volume_id, connector=None,
+ attachment_id=attachment['attachment_id'])
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume_id, 'available')
+ vol_info = self.volumes_client.show_volume(volume_id)['volume']
+ self.assertIn('attachments', vol_info)
+ self.assertEmpty(vol_info['attachments'])
diff --git a/tempest/api/volume/admin/test_volumes_backup.py b/tempest/api/volume/admin/test_volumes_backup.py
index ed34a9b..45060d0 100644
--- a/tempest/api/volume/admin/test_volumes_backup.py
+++ b/tempest/api/volume/admin/test_volumes_backup.py
@@ -13,87 +13,59 @@
# License for the specific language governing permissions and limitations
# under the License.
-from oslo_log import log as logging
-from tempest_lib import decorators
+from oslo_serialization import base64
+from oslo_serialization import jsonutils as json
from tempest.api.volume import base
-from tempest.common.utils import data_utils
+from tempest.common import waiters
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
CONF = config.CONF
-LOG = logging.getLogger(__name__)
-class VolumesBackupsV2Test(base.BaseVolumeAdminTest):
+class VolumesBackupsAdminTest(base.BaseVolumeAdminTest):
@classmethod
def skip_checks(cls):
- super(VolumesBackupsV2Test, cls).skip_checks()
+ super(VolumesBackupsAdminTest, cls).skip_checks()
if not CONF.volume_feature_enabled.backup:
raise cls.skipException("Cinder backup feature disabled")
- @classmethod
- def resource_setup(cls):
- super(VolumesBackupsV2Test, cls).resource_setup()
-
- cls.volume = cls.create_volume()
-
def _delete_backup(self, backup_id):
- self.backups_adm_client.delete_backup(backup_id)
- self.backups_adm_client.wait_for_backup_deletion(backup_id)
+ self.admin_backups_client.delete_backup(backup_id)
+ self.admin_backups_client.wait_for_resource_deletion(backup_id)
- @test.idempotent_id('a66eb488-8ee1-47d4-8e9f-575a095728c6')
- def test_volume_backup_create_get_detailed_list_restore_delete(self):
- # Create backup
- backup_name = data_utils.rand_name('Backup')
- create_backup = self.backups_adm_client.create_backup
- backup = create_backup(volume_id=self.volume['id'],
- name=backup_name)['backup']
- self.addCleanup(self.backups_adm_client.delete_backup,
- backup['id'])
- self.assertEqual(backup_name, backup['name'])
- self.admin_volume_client.wait_for_volume_status(
- self.volume['id'], 'available')
- self.backups_adm_client.wait_for_backup_status(backup['id'],
- 'available')
+ def _decode_url(self, backup_url):
+ return json.loads(base64.decode_as_text(backup_url))
- # Get a given backup
- backup = self.backups_adm_client.show_backup(backup['id'])['backup']
- self.assertEqual(backup_name, backup['name'])
+ def _encode_backup(self, backup):
+ retval = json.dumps(backup)
+ return base64.encode_as_text(retval)
- # Get all backups with detail
- backups = self.backups_adm_client.list_backups(detail=True)['backups']
- self.assertIn((backup['name'], backup['id']),
- [(m['name'], m['id']) for m in backups])
+ def _modify_backup_url(self, backup_url, changes):
+ backup = self._decode_url(backup_url)
+ backup.update(changes)
+ return self._encode_backup(backup)
- # Restore backup
- restore = self.backups_adm_client.restore_backup(
- backup['id'])['restore']
-
- # Delete backup
- self.addCleanup(self.admin_volume_client.delete_volume,
- restore['volume_id'])
- self.assertEqual(backup['id'], restore['backup_id'])
- self.backups_adm_client.wait_for_backup_status(backup['id'],
- 'available')
- self.admin_volume_client.wait_for_volume_status(
- restore['volume_id'], 'available')
-
- @decorators.skip_because(bug='1455043')
- @test.idempotent_id('a99c54a1-dd80-4724-8a13-13bf58d4068d')
+ @decorators.idempotent_id('a99c54a1-dd80-4724-8a13-13bf58d4068d')
def test_volume_backup_export_import(self):
+ """Test backup export import functionality.
+
+ Cinder allows exporting DB backup information through its API so it can
+ be imported back in case of a DB loss.
+ """
+ volume = self.create_volume()
# Create backup
- backup_name = data_utils.rand_name('Backup')
- backup = (self.backups_adm_client.create_backup(
- volume_id=self.volume['id'], name=backup_name)['backup'])
- self.addCleanup(self._delete_backup, backup['id'])
+ backup_name = data_utils.rand_name(self.__class__.__name__ + '-Backup')
+ backup = self.create_backup(volume_id=volume['id'], name=backup_name)
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume['id'], 'available')
self.assertEqual(backup_name, backup['name'])
- self.backups_adm_client.wait_for_backup_status(backup['id'],
- 'available')
# Export Backup
- export_backup = (self.backups_adm_client.export_backup(backup['id'])
+ export_backup = (self.admin_backups_client.export_backup(backup['id'])
['backup-record'])
self.assertIn('backup_service', export_backup)
self.assertIn('backup_url', export_backup)
@@ -101,34 +73,66 @@
'cinder.backup.drivers'))
self.assertIsNotNone(export_backup['backup_url'])
+ # NOTE(geguileo): Backups are imported with the same backup id
+ # (important for incremental backups among other things), so we cannot
+ # import the exported backup information as it is, because that Backup
+ # ID already exists. So we'll fake the data by changing the backup id
+ # in the exported backup DB info we have retrieved before importing it
+ # back.
+ new_id = data_utils.rand_uuid()
+ new_url = self._modify_backup_url(
+ export_backup['backup_url'], {'id': new_id})
+
# Import Backup
- import_backup = self.backups_adm_client.import_backup(
+ import_backup = self.admin_backups_client.import_backup(
backup_service=export_backup['backup_service'],
- backup_url=export_backup['backup_url'])['backup']
- self.addCleanup(self._delete_backup, import_backup['id'])
+ backup_url=new_url)['backup']
+
+ # NOTE(geguileo): We delete both backups, but only one of those
+ # deletions will delete data from the backup back-end because they
+ # were both pointing to the same backend data.
+ self.addCleanup(self._delete_backup, new_id)
self.assertIn("id", import_backup)
- self.backups_adm_client.wait_for_backup_status(import_backup['id'],
- 'available')
+ self.assertEqual(new_id, import_backup['id'])
+ waiters.wait_for_volume_resource_status(self.admin_backups_client,
+ import_backup['id'],
+ 'available')
# Verify Import Backup
- backups = self.backups_adm_client.list_backups(detail=True)['backups']
- self.assertIn(import_backup['id'], [b['id'] for b in backups])
+ backups = self.admin_backups_client.list_backups()['backups']
+ self.assertIn(new_id, [b['id'] for b in backups])
# Restore backup
- restore = (self.backups_adm_client.restore_backup(import_backup['id'])
- ['restore'])
- self.addCleanup(self.admin_volume_client.delete_volume,
+ restore = self.backups_client.restore_backup(backup['id'])['restore']
+ self.addCleanup(self.volumes_client.delete_volume,
restore['volume_id'])
- self.assertEqual(import_backup['id'], restore['backup_id'])
- self.admin_volume_client.wait_for_volume_status(restore['volume_id'],
- 'available')
+ self.assertEqual(backup['id'], restore['backup_id'])
+
+ # When restore operation is performed then, backup['id']
+ # goes to 'restoring' state so we need to wait for
+ # backup['id'] to become 'available'.
+ 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')
# Verify if restored volume is there in volume list
- volumes = self.admin_volume_client.list_volumes()['volumes']
+ volumes = self.volumes_client.list_volumes()['volumes']
self.assertIn(restore['volume_id'], [v['id'] for v in volumes])
- self.backups_adm_client.wait_for_backup_status(import_backup['id'],
- 'available')
-
-class VolumesBackupsV1Test(VolumesBackupsV2Test):
- _api_version = 1
+ @decorators.idempotent_id('47a35425-a891-4e13-961c-c45deea21e94')
+ def test_volume_backup_reset_status(self):
+ # Create a volume
+ volume = self.create_volume()
+ # Create a backup
+ backup_name = data_utils.rand_name(
+ self.__class__.__name__ + '-Backup')
+ backup = self.create_backup(volume_id=volume['id'], name=backup_name)
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume['id'], 'available')
+ self.assertEqual(backup_name, backup['name'])
+ # Reset backup status to error
+ self.admin_backups_client.reset_backup_status(backup_id=backup['id'],
+ status="error")
+ waiters.wait_for_volume_resource_status(self.backups_client,
+ backup['id'], 'error')
diff --git a/tempest/api/volume/admin/test_volumes_list.py b/tempest/api/volume/admin/test_volumes_list.py
new file mode 100644
index 0000000..6ce4a85
--- /dev/null
+++ b/tempest/api/volume/admin/test_volumes_list.py
@@ -0,0 +1,66 @@
+# 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.
+
+import operator
+
+from tempest.api.volume import base
+from tempest.common import waiters
+from tempest import config
+from tempest.lib import decorators
+
+CONF = config.CONF
+
+
+class VolumesListAdminTestJSON(base.BaseVolumeAdminTest):
+
+ @classmethod
+ def resource_setup(cls):
+ super(VolumesListAdminTestJSON, cls).resource_setup()
+ # Create 3 test volumes
+ # NOTE(zhufl): When using pre-provisioned credentials, the project
+ # may have volumes other than those created below.
+ cls.volume_list = cls.volumes_client.list_volumes()['volumes']
+ for _ in range(3):
+ volume = cls.create_volume()
+ # Fetch volume details
+ volume_details = cls.volumes_client.show_volume(
+ volume['id'])['volume']
+ cls.volume_list.append(volume_details)
+
+ @decorators.idempotent_id('5866286f-3290-4cfd-a414-088aa6cdc469')
+ def test_volume_list_param_tenant(self):
+ # Test to list volumes from single tenant
+ # Create a volume in admin tenant
+ adm_vol = self.admin_volume_client.create_volume(
+ size=CONF.volume.volume_size)['volume']
+ self.addCleanup(self.admin_volume_client.delete_volume, adm_vol['id'])
+ waiters.wait_for_volume_resource_status(self.admin_volume_client,
+ adm_vol['id'], 'available')
+ params = {'all_tenants': 1,
+ 'project_id': self.volumes_client.tenant_id}
+ # Getting volume list from primary tenant using admin credentials
+ fetched_list = self.admin_volume_client.list_volumes(
+ detail=True, params=params)['volumes']
+ # Verifying fetched volume ids list is related to primary tenant
+ fetched_list_ids = map(operator.itemgetter('id'), fetched_list)
+ expected_list_ids = map(operator.itemgetter('id'), self.volume_list)
+ self.assertEqual(sorted(expected_list_ids), sorted(fetched_list_ids))
+ # Verifying tenant id of volumes fetched list is related to
+ # primary tenant
+ fetched_tenant_id = [operator.itemgetter(
+ 'os-vol-tenant-attr:tenant_id')(item) for item in fetched_list]
+ expected_tenant_id = [self.volumes_client.tenant_id] * \
+ len(self.volume_list)
+ self.assertEqual(expected_tenant_id, fetched_tenant_id)
diff --git a/tempest/api/volume/api_microversion_fixture.py b/tempest/api/volume/api_microversion_fixture.py
new file mode 100644
index 0000000..7bbe674
--- /dev/null
+++ b/tempest/api/volume/api_microversion_fixture.py
@@ -0,0 +1,30 @@
+#
+# 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 fixtures
+
+from tempest.lib.services.volume import base_client
+
+
+class APIMicroversionFixture(fixtures.Fixture):
+
+ def __init__(self, volume_microversion):
+ self.volume_microversion = volume_microversion
+
+ def _setUp(self):
+ super(APIMicroversionFixture, self)._setUp()
+ base_client.VOLUME_MICROVERSION = self.volume_microversion
+ self.addCleanup(self._reset_volume_microversion)
+
+ def _reset_volume_microversion(self):
+ base_client.VOLUME_MICROVERSION = None
diff --git a/tempest/api/volume/base.py b/tempest/api/volume/base.py
index 5c4d0e1..81fd6e6 100644
--- a/tempest/api/volume/base.py
+++ b/tempest/api/volume/base.py
@@ -13,24 +13,29 @@
# License for the specific language governing permissions and limitations
# under the License.
-from oslo_log import log as logging
-from tempest_lib import exceptions as lib_exc
-
+from tempest.api.volume import api_microversion_fixture
from tempest.common import compute
-from tempest.common.utils import data_utils
+from tempest.common import waiters
from tempest import config
-from tempest import exceptions
+from tempest.lib.common import api_version_utils
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import exceptions
import tempest.test
CONF = config.CONF
-LOG = logging.getLogger(__name__)
-
-class BaseVolumeTest(tempest.test.BaseTestCase):
+class BaseVolumeTest(api_version_utils.BaseMicroversionTest,
+ tempest.test.BaseTestCase):
"""Base test case class for all Cinder API tests."""
_api_version = 2
+ # if api_v2 is not enabled while api_v3 is enabled, the volume v2 classes
+ # should be transferred to volume v3 classes.
+ if (not CONF.volume_feature_enabled.api_v2 and
+ CONF.volume_feature_enabled.api_v3):
+ _api_version = 3
credentials = ['primary']
@classmethod
@@ -40,17 +45,21 @@
if not CONF.service_available.cinder:
skip_msg = ("%s skipped as Cinder is not available" % cls.__name__)
raise cls.skipException(skip_msg)
- if cls._api_version == 1:
- if not CONF.volume_feature_enabled.api_v1:
- msg = "Volume API v1 is disabled"
- raise cls.skipException(msg)
- elif cls._api_version == 2:
+ if cls._api_version == 2:
if not CONF.volume_feature_enabled.api_v2:
msg = "Volume API v2 is disabled"
raise cls.skipException(msg)
+ elif cls._api_version == 3:
+ if not CONF.volume_feature_enabled.api_v3:
+ msg = "Volume API v3 is disabled"
+ raise cls.skipException(msg)
else:
msg = ("Invalid Cinder API version (%s)" % cls._api_version)
- raise exceptions.InvalidConfiguration(message=msg)
+ raise exceptions.InvalidConfiguration(msg)
+
+ api_version_utils.check_skip_with_microversion(
+ cls.min_microversion, cls.max_microversion,
+ CONF.volume.min_microversion, CONF.volume.max_microversion)
@classmethod
def setup_credentials(cls):
@@ -60,116 +69,174 @@
@classmethod
def setup_clients(cls):
super(BaseVolumeTest, cls).setup_clients()
- cls.servers_client = cls.os.servers_client
- cls.compute_networks_client = cls.os.compute_networks_client
- cls.images_client = cls.os.images_client
+ cls.servers_client = cls.os_primary.servers_client
- if cls._api_version == 1:
- cls.snapshots_client = cls.os.snapshots_client
- cls.volumes_client = cls.os.volumes_client
- cls.backups_client = cls.os.backups_client
- cls.volume_services_client = cls.os.volume_services_client
- cls.volumes_extension_client = cls.os.volumes_extension_client
- cls.availability_zone_client = (
- cls.os.volume_availability_zone_client)
+ if CONF.service_available.glance:
+ cls.images_client = cls.os_primary.image_client_v2
+
+ if cls._api_version == 3:
+ cls.backups_client = cls.os_primary.backups_v3_client
+ cls.volumes_client = cls.os_primary.volumes_v3_client
+ cls.messages_client = cls.os_primary.volume_v3_messages_client
+ cls.versions_client = cls.os_primary.volume_v3_versions_client
+ cls.groups_client = cls.os_primary.groups_v3_client
+ cls.group_snapshots_client = (
+ cls.os_primary.group_snapshots_v3_client)
else:
- cls.snapshots_client = cls.os.snapshots_v2_client
- cls.volumes_client = cls.os.volumes_v2_client
- cls.volumes_extension_client = cls.os.volumes_v2_extension_client
- cls.availability_zone_client = (
- cls.os.volume_v2_availability_zone_client)
+ cls.backups_client = cls.os_primary.backups_v2_client
+ cls.volumes_client = cls.os_primary.volumes_v2_client
+
+ cls.snapshots_client = cls.os_primary.snapshots_v2_client
+ cls.volumes_extension_client =\
+ cls.os_primary.volumes_v2_extension_client
+ cls.availability_zone_client = (
+ cls.os_primary.volume_v2_availability_zone_client)
+ cls.volume_limits_client = cls.os_primary.volume_v2_limits_client
+
+ def setUp(self):
+ super(BaseVolumeTest, self).setUp()
+ self.useFixture(api_microversion_fixture.APIMicroversionFixture(
+ self.request_microversion))
@classmethod
def resource_setup(cls):
super(BaseVolumeTest, cls).resource_setup()
+ cls.request_microversion = (
+ api_version_utils.select_request_microversion(
+ cls.min_microversion,
+ CONF.volume.min_microversion))
- cls.snapshots = []
- cls.volumes = []
cls.image_ref = CONF.compute.image_ref
cls.flavor_ref = CONF.compute.flavor_ref
cls.build_interval = CONF.volume.build_interval
cls.build_timeout = CONF.volume.build_timeout
- if cls._api_version == 1:
- # Special fields and resp code for cinder v1
- cls.special_fields = {'name_field': 'display_name',
- 'descrip_field': 'display_description'}
- else:
- # Special fields and resp code for cinder v2
- cls.special_fields = {'name_field': 'name',
- 'descrip_field': 'description'}
-
@classmethod
- def resource_cleanup(cls):
- cls.clear_snapshots()
- cls.clear_volumes()
- super(BaseVolumeTest, cls).resource_cleanup()
+ def create_volume(cls, wait_until='available', **kwargs):
+ """Wrapper utility that returns a test volume.
- @classmethod
- def create_volume(cls, size=None, **kwargs):
- """Wrapper utility that returns a test volume."""
- name = data_utils.rand_name('Volume')
+ :param wait_until: wait till volume status.
+ """
+ if 'size' not in kwargs:
+ kwargs['size'] = CONF.volume.volume_size
- name_field = cls.special_fields['name_field']
+ if 'imageRef' in kwargs:
+ image = cls.images_client.show_image(kwargs['imageRef'])
+ min_disk = image['min_disk']
+ kwargs['size'] = max(kwargs['size'], min_disk)
- kwargs[name_field] = name
- volume = cls.volumes_client.create_volume(size, **kwargs)['volume']
+ if 'name' not in kwargs:
+ name = data_utils.rand_name(cls.__name__ + '-Volume')
+ kwargs['name'] = name
- cls.volumes.append(volume)
- cls.volumes_client.wait_for_volume_status(volume['id'], 'available')
+ volume = cls.volumes_client.create_volume(**kwargs)['volume']
+ cls.addClassResourceCleanup(test_utils.call_and_ignore_notfound_exc,
+ cls.delete_volume, cls.volumes_client,
+ volume['id'])
+ waiters.wait_for_volume_resource_status(cls.volumes_client,
+ volume['id'], wait_until)
return volume
@classmethod
def create_snapshot(cls, volume_id=1, **kwargs):
"""Wrapper utility that returns a test snapshot."""
+ if 'name' not in kwargs:
+ name = data_utils.rand_name(cls.__name__ + '-Snapshot')
+ kwargs['name'] = name
+
snapshot = cls.snapshots_client.create_snapshot(
- volume_id, **kwargs)['snapshot']
- cls.snapshots.append(snapshot)
- cls.snapshots_client.wait_for_snapshot_status(snapshot['id'],
- 'available')
+ volume_id=volume_id, **kwargs)['snapshot']
+ cls.addClassResourceCleanup(test_utils.call_and_ignore_notfound_exc,
+ cls.delete_snapshot, snapshot['id'])
+ waiters.wait_for_volume_resource_status(cls.snapshots_client,
+ snapshot['id'], 'available')
return snapshot
+ def create_backup(self, volume_id, backup_client=None, **kwargs):
+ """Wrapper utility that returns a test backup."""
+ if backup_client is None:
+ backup_client = self.backups_client
+ if 'name' not in kwargs:
+ name = data_utils.rand_name(self.__class__.__name__ + '-Backup')
+ kwargs['name'] = name
+
+ backup = backup_client.create_backup(
+ volume_id=volume_id, **kwargs)['backup']
+ self.addCleanup(backup_client.delete_backup, backup['id'])
+ waiters.wait_for_volume_resource_status(backup_client, backup['id'],
+ 'available')
+ return backup
+
# NOTE(afazekas): these create_* and clean_* could be defined
# only in a single location in the source, and could be more general.
- @classmethod
- def clear_volumes(cls):
- for volume in cls.volumes:
- try:
- cls.volumes_client.delete_volume(volume['id'])
- except Exception:
- pass
-
- for volume in cls.volumes:
- try:
- cls.volumes_client.wait_for_resource_deletion(volume['id'])
- except Exception:
- pass
+ @staticmethod
+ def delete_volume(client, volume_id):
+ """Delete volume by the given client"""
+ client.delete_volume(volume_id)
+ client.wait_for_resource_deletion(volume_id)
@classmethod
- def clear_snapshots(cls):
- for snapshot in cls.snapshots:
- try:
- cls.snapshots_client.delete_snapshot(snapshot['id'])
- except Exception:
- pass
+ def delete_snapshot(cls, snapshot_id, snapshots_client=None):
+ """Delete snapshot by the given client"""
+ if snapshots_client is None:
+ snapshots_client = cls.snapshots_client
+ snapshots_client.delete_snapshot(snapshot_id)
+ snapshots_client.wait_for_resource_deletion(snapshot_id)
- for snapshot in cls.snapshots:
- try:
- cls.snapshots_client.wait_for_resource_deletion(snapshot['id'])
- except Exception:
- pass
+ def attach_volume(self, server_id, volume_id):
+ """Attach a volume to a server"""
+ self.servers_client.attach_volume(
+ server_id, volumeId=volume_id,
+ device='/dev/%s' % CONF.compute.volume_device_name)
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume_id, 'in-use')
+ self.addCleanup(waiters.wait_for_volume_resource_status,
+ self.volumes_client, volume_id, 'available')
+ self.addCleanup(self.servers_client.detach_volume, server_id,
+ volume_id)
- @classmethod
- def create_server(cls, name, **kwargs):
- tenant_network = cls.get_tenant_network()
+ def create_server(self, wait_until='ACTIVE', **kwargs):
+ name = kwargs.pop(
+ 'name',
+ data_utils.rand_name(self.__class__.__name__ + '-instance'))
+
+ tenant_network = self.get_tenant_network()
body, _ = compute.create_test_server(
- cls.os,
+ self.os_primary,
tenant_network=tenant_network,
name=name,
+ wait_until=wait_until,
**kwargs)
+
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ waiters.wait_for_server_termination,
+ self.servers_client, body['id'])
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.servers_client.delete_server, body['id'])
return body
+ def create_group(self, **kwargs):
+ if 'name' not in kwargs:
+ kwargs['name'] = data_utils.rand_name(
+ self.__class__.__name__ + '-Group')
+
+ group = self.groups_client.create_group(**kwargs)['group']
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.delete_group, group['id'])
+ waiters.wait_for_volume_resource_status(
+ self.groups_client, group['id'], 'available')
+ return group
+
+ def delete_group(self, group_id, delete_volumes=True):
+ self.groups_client.delete_group(group_id, delete_volumes)
+ if delete_volumes:
+ vols = self.volumes_client.list_volumes(detail=True)['volumes']
+ for vol in vols:
+ if vol['group_id'] == group_id:
+ self.volumes_client.wait_for_resource_deletion(vol['id'])
+ self.groups_client.wait_for_resource_deletion(group_id)
+
class BaseVolumeAdminTest(BaseVolumeTest):
"""Base test case class for all Volume Admin API tests."""
@@ -180,60 +247,77 @@
def setup_clients(cls):
super(BaseVolumeAdminTest, cls).setup_clients()
- if cls._api_version == 1:
- cls.volume_qos_client = cls.os_adm.volume_qos_client
- cls.admin_volume_services_client = \
- cls.os_adm.volume_services_client
- cls.volume_types_client = cls.os_adm.volume_types_client
- cls.admin_volume_client = cls.os_adm.volumes_client
- cls.hosts_client = cls.os_adm.volume_hosts_client
- cls.admin_snapshots_client = cls.os_adm.snapshots_client
- cls.backups_adm_client = cls.os_adm.backups_client
- cls.quotas_client = cls.os_adm.volume_quotas_client
- elif cls._api_version == 2:
- cls.volume_qos_client = cls.os_adm.volume_qos_v2_client
- cls.admin_volume_services_client = \
- cls.os_adm.volume_services_v2_client
- cls.volume_types_client = cls.os_adm.volume_types_v2_client
- cls.admin_volume_client = cls.os_adm.volumes_v2_client
- cls.hosts_client = cls.os_adm.volume_hosts_v2_client
- cls.admin_snapshots_client = cls.os_adm.snapshots_v2_client
- cls.backups_adm_client = cls.os_adm.backups_v2_client
- cls.quotas_client = cls.os_adm.volume_quotas_v2_client
-
- @classmethod
- def resource_setup(cls):
- super(BaseVolumeAdminTest, cls).resource_setup()
-
- cls.qos_specs = []
-
- @classmethod
- def resource_cleanup(cls):
- cls.clear_qos_specs()
- super(BaseVolumeAdminTest, cls).resource_cleanup()
+ cls.admin_volume_qos_client = cls.os_admin.volume_qos_v2_client
+ cls.admin_volume_services_client = \
+ cls.os_admin.volume_services_v2_client
+ cls.admin_volume_types_client = cls.os_admin.volume_types_v2_client
+ cls.admin_volume_manage_client = cls.os_admin.volume_manage_v2_client
+ cls.admin_volume_client = cls.os_admin.volumes_v2_client
+ if cls._api_version == 3:
+ cls.admin_volume_client = cls.os_admin.volumes_v3_client
+ cls.admin_groups_client = cls.os_admin.groups_v3_client
+ cls.admin_messages_client = cls.os_admin.volume_v3_messages_client
+ cls.admin_group_snapshots_client = \
+ cls.os_admin.group_snapshots_v3_client
+ cls.admin_group_types_client = cls.os_admin.group_types_v3_client
+ cls.admin_hosts_client = cls.os_admin.volume_hosts_v2_client
+ cls.admin_snapshot_manage_client = \
+ cls.os_admin.snapshot_manage_v2_client
+ cls.admin_snapshots_client = cls.os_admin.snapshots_v2_client
+ cls.admin_backups_client = cls.os_admin.backups_v2_client
+ cls.admin_encryption_types_client = \
+ cls.os_admin.encryption_types_v2_client
+ cls.admin_quota_classes_client = \
+ cls.os_admin.volume_quota_classes_v2_client
+ cls.admin_quotas_client = cls.os_admin.volume_quotas_v2_client
+ cls.admin_volume_limits_client = cls.os_admin.volume_v2_limits_client
+ cls.admin_capabilities_client = \
+ cls.os_admin.volume_capabilities_v2_client
+ cls.admin_scheduler_stats_client = \
+ cls.os_admin.volume_scheduler_stats_v2_client
@classmethod
def create_test_qos_specs(cls, name=None, consumer=None, **kwargs):
"""create a test Qos-Specs."""
name = name or data_utils.rand_name(cls.__name__ + '-QoS')
consumer = consumer or 'front-end'
- qos_specs = cls.volume_qos_client.create_qos(
+ qos_specs = cls.admin_volume_qos_client.create_qos(
name=name, consumer=consumer, **kwargs)['qos_specs']
- cls.qos_specs.append(qos_specs['id'])
+ cls.addClassResourceCleanup(cls.clear_qos_spec, qos_specs['id'])
return qos_specs
@classmethod
- def clear_qos_specs(cls):
- for qos_id in cls.qos_specs:
- try:
- cls.volume_qos_client.delete_qos(qos_id)
- except lib_exc.NotFound:
- # The qos_specs may have already been deleted which is OK.
- pass
+ def create_volume_type(cls, name=None, **kwargs):
+ """Create a test volume-type"""
+ name = name or data_utils.rand_name(cls.__name__ + '-volume-type')
+ volume_type = cls.admin_volume_types_client.create_volume_type(
+ name=name, **kwargs)['volume_type']
+ cls.addClassResourceCleanup(cls.clear_volume_type, volume_type['id'])
+ return volume_type
- for qos_id in cls.qos_specs:
- try:
- cls.volume_qos_client.wait_for_resource_deletion(qos_id)
- except lib_exc.NotFound:
- # The qos_specs may have already been deleted which is OK.
- pass
+ def create_group_type(self, name=None, **kwargs):
+ """Create a test group-type"""
+ name = name or data_utils.rand_name(
+ self.__class__.__name__ + '-group-type')
+ group_type = self.admin_group_types_client.create_group_type(
+ name=name, **kwargs)['group_type']
+ self.addCleanup(self.admin_group_types_client.delete_group_type,
+ group_type['id'])
+ return group_type
+
+ @classmethod
+ def clear_qos_spec(cls, qos_id):
+ test_utils.call_and_ignore_notfound_exc(
+ cls.admin_volume_qos_client.delete_qos, qos_id)
+
+ test_utils.call_and_ignore_notfound_exc(
+ cls.admin_volume_qos_client.wait_for_resource_deletion, qos_id)
+
+ @classmethod
+ def clear_volume_type(cls, vol_type_id):
+ test_utils.call_and_ignore_notfound_exc(
+ cls.admin_volume_types_client.delete_volume_type, vol_type_id)
+
+ test_utils.call_and_ignore_notfound_exc(
+ cls.admin_volume_types_client.wait_for_resource_deletion,
+ vol_type_id)
diff --git a/tempest/api/volume/test_availability_zone.py b/tempest/api/volume/test_availability_zone.py
index fe51375..0b6ee38 100644
--- a/tempest/api/volume/test_availability_zone.py
+++ b/tempest/api/volume/test_availability_zone.py
@@ -14,24 +14,16 @@
# under the License.
from tempest.api.volume import base
-from tempest import test
+from tempest.lib import decorators
-class AvailabilityZoneV2TestJSON(base.BaseVolumeTest):
- """Tests Availability Zone V2 API List"""
+class AvailabilityZoneTestJSON(base.BaseVolumeTest):
+ """Tests Availability Zone API List"""
- @classmethod
- def setup_clients(cls):
- super(AvailabilityZoneV2TestJSON, cls).setup_clients()
- cls.client = cls.availability_zone_client
-
- @test.idempotent_id('01f1ae88-eba9-4c6b-a011-6f7ace06b725')
+ @decorators.idempotent_id('01f1ae88-eba9-4c6b-a011-6f7ace06b725')
def test_get_availability_zone_list(self):
# List of availability zone
- availability_zone = (self.client.list_availability_zones()
- ['availabilityZoneInfo'])
- self.assertTrue(len(availability_zone) > 0)
-
-
-class AvailabilityZoneV1TestJSON(AvailabilityZoneV2TestJSON):
- _api_version = 1
+ availability_zone = (
+ self.availability_zone_client.list_availability_zones()
+ ['availabilityZoneInfo'])
+ self.assertNotEmpty(availability_zone)
diff --git a/tempest/api/volume/test_extensions.py b/tempest/api/volume/test_extensions.py
index cce9ace..39ce00c 100644
--- a/tempest/api/volume/test_extensions.py
+++ b/tempest/api/volume/test_extensions.py
@@ -17,7 +17,7 @@
from tempest.api.volume import base
from tempest import config
-from tempest import test
+from tempest.lib import decorators
CONF = config.CONF
@@ -25,17 +25,17 @@
LOG = logging.getLogger(__name__)
-class ExtensionsV2TestJSON(base.BaseVolumeTest):
+class ExtensionsTestJSON(base.BaseVolumeTest):
- @test.idempotent_id('94607eb0-43a5-47ca-82aa-736b41bd2e2c')
+ @decorators.idempotent_id('94607eb0-43a5-47ca-82aa-736b41bd2e2c')
def test_list_extensions(self):
# List of all extensions
extensions = (self.volumes_extension_client.list_extensions()
['extensions'])
- if len(CONF.volume_feature_enabled.api_extensions) == 0:
+ if not CONF.volume_feature_enabled.api_extensions:
raise self.skipException('There are not any extensions configured')
extension_list = [extension.get('alias') for extension in extensions]
- LOG.debug("Cinder extensions: %s" % ','.join(extension_list))
+ LOG.debug("Cinder extensions: %s", ','.join(extension_list))
ext = CONF.volume_feature_enabled.api_extensions[0]
if ext == 'all':
self.assertIn('Hosts', map(lambda x: x['name'], extensions))
@@ -43,7 +43,3 @@
self.assertIn(ext, map(lambda x: x['alias'], extensions))
else:
raise self.skipException('There are not any extensions configured')
-
-
-class ExtensionsV1TestJSON(ExtensionsV2TestJSON):
- _api_version = 1
diff --git a/tempest/api/volume/test_image_metadata.py b/tempest/api/volume/test_image_metadata.py
new file mode 100644
index 0000000..53b3acc
--- /dev/null
+++ b/tempest/api/volume/test_image_metadata.py
@@ -0,0 +1,72 @@
+# 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 testtools import matchers
+
+from tempest.api.volume import base
+from tempest.common import utils
+from tempest import config
+from tempest.lib import decorators
+
+CONF = config.CONF
+
+
+class VolumesImageMetadata(base.BaseVolumeTest):
+
+ @classmethod
+ def skip_checks(cls):
+ super(VolumesImageMetadata, cls).skip_checks()
+ if not CONF.service_available.glance:
+ skip_msg = ("%s skipped as Glance is not available" % cls.__name__)
+ raise cls.skipException(skip_msg)
+
+ @classmethod
+ def resource_setup(cls):
+ super(VolumesImageMetadata, cls).resource_setup()
+ # Create a volume from image ID
+ cls.volume = cls.create_volume(imageRef=CONF.compute.image_ref)
+
+ @decorators.idempotent_id('03efff0b-5c75-4822-8f10-8789ac15b13e')
+ @utils.services('image')
+ def test_update_show_delete_image_metadata(self):
+ # Update image metadata
+ image_metadata = {'image_id': '5137a025-3c5f-43c1-bc64-5f41270040a5',
+ 'image_name': 'image',
+ 'kernel_id': '6ff710d2-942b-4d6b-9168-8c9cc2404ab1',
+ 'ramdisk_id': 'somedisk'}
+ self.volumes_client.update_volume_image_metadata(self.volume['id'],
+ **image_metadata)
+
+ # Fetch volume's image metadata by show_volume method
+ volume_image_metadata = self.volumes_client.show_volume(
+ self.volume['id'])['volume']['volume_image_metadata']
+
+ # Verify image metadata was updated
+ self.assertThat(volume_image_metadata.items(),
+ matchers.ContainsAll(image_metadata.items()))
+
+ # Delete one item from image metadata of the volume
+ self.volumes_client.delete_volume_image_metadata(self.volume['id'],
+ 'ramdisk_id')
+ del image_metadata['ramdisk_id']
+
+ # Fetch volume's image metadata by show_volume_image_metadata method
+ volume_image_metadata = self.volumes_client.show_volume_image_metadata(
+ self.volume['id'])['metadata']
+
+ # Verify image metadata was updated after item deletion
+ self.assertThat(volume_image_metadata.items(),
+ matchers.ContainsAll(image_metadata.items()))
+ self.assertNotIn('ramdisk_id', volume_image_metadata)
diff --git a/tempest/api/volume/test_qos.py b/tempest/api/volume/test_qos.py
deleted file mode 100644
index 722a39a..0000000
--- a/tempest/api/volume/test_qos.py
+++ /dev/null
@@ -1,181 +0,0 @@
-# All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-from tempest.api.volume import base
-from tempest.common.utils import data_utils as utils
-from tempest import test
-
-
-class QosSpecsV2TestJSON(base.BaseVolumeAdminTest):
- """Test the Cinder QoS-specs.
-
- Tests for create, list, delete, show, associate,
- disassociate, set/unset key V2 APIs.
- """
-
- @classmethod
- def resource_setup(cls):
- super(QosSpecsV2TestJSON, cls).resource_setup()
- # Create admin qos client
- # Create a test shared qos-specs for tests
- cls.qos_name = utils.rand_name(cls.__name__ + '-QoS')
- cls.qos_consumer = 'front-end'
-
- cls.created_qos = cls.create_test_qos_specs(cls.qos_name,
- cls.qos_consumer,
- read_iops_sec='2000')
-
- def _create_delete_test_qos_with_given_consumer(self, consumer):
- name = utils.rand_name('qos')
- qos = {'name': name, 'consumer': consumer}
- body = self.create_test_qos_specs(name, consumer)
- for key in ['name', 'consumer']:
- self.assertEqual(qos[key], body[key])
-
- self.volume_qos_client.delete_qos(body['id'])
- self.volume_qos_client.wait_for_resource_deletion(body['id'])
-
- # validate the deletion
- list_qos = self.volume_qos_client.list_qos()['qos_specs']
- self.assertNotIn(body, list_qos)
-
- def _create_test_volume_type(self):
- vol_type_name = utils.rand_name("volume-type")
- vol_type = self.volume_types_client.create_volume_type(
- name=vol_type_name)['volume_type']
- self.addCleanup(self.volume_types_client.delete_volume_type,
- vol_type['id'])
- return vol_type
-
- def _test_associate_qos(self, vol_type_id):
- self.volume_qos_client.associate_qos(
- self.created_qos['id'], vol_type_id)
-
- def _test_get_association_qos(self):
- body = self.volume_qos_client.show_association_qos(
- self.created_qos['id'])['qos_associations']
-
- associations = []
- for association in body:
- associations.append(association['id'])
-
- return associations
-
- @test.idempotent_id('7e15f883-4bef-49a9-95eb-f94209a1ced1')
- def test_create_delete_qos_with_front_end_consumer(self):
- """Tests the creation and deletion of QoS specs
-
- With consumer as front end
- """
- self._create_delete_test_qos_with_given_consumer('front-end')
-
- @test.idempotent_id('b115cded-8f58-4ee4-aab5-9192cfada08f')
- def test_create_delete_qos_with_back_end_consumer(self):
- """Tests the creation and deletion of QoS specs
-
- With consumer as back-end
- """
- self._create_delete_test_qos_with_given_consumer('back-end')
-
- @test.idempotent_id('f88d65eb-ea0d-487d-af8d-71f4011575a4')
- def test_create_delete_qos_with_both_consumer(self):
- """Tests the creation and deletion of QoS specs
-
- With consumer as both front end and back end
- """
- self._create_delete_test_qos_with_given_consumer('both')
-
- @test.idempotent_id('7aa214cc-ac1a-4397-931f-3bb2e83bb0fd')
- def test_get_qos(self):
- """Tests the detail of a given qos-specs"""
- body = self.volume_qos_client.show_qos(
- self.created_qos['id'])['qos_specs']
- self.assertEqual(self.qos_name, body['name'])
- self.assertEqual(self.qos_consumer, body['consumer'])
-
- @test.idempotent_id('75e04226-bcf7-4595-a34b-fdf0736f38fc')
- def test_list_qos(self):
- """Tests the list of all qos-specs"""
- body = self.volume_qos_client.list_qos()['qos_specs']
- self.assertIn(self.created_qos, body)
-
- @test.idempotent_id('ed00fd85-4494-45f2-8ceb-9e2048919aed')
- def test_set_unset_qos_key(self):
- """Test the addition of a specs key to qos-specs"""
- args = {'iops_bytes': '500'}
- body = self.volume_qos_client.set_qos_key(
- self.created_qos['id'],
- iops_bytes='500')['qos_specs']
- self.assertEqual(args, body)
- body = self.volume_qos_client.show_qos(
- self.created_qos['id'])['qos_specs']
- self.assertEqual(args['iops_bytes'], body['specs']['iops_bytes'])
-
- # test the deletion of a specs key from qos-specs
- keys = ['iops_bytes']
- self.volume_qos_client.unset_qos_key(self.created_qos['id'], keys)
- operation = 'qos-key-unset'
- self.volume_qos_client.wait_for_qos_operations(self.created_qos['id'],
- operation, keys)
- body = self.volume_qos_client.show_qos(
- self.created_qos['id'])['qos_specs']
- self.assertNotIn(keys[0], body['specs'])
-
- @test.idempotent_id('1dd93c76-6420-485d-a771-874044c416ac')
- def test_associate_disassociate_qos(self):
- """Test the following operations :
-
- 1. associate_qos
- 2. get_association_qos
- 3. disassociate_qos
- 4. disassociate_all_qos
- """
-
- # create a test volume-type
- vol_type = []
- for _ in range(0, 3):
- vol_type.append(self._create_test_volume_type())
-
- # associate the qos-specs with volume-types
- for i in range(0, 3):
- self._test_associate_qos(vol_type[i]['id'])
-
- # get the association of the qos-specs
- associations = self._test_get_association_qos()
-
- for i in range(0, 3):
- self.assertIn(vol_type[i]['id'], associations)
-
- # disassociate a volume-type with qos-specs
- self.volume_qos_client.disassociate_qos(
- self.created_qos['id'], vol_type[0]['id'])
- operation = 'disassociate'
- self.volume_qos_client.wait_for_qos_operations(self.created_qos['id'],
- operation,
- vol_type[0]['id'])
- associations = self._test_get_association_qos()
- self.assertNotIn(vol_type[0]['id'], associations)
-
- # disassociate all volume-types from qos-specs
- self.volume_qos_client.disassociate_all_qos(
- self.created_qos['id'])
- operation = 'disassociate-all'
- self.volume_qos_client.wait_for_qos_operations(self.created_qos['id'],
- operation)
- associations = self._test_get_association_qos()
- self.assertEmpty(associations)
-
-
-class QosSpecsV1TestJSON(QosSpecsV2TestJSON):
- _api_version = 1
diff --git a/tempest/api/volume/test_snapshot_metadata.py b/tempest/api/volume/test_snapshot_metadata.py
index e50ca95..e6fe25d 100644
--- a/tempest/api/volume/test_snapshot_metadata.py
+++ b/tempest/api/volume/test_snapshot_metadata.py
@@ -17,86 +17,71 @@
from tempest.api.volume import base
from tempest import config
-from tempest import test
+from tempest.lib import decorators
CONF = config.CONF
-class SnapshotV2MetadataTestJSON(base.BaseVolumeTest):
+class SnapshotMetadataTestJSON(base.BaseVolumeTest):
@classmethod
def skip_checks(cls):
- super(SnapshotV2MetadataTestJSON, cls).skip_checks()
+ super(SnapshotMetadataTestJSON, cls).skip_checks()
if not CONF.volume_feature_enabled.snapshot:
raise cls.skipException("Cinder snapshot feature disabled")
@classmethod
- def setup_clients(cls):
- super(SnapshotV2MetadataTestJSON, cls).setup_clients()
- cls.client = cls.snapshots_client
-
- @classmethod
def resource_setup(cls):
- super(SnapshotV2MetadataTestJSON, cls).resource_setup()
+ super(SnapshotMetadataTestJSON, cls).resource_setup()
# Create a volume
cls.volume = cls.create_volume()
# Create a snapshot
cls.snapshot = cls.create_snapshot(volume_id=cls.volume['id'])
- cls.snapshot_id = cls.snapshot['id']
def tearDown(self):
# Update the metadata to {}
- self.client.update_snapshot_metadata(self.snapshot_id, {})
- super(SnapshotV2MetadataTestJSON, self).tearDown()
+ self.snapshots_client.update_snapshot_metadata(
+ self.snapshot['id'], metadata={})
+ super(SnapshotMetadataTestJSON, self).tearDown()
- @test.idempotent_id('a2f20f99-e363-4584-be97-bc33afb1a56c')
- def test_create_get_delete_snapshot_metadata(self):
+ @decorators.idempotent_id('a2f20f99-e363-4584-be97-bc33afb1a56c')
+ def test_crud_snapshot_metadata(self):
# Create metadata for the snapshot
metadata = {"key1": "value1",
"key2": "value2",
"key3": "value3"}
- expected = {"key2": "value2",
- "key3": "value3"}
- body = self.client.create_snapshot_metadata(
- self.snapshot_id, metadata)['metadata']
- # Get the metadata of the snapshot
- body = self.client.show_snapshot_metadata(
- self.snapshot_id)['metadata']
- self.assertThat(body.items(), matchers.ContainsAll(metadata.items()))
-
- # Delete one item metadata of the snapshot
- self.client.delete_snapshot_metadata_item(
- self.snapshot_id, "key1")
- body = self.client.show_snapshot_metadata(
- self.snapshot_id)['metadata']
- self.assertThat(body.items(), matchers.ContainsAll(expected.items()))
- self.assertNotIn("key1", body)
-
- @test.idempotent_id('bd2363bc-de92-48a4-bc98-28943c6e4be1')
- def test_update_snapshot_metadata(self):
- # Update metadata for the snapshot
- metadata = {"key1": "value1",
- "key2": "value2",
- "key3": "value3"}
update = {"key3": "value3_update",
"key4": "value4"}
- # Create metadata for the snapshot
- body = self.client.create_snapshot_metadata(
- self.snapshot_id, metadata)['metadata']
- # Get the metadata of the snapshot
- body = self.client.show_snapshot_metadata(
- self.snapshot_id)['metadata']
+ expect = {"key4": "value4"}
+ # Create metadata
+ body = self.snapshots_client.create_snapshot_metadata(
+ self.snapshot['id'], metadata)['metadata']
self.assertThat(body.items(), matchers.ContainsAll(metadata.items()))
- # Update metadata item
- body = self.client.update_snapshot_metadata(
- self.snapshot_id, update)['metadata']
# Get the metadata of the snapshot
- body = self.client.show_snapshot_metadata(
- self.snapshot_id)['metadata']
- self.assertEqual(update, body)
+ body = self.snapshots_client.show_snapshot_metadata(
+ self.snapshot['id'])['metadata']
+ self.assertThat(body.items(), matchers.ContainsAll(metadata.items()),
+ 'Create snapshot metadata failed')
- @test.idempotent_id('e8ff85c5-8f97-477f-806a-3ac364a949ed')
- def test_update_snapshot_metadata_item(self):
+ # Update metadata
+ body = self.snapshots_client.update_snapshot_metadata(
+ self.snapshot['id'], metadata=update)['metadata']
+ self.assertEqual(update, body)
+ body = self.snapshots_client.show_snapshot_metadata(
+ self.snapshot['id'])['metadata']
+ self.assertEqual(update, body, 'Update snapshot metadata failed')
+
+ # Delete one item metadata of the snapshot
+ self.snapshots_client.delete_snapshot_metadata_item(
+ self.snapshot['id'], "key3")
+ body = self.snapshots_client.show_snapshot_metadata(
+ self.snapshot['id'])['metadata']
+ self.assertThat(body.items(), matchers.ContainsAll(expect.items()),
+ 'Delete one item metadata of the snapshot failed')
+ self.assertNotIn("key3", body)
+
+ @decorators.idempotent_id('e8ff85c5-8f97-477f-806a-3ac364a949ed')
+ def test_update_show_snapshot_metadata_item(self):
# Update metadata item for the snapshot
metadata = {"key1": "value1",
"key2": "value2",
@@ -106,20 +91,23 @@
"key2": "value2",
"key3": "value3_update"}
# Create metadata for the snapshot
- body = self.client.create_snapshot_metadata(
- self.snapshot_id, metadata)['metadata']
+ self.snapshots_client.create_snapshot_metadata(
+ self.snapshot['id'], metadata)
# Get the metadata of the snapshot
- body = self.client.show_snapshot_metadata(
- self.snapshot_id)['metadata']
+ body = self.snapshots_client.show_snapshot_metadata(
+ self.snapshot['id'])['metadata']
self.assertThat(body.items(), matchers.ContainsAll(metadata.items()))
# Update metadata item
- body = self.client.update_snapshot_metadata_item(
- self.snapshot_id, "key3", update_item)['meta']
+ body = self.snapshots_client.update_snapshot_metadata_item(
+ self.snapshot['id'], "key3", meta=update_item)['meta']
+ self.assertEqual(update_item, body)
+
+ # Get a specific metadata item of the snapshot
+ body = self.snapshots_client.show_snapshot_metadata_item(
+ self.snapshot['id'], "key3")['meta']
+ self.assertEqual({"key3": expect['key3']}, body)
+
# Get the metadata of the snapshot
- body = self.client.show_snapshot_metadata(
- self.snapshot_id)['metadata']
+ body = self.snapshots_client.show_snapshot_metadata(
+ self.snapshot['id'])['metadata']
self.assertThat(body.items(), matchers.ContainsAll(expect.items()))
-
-
-class SnapshotV1MetadataTestJSON(SnapshotV2MetadataTestJSON):
- _api_version = 1
diff --git a/tempest/api/volume/test_versions.py b/tempest/api/volume/test_versions.py
new file mode 100644
index 0000000..b4d48db
--- /dev/null
+++ b/tempest/api/volume/test_versions.py
@@ -0,0 +1,29 @@
+# Copyright 2017 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.volume import base
+from tempest.lib import decorators
+
+
+class VersionsTest(base.BaseVolumeTest):
+
+ _api_version = 3
+
+ @decorators.idempotent_id('77838fc4-b49b-4c64-9533-166762517369')
+ @decorators.attr(type='smoke')
+ def test_list_versions(self):
+ # NOTE: The version data is checked on service client side
+ # with JSON-Schema validation. It is enough to just call
+ # the API here.
+ self.versions_client.list_versions()
diff --git a/tempest/api/volume/test_volume_absolute_limits.py b/tempest/api/volume/test_volume_absolute_limits.py
new file mode 100644
index 0000000..00a3375
--- /dev/null
+++ b/tempest/api/volume/test_volume_absolute_limits.py
@@ -0,0 +1,57 @@
+# Copyright 2016 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from tempest.api.volume import base
+from tempest import config
+from tempest.lib import decorators
+
+CONF = config.CONF
+
+
+# NOTE(zhufl): This inherits from BaseVolumeAdminTest because
+# it requires force_tenant_isolation=True, which need admin
+# credentials to create non-admin users for the tests.
+class AbsoluteLimitsTests(base.BaseVolumeAdminTest): # noqa
+
+ # avoid existing volumes of pre-defined tenant
+ force_tenant_isolation = True
+
+ @classmethod
+ def resource_setup(cls):
+ super(AbsoluteLimitsTests, cls).resource_setup()
+
+ # Create a shared volume for tests
+ cls.volume = cls.create_volume()
+
+ @classmethod
+ def skip_checks(cls):
+ super(AbsoluteLimitsTests, cls).skip_checks()
+ if not CONF.auth.use_dynamic_credentials:
+ raise cls.skipException("Must use dynamic credentials.")
+
+ @decorators.idempotent_id('8e943f53-e9d6-4272-b2e9-adcf2f7c29ad')
+ def test_get_volume_absolute_limits(self):
+ # get volume limit for a tenant
+ absolute_limits = \
+ self.volume_limits_client.show_limits(
+ )['limits']['absolute']
+
+ # verify volume limits and defaults per tenants
+ self.assertEqual(absolute_limits['totalGigabytesUsed'],
+ CONF.volume.volume_size)
+ self.assertEqual(absolute_limits['totalVolumesUsed'], 1)
+ self.assertEqual(absolute_limits['totalSnapshotsUsed'], 0)
+ self.assertEqual(absolute_limits['totalBackupsUsed'], 0)
+ self.assertEqual(absolute_limits['totalBackupGigabytesUsed'], 0)
diff --git a/tempest/api/volume/test_volume_delete_cascade.py b/tempest/api/volume/test_volume_delete_cascade.py
new file mode 100644
index 0000000..bb32c11
--- /dev/null
+++ b/tempest/api/volume/test_volume_delete_cascade.py
@@ -0,0 +1,101 @@
+# 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.
+
+import operator
+
+import testtools
+
+from tempest.api.volume import base
+from tempest import config
+from tempest.lib import decorators
+
+CONF = config.CONF
+
+
+class VolumesDeleteCascade(base.BaseVolumeTest):
+ """Delete a volume with associated snapshots.
+
+ Cinder provides the ability to delete a volume with its
+ associated snapshots.
+ It is allow a volume and its snapshots to be removed in one operation
+ both for usability and performance reasons.
+ """
+
+ @classmethod
+ def skip_checks(cls):
+ super(VolumesDeleteCascade, cls).skip_checks()
+ if not CONF.volume_feature_enabled.snapshot:
+ raise cls.skipException("Cinder snapshot feature disabled")
+
+ def _assert_cascade_delete(self, volume_id):
+ # Fetch volume ids
+ volume_list = [
+ vol['id'] for vol in
+ self.volumes_client.list_volumes()['volumes']
+ ]
+
+ # Verify the parent volume was deleted
+ self.assertNotIn(volume_id, volume_list)
+
+ # List snapshots
+ snapshot_list = self.snapshots_client.list_snapshots()['snapshots']
+
+ # Verify snapshots were deleted
+ self.assertNotIn(volume_id, map(operator.itemgetter('volume_id'),
+ snapshot_list))
+
+ @decorators.idempotent_id('994e2d40-de37-46e8-b328-a58fba7e4a95')
+ def test_volume_delete_cascade(self):
+ # The case validates the ability to delete a volume
+ # with associated snapshots.
+
+ # Create a volume
+ volume = self.create_volume()
+
+ for _ in range(2):
+ self.create_snapshot(volume['id'])
+
+ # Delete the parent volume with associated snapshots
+ self.volumes_client.delete_volume(volume['id'], cascade=True)
+ self.volumes_client.wait_for_resource_deletion(volume['id'])
+
+ # Verify volume parent was deleted with its associated snapshots
+ self._assert_cascade_delete(volume['id'])
+
+ @decorators.idempotent_id('59a77ede-609b-4ee8-9f68-fc3c6ffe97b5')
+ @testtools.skipIf(CONF.volume.storage_protocol == 'ceph',
+ 'Skip because of Bug#1677525')
+ def test_volume_from_snapshot_cascade_delete(self):
+ # The case validates the ability to delete a volume with
+ # associated snapshot while there is another volume created
+ # from that snapshot.
+
+ # Create a volume
+ volume = self.create_volume()
+
+ snapshot = self.create_snapshot(volume['id'])
+
+ # Create volume from snapshot
+ volume_snap = self.create_volume(snapshot_id=snapshot['id'])
+ volume_details = self.volumes_client.show_volume(
+ volume_snap['id'])['volume']
+ self.assertEqual(snapshot['id'], volume_details['snapshot_id'])
+
+ # Delete the parent volume with associated snapshot
+ self.volumes_client.delete_volume(volume['id'], cascade=True)
+ self.volumes_client.wait_for_resource_deletion(volume['id'])
+
+ # Verify volume parent was deleted with its associated snapshot
+ self._assert_cascade_delete(volume['id'])
diff --git a/tempest/api/volume/test_volume_metadata.py b/tempest/api/volume/test_volume_metadata.py
index e529538..d203b2d 100644
--- a/tempest/api/volume/test_volume_metadata.py
+++ b/tempest/api/volume/test_volume_metadata.py
@@ -16,73 +16,61 @@
from testtools import matchers
from tempest.api.volume import base
-from tempest import test
+from tempest.lib import decorators
-class VolumesV2MetadataTest(base.BaseVolumeTest):
+class VolumesMetadataTest(base.BaseVolumeTest):
@classmethod
def resource_setup(cls):
- super(VolumesV2MetadataTest, cls).resource_setup()
+ super(VolumesMetadataTest, cls).resource_setup()
# Create a volume
cls.volume = cls.create_volume()
- cls.volume_id = cls.volume['id']
def tearDown(self):
# Update the metadata to {}
- self.volumes_client.update_volume_metadata(self.volume_id, {})
- super(VolumesV2MetadataTest, self).tearDown()
+ self.volumes_client.update_volume_metadata(self.volume['id'], {})
+ super(VolumesMetadataTest, self).tearDown()
- @test.idempotent_id('6f5b125b-f664-44bf-910f-751591fe5769')
- def test_create_get_delete_volume_metadata(self):
+ @decorators.idempotent_id('6f5b125b-f664-44bf-910f-751591fe5769')
+ def test_crud_volume_metadata(self):
# Create metadata for the volume
metadata = {"key1": "value1",
"key2": "value2",
"key3": "value3",
"key4": "<value&special_chars>"}
-
- body = self.volumes_client.create_volume_metadata(self.volume_id,
- metadata)['metadata']
- # Get the metadata of the volume
- body = self.volumes_client.show_volume_metadata(
- self.volume_id)['metadata']
- self.assertThat(body.items(), matchers.ContainsAll(metadata.items()))
- # Delete one item metadata of the volume
- self.volumes_client.delete_volume_metadata_item(
- self.volume_id, "key1")
- body = self.volumes_client.show_volume_metadata(
- self.volume_id)['metadata']
- self.assertNotIn("key1", body)
- del metadata["key1"]
- self.assertThat(body.items(), matchers.ContainsAll(metadata.items()))
-
- @test.idempotent_id('774d2918-9beb-4f30-b3d1-2a4e8179ec0a')
- def test_update_volume_metadata(self):
- # Update metadata for the volume
- metadata = {"key1": "value1",
- "key2": "value2",
- "key3": "value3"}
-
update = {"key4": "value4",
"key1": "value1_update"}
+ expected = {"key4": "value4"}
- # Create metadata for the volume
- body = self.volumes_client.create_volume_metadata(
- self.volume_id, metadata)['metadata']
+ body = self.volumes_client.create_volume_metadata(self.volume['id'],
+ metadata)['metadata']
+ self.assertThat(body.items(), matchers.ContainsAll(metadata.items()))
# Get the metadata of the volume
body = self.volumes_client.show_volume_metadata(
- self.volume_id)['metadata']
- self.assertThat(body.items(), matchers.ContainsAll(metadata.items()))
+ self.volume['id'])['metadata']
+ self.assertThat(body.items(), matchers.ContainsAll(metadata.items()),
+ 'Create metadata for the volume failed')
+
# Update metadata
body = self.volumes_client.update_volume_metadata(
- self.volume_id, update)['metadata']
- # Get the metadata of the volume
- body = self.volumes_client.show_volume_metadata(
- self.volume_id)['metadata']
+ self.volume['id'], update)['metadata']
self.assertEqual(update, body)
+ body = self.volumes_client.show_volume_metadata(
+ self.volume['id'])['metadata']
+ self.assertEqual(update, body, 'Update metadata failed')
- @test.idempotent_id('862261c5-8df4-475a-8c21-946e50e36a20')
- def test_update_volume_metadata_item(self):
+ # Delete one item metadata of the volume
+ self.volumes_client.delete_volume_metadata_item(
+ self.volume['id'], "key1")
+ body = self.volumes_client.show_volume_metadata(
+ self.volume['id'])['metadata']
+ self.assertNotIn("key1", body)
+ self.assertThat(body.items(), matchers.ContainsAll(expected.items()),
+ 'Delete one item metadata of the volume failed')
+
+ @decorators.idempotent_id('862261c5-8df4-475a-8c21-946e50e36a20')
+ def test_update_show_volume_metadata_item(self):
# Update metadata item for the volume
metadata = {"key1": "value1",
"key2": "value2",
@@ -93,16 +81,20 @@
"key3": "value3_update"}
# Create metadata for the volume
body = self.volumes_client.create_volume_metadata(
- self.volume_id, metadata)['metadata']
- self.assertThat(body.items(), matchers.ContainsAll(metadata.items()))
+ self.volume['id'], metadata)['metadata']
+ self.assertThat(body.items(),
+ matchers.ContainsAll(metadata.items()))
# Update metadata item
body = self.volumes_client.update_volume_metadata_item(
- self.volume_id, "key3", update_item)['meta']
+ self.volume['id'], "key3", update_item)['meta']
+ self.assertEqual(update_item, body)
+
+ # Get a specific metadata item of the volume
+ body = self.volumes_client.show_volume_metadata_item(
+ self.volume['id'], "key3")['meta']
+ self.assertEqual({"key3": expect['key3']}, body)
+
# Get the metadata of the volume
body = self.volumes_client.show_volume_metadata(
- self.volume_id)['metadata']
+ self.volume['id'])['metadata']
self.assertThat(body.items(), matchers.ContainsAll(expect.items()))
-
-
-class VolumesV1MetadataTest(VolumesV2MetadataTest):
- _api_version = 1
diff --git a/tempest/api/volume/test_volume_transfers.py b/tempest/api/volume/test_volume_transfers.py
index c0b6b7e..75e81b7 100644
--- a/tempest/api/volume/test_volume_transfers.py
+++ b/tempest/api/volume/test_volume_transfers.py
@@ -13,45 +13,43 @@
# License for the specific language governing permissions and limitations
# under the License.
-from testtools import matchers
-
from tempest.api.volume import base
-from tempest import config
-from tempest import test
-
-CONF = config.CONF
+from tempest.common import waiters
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
-class VolumesV2TransfersTest(base.BaseVolumeTest):
+class VolumesTransfersTest(base.BaseVolumeTest):
credentials = ['primary', 'alt', 'admin']
@classmethod
def setup_clients(cls):
- super(VolumesV2TransfersTest, cls).setup_clients()
+ super(VolumesTransfersTest, cls).setup_clients()
- cls.client = cls.volumes_client
- cls.alt_client = cls.os_alt.volumes_client
- cls.alt_tenant_id = cls.alt_client.tenant_id
- cls.adm_client = cls.os_adm.volumes_client
+ cls.client = cls.os_primary.volume_transfers_v2_client
+ cls.alt_client = cls.os_alt.volume_transfers_v2_client
+ cls.alt_volumes_client = cls.os_alt.volumes_v2_client
+ cls.adm_volumes_client = cls.os_admin.volumes_v2_client
- def _delete_volume(self, volume_id):
- # Delete the specified volume using admin creds
- self.adm_client.delete_volume(volume_id)
- self.adm_client.wait_for_resource_deletion(volume_id)
-
- @test.idempotent_id('4d75b645-a478-48b1-97c8-503f64242f1a')
+ @decorators.idempotent_id('4d75b645-a478-48b1-97c8-503f64242f1a')
def test_create_get_list_accept_volume_transfer(self):
# Create a volume first
volume = self.create_volume()
- self.addCleanup(self._delete_volume, volume['id'])
+ self.addCleanup(self.delete_volume,
+ self.adm_volumes_client,
+ volume['id'])
# Create a volume transfer
- transfer = self.client.create_volume_transfer(volume['id'])['transfer']
+ transfer = self.client.create_volume_transfer(
+ volume_id=volume['id'])['transfer']
transfer_id = transfer['id']
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.client.delete_volume_transfer,
+ transfer_id)
auth_key = transfer['auth_key']
- self.client.wait_for_volume_status(volume['id'],
- 'awaiting-transfer')
+ waiters.wait_for_volume_resource_status(
+ self.volumes_client, volume['id'], 'awaiting-transfer')
# Get a volume transfer
body = self.client.show_volume_transfer(transfer_id)['transfer']
@@ -60,37 +58,50 @@
# List volume transfers, the result should be greater than
# or equal to 1
body = self.client.list_volume_transfers()['transfers']
- self.assertThat(len(body), matchers.GreaterThan(0))
+ self.assertNotEmpty(body)
# Accept a volume transfer by alt_tenant
- body = self.alt_client.accept_volume_transfer(transfer_id,
- auth_key)['transfer']
- self.alt_client.wait_for_volume_status(volume['id'], 'available')
+ body = self.alt_client.accept_volume_transfer(
+ transfer_id, auth_key=auth_key)['transfer']
+ for key in ['id', 'name', 'links', 'volume_id']:
+ self.assertIn(key, body)
+ waiters.wait_for_volume_resource_status(self.alt_volumes_client,
+ volume['id'], 'available')
+ accepted_volume = self.alt_volumes_client.show_volume(
+ volume['id'])['volume']
+ self.assertEqual(self.os_alt.credentials.user_id,
+ accepted_volume['user_id'])
+ self.assertEqual(self.os_alt.credentials.project_id,
+ accepted_volume['os-vol-tenant-attr:tenant_id'])
- @test.idempotent_id('ab526943-b725-4c07-b875-8e8ef87a2c30')
+ @decorators.idempotent_id('ab526943-b725-4c07-b875-8e8ef87a2c30')
def test_create_list_delete_volume_transfer(self):
# Create a volume first
volume = self.create_volume()
- self.addCleanup(self._delete_volume, volume['id'])
+ self.addCleanup(self.delete_volume,
+ self.adm_volumes_client,
+ volume['id'])
# Create a volume transfer
- body = self.client.create_volume_transfer(volume['id'])['transfer']
- transfer_id = body['id']
- self.client.wait_for_volume_status(volume['id'],
- 'awaiting-transfer')
+ transfer_id = self.client.create_volume_transfer(
+ volume_id=volume['id'])['transfer']['id']
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.client.delete_volume_transfer,
+ transfer_id)
+ waiters.wait_for_volume_resource_status(
+ self.volumes_client, volume['id'], 'awaiting-transfer')
- # List all volume transfers (looking for the one we created)
- body = self.client.list_volume_transfers()['transfers']
- for transfer in body:
- if volume['id'] == transfer['volume_id']:
- break
- else:
- self.fail('Transfer not found for volume %s' % volume['id'])
+ # List all volume transfers with details, check the detail-specific
+ # elements, and look for the created transfer.
+ transfers = self.client.list_volume_transfers(detail=True)['transfers']
+ self.assertNotEmpty(transfers)
+ for transfer in transfers:
+ self.assertIn('created_at', transfer)
+ volume_list = [transfer['volume_id'] for transfer in transfers]
+ self.assertIn(volume['id'], volume_list,
+ 'Transfer not found for volume %s' % volume['id'])
# Delete a volume transfer
self.client.delete_volume_transfer(transfer_id)
- self.client.wait_for_volume_status(volume['id'], 'available')
-
-
-class VolumesV1TransfersTest(VolumesV2TransfersTest):
- _api_version = 1
+ waiters.wait_for_volume_resource_status(
+ self.volumes_client, volume['id'], 'available')
diff --git a/tempest/api/volume/test_volumes_actions.py b/tempest/api/volume/test_volumes_actions.py
index d4636ee..be5638e 100644
--- a/tempest/api/volume/test_volumes_actions.py
+++ b/tempest/api/volume/test_volumes_actions.py
@@ -14,149 +14,132 @@
# under the License.
from tempest.api.volume import base
-from tempest.common.utils import data_utils
+from tempest.common import utils
from tempest.common import waiters
from tempest import config
-from tempest import test
-import testtools
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
CONF = config.CONF
-class VolumesV2ActionsTest(base.BaseVolumeTest):
-
- @classmethod
- def setup_clients(cls):
- super(VolumesV2ActionsTest, cls).setup_clients()
- cls.client = cls.volumes_client
- cls.image_client = cls.os.image_client
+class VolumesActionsTest(base.BaseVolumeTest):
@classmethod
def resource_setup(cls):
- super(VolumesV2ActionsTest, cls).resource_setup()
-
- # Create a test shared instance
- srv_name = data_utils.rand_name(cls.__name__ + '-Instance')
- cls.server = cls.create_server(
- name=srv_name,
- wait_until='ACTIVE')
+ super(VolumesActionsTest, cls).resource_setup()
# Create a test shared volume for attach/detach tests
cls.volume = cls.create_volume()
- cls.client.wait_for_volume_status(cls.volume['id'], 'available')
- @classmethod
- def resource_cleanup(cls):
- # Delete the test instance
- cls.servers_client.delete_server(cls.server['id'])
- waiters.wait_for_server_termination(cls.servers_client,
- cls.server['id'])
-
- super(VolumesV2ActionsTest, cls).resource_cleanup()
-
- @test.idempotent_id('fff42874-7db5-4487-a8e1-ddda5fb5288d')
- @test.stresstest(class_setup_per='process')
- @test.attr(type='smoke')
- @test.services('compute')
+ @decorators.idempotent_id('fff42874-7db5-4487-a8e1-ddda5fb5288d')
+ @decorators.attr(type='smoke')
+ @utils.services('compute')
def test_attach_detach_volume_to_instance(self):
+ # Create a server
+ server = self.create_server()
# Volume is attached and detached successfully from an instance
- mountpoint = '/dev/vdc'
- self.client.attach_volume(self.volume['id'],
- self.server['id'],
- mountpoint)
- self.client.wait_for_volume_status(self.volume['id'], 'in-use')
- self.client.detach_volume(self.volume['id'])
- self.client.wait_for_volume_status(self.volume['id'], 'available')
+ self.volumes_client.attach_volume(self.volume['id'],
+ instance_uuid=server['id'],
+ mountpoint='/dev/%s' %
+ CONF.compute.volume_device_name)
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ self.volume['id'], 'in-use')
+ self.volumes_client.detach_volume(self.volume['id'])
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ self.volume['id'], 'available')
- @test.idempotent_id('63e21b4c-0a0c-41f6-bfc3-7c2816815599')
- @testtools.skipUnless(CONF.volume_feature_enabled.bootable,
- 'Update bootable status of a volume is not enabled.')
+ @decorators.idempotent_id('63e21b4c-0a0c-41f6-bfc3-7c2816815599')
def test_volume_bootable(self):
# Verify that a volume bootable flag is retrieved
for bool_bootable in [True, False]:
- self.client.set_bootable_volume(self.volume['id'], bool_bootable)
- fetched_volume = self.client.show_volume(
+ self.volumes_client.set_bootable_volume(self.volume['id'],
+ bootable=bool_bootable)
+ fetched_volume = self.volumes_client.show_volume(
self.volume['id'])['volume']
# Get Volume information
- bool_flag = self._is_true(fetched_volume['bootable'])
- self.assertEqual(bool_bootable, bool_flag)
+ # NOTE(masayukig): 'bootable' is "true" or "false" in the current
+ # cinder implementation. So we need to cast boolean values to str
+ # and make it lower to compare here.
+ self.assertEqual(str(bool_bootable).lower(),
+ fetched_volume['bootable'])
- @test.idempotent_id('9516a2c8-9135-488c-8dd6-5677a7e5f371')
- @test.stresstest(class_setup_per='process')
- @test.services('compute')
+ @decorators.idempotent_id('9516a2c8-9135-488c-8dd6-5677a7e5f371')
+ @utils.services('compute')
def test_get_volume_attachment(self):
+ # Create a server
+ server = self.create_server()
# Verify that a volume's attachment information is retrieved
- mountpoint = '/dev/vdc'
- self.client.attach_volume(self.volume['id'],
- self.server['id'],
- mountpoint)
- self.client.wait_for_volume_status(self.volume['id'], 'in-use')
- # NOTE(gfidente): added in reverse order because functions will be
- # called in reverse order to the order they are added (LIFO)
- self.addCleanup(self.client.wait_for_volume_status,
- self.volume['id'],
- 'available')
- self.addCleanup(self.client.detach_volume, self.volume['id'])
- volume = self.client.show_volume(self.volume['id'])['volume']
+ self.volumes_client.attach_volume(self.volume['id'],
+ instance_uuid=server['id'],
+ mountpoint='/dev/%s' %
+ CONF.compute.volume_device_name)
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ self.volume['id'],
+ 'in-use')
+ self.addCleanup(waiters.wait_for_volume_resource_status,
+ self.volumes_client,
+ self.volume['id'], 'available')
+ self.addCleanup(self.volumes_client.detach_volume, self.volume['id'])
+ volume = self.volumes_client.show_volume(self.volume['id'])['volume']
self.assertIn('attachments', volume)
- attachment = self.client.get_attachment_from_volume(volume)
- self.assertEqual(mountpoint, attachment['device'])
- self.assertEqual(self.server['id'], attachment['server_id'])
+ attachment = volume['attachments'][0]
+
+ self.assertEqual('/dev/%s' %
+ CONF.compute.volume_device_name,
+ attachment['device'])
+ self.assertEqual(server['id'], attachment['server_id'])
self.assertEqual(self.volume['id'], attachment['id'])
self.assertEqual(self.volume['id'], attachment['volume_id'])
- @test.idempotent_id('d8f1ca95-3d5b-44a3-b8ca-909691c9532d')
- @test.services('image')
+ @decorators.idempotent_id('d8f1ca95-3d5b-44a3-b8ca-909691c9532d')
+ @utils.services('image')
def test_volume_upload(self):
# NOTE(gfidente): the volume uploaded in Glance comes from setUpClass,
# it is shared with the other tests. After it is uploaded in Glance,
# there is no way to delete it from Cinder, so we delete it from Glance
- # using the Glance image_client and from Cinder via tearDownClass.
- image_name = data_utils.rand_name('Image')
- body = self.client.upload_volume(
- self.volume['id'], image_name,
- CONF.volume.disk_format)['os-volume_upload_image']
+ # using the Glance images_client and from Cinder via tearDownClass.
+ image_name = data_utils.rand_name(self.__class__.__name__ + '-Image')
+ body = self.volumes_client.upload_volume(
+ self.volume['id'], image_name=image_name,
+ disk_format=CONF.volume.disk_format)['os-volume_upload_image']
image_id = body["image_id"]
- self.addCleanup(self.image_client.delete_image, image_id)
- self.image_client.wait_for_image_status(image_id, 'active')
- self.client.wait_for_volume_status(self.volume['id'], 'available')
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.images_client.delete_image,
+ image_id)
+ waiters.wait_for_image_status(self.images_client, image_id, 'active')
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ self.volume['id'], 'available')
- @test.idempotent_id('92c4ef64-51b2-40c0-9f7e-4749fbaaba33')
+ image_info = self.images_client.show_image(image_id)
+ self.assertEqual(image_name, image_info['name'])
+ self.assertEqual(CONF.volume.disk_format, image_info['disk_format'])
+
+ @decorators.idempotent_id('92c4ef64-51b2-40c0-9f7e-4749fbaaba33')
def test_reserve_unreserve_volume(self):
# Mark volume as reserved.
- body = self.client.reserve_volume(self.volume['id'])
+ self.volumes_client.reserve_volume(self.volume['id'])
# To get the volume info
- body = self.client.show_volume(self.volume['id'])['volume']
+ body = self.volumes_client.show_volume(self.volume['id'])['volume']
self.assertIn('attaching', body['status'])
# Unmark volume as reserved.
- body = self.client.unreserve_volume(self.volume['id'])
+ self.volumes_client.unreserve_volume(self.volume['id'])
# To get the volume info
- body = self.client.show_volume(self.volume['id'])['volume']
+ body = self.volumes_client.show_volume(self.volume['id'])['volume']
self.assertIn('available', body['status'])
- def _is_true(self, val):
- return val in ['true', 'True', True]
-
- @test.idempotent_id('fff74e1e-5bd3-4b33-9ea9-24c103bc3f59')
+ @decorators.idempotent_id('fff74e1e-5bd3-4b33-9ea9-24c103bc3f59')
def test_volume_readonly_update(self):
- # Update volume readonly true
- readonly = True
- self.client.update_volume_readonly(self.volume['id'],
- readonly)
- # Get Volume information
- fetched_volume = self.client.show_volume(self.volume['id'])['volume']
- bool_flag = self._is_true(fetched_volume['metadata']['readonly'])
- self.assertEqual(True, bool_flag)
-
- # Update volume readonly false
- readonly = False
- self.client.update_volume_readonly(self.volume['id'], readonly)
-
- # Get Volume information
- fetched_volume = self.client.show_volume(self.volume['id'])['volume']
- bool_flag = self._is_true(fetched_volume['metadata']['readonly'])
- self.assertEqual(False, bool_flag)
-
-
-class VolumesV1ActionsTest(VolumesV2ActionsTest):
- _api_version = 1
+ for readonly in [True, False]:
+ # Update volume readonly
+ self.volumes_client.update_volume_readonly(self.volume['id'],
+ readonly=readonly)
+ # Get Volume information
+ fetched_volume = self.volumes_client.show_volume(
+ self.volume['id'])['volume']
+ # NOTE(masayukig): 'readonly' is "True" or "False" in the current
+ # cinder implementation. So we need to cast boolean values to str
+ # to compare here.
+ self.assertEqual(str(readonly),
+ fetched_volume['metadata']['readonly'])
diff --git a/tempest/api/volume/test_volumes_backup.py b/tempest/api/volume/test_volumes_backup.py
new file mode 100644
index 0000000..c178272
--- /dev/null
+++ b/tempest/api/volume/test_volumes_backup.py
@@ -0,0 +1,185 @@
+# 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.
+
+import testtools
+from testtools import matchers
+
+from tempest.api.volume import base
+from tempest.common import utils
+from tempest.common import waiters
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+
+CONF = config.CONF
+
+
+class VolumesBackupsTest(base.BaseVolumeTest):
+
+ @classmethod
+ def skip_checks(cls):
+ super(VolumesBackupsTest, cls).skip_checks()
+ if not CONF.volume_feature_enabled.backup:
+ raise cls.skipException("Cinder backup feature disabled")
+
+ def restore_backup(self, backup_id):
+ # Restore a backup
+ restored_volume = self.backups_client.restore_backup(
+ backup_id)['restore']
+
+ # Delete backup
+ self.addCleanup(self.delete_volume, self.volumes_client,
+ restored_volume['volume_id'])
+ self.assertEqual(backup_id, restored_volume['backup_id'])
+ waiters.wait_for_volume_resource_status(self.backups_client,
+ backup_id, 'available')
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ restored_volume['volume_id'],
+ 'available')
+ return restored_volume
+
+ @testtools.skipIf(CONF.volume.storage_protocol == 'ceph',
+ 'ceph does not support arbitrary container names')
+ @decorators.idempotent_id('a66eb488-8ee1-47d4-8e9f-575a095728c6')
+ def test_volume_backup_create_get_detailed_list_restore_delete(self):
+ # Create a volume with metadata
+ metadata = {"vol-meta1": "value1",
+ "vol-meta2": "value2",
+ "vol-meta3": "value3"}
+ volume = self.create_volume(metadata=metadata)
+ self.addCleanup(self.delete_volume, self.volumes_client, volume['id'])
+
+ # Create a backup
+ backup_name = data_utils.rand_name(
+ self.__class__.__name__ + '-Backup')
+ description = data_utils.rand_name("volume-backup-description")
+ backup = self.create_backup(volume_id=volume['id'],
+ name=backup_name,
+ description=description,
+ container='container')
+ self.assertEqual(backup_name, backup['name'])
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume['id'], 'available')
+
+ # Get a given backup
+ backup = self.backups_client.show_backup(backup['id'])['backup']
+ self.assertEqual(backup_name, backup['name'])
+ self.assertEqual(description, backup['description'])
+ self.assertEqual('container', backup['container'])
+
+ # Get all backups with detail
+ backups = self.backups_client.list_backups(
+ detail=True)['backups']
+ for backup_info in backups:
+ self.assertIn('created_at', backup_info)
+ self.assertIn('links', backup_info)
+ self.assertIn((backup['name'], backup['id']),
+ [(m['name'], m['id']) for m in backups])
+
+ restored_volume = self.restore_backup(backup['id'])
+
+ restored_volume_metadata = self.volumes_client.show_volume(
+ restored_volume['volume_id'])['volume']['metadata']
+
+ # Verify the backups has been restored successfully
+ # with the metadata of the source volume.
+ self.assertThat(restored_volume_metadata.items(),
+ matchers.ContainsAll(metadata.items()))
+
+ @decorators.idempotent_id('07af8f6d-80af-44c9-a5dc-c8427b1b62e6')
+ @utils.services('compute')
+ def test_backup_create_attached_volume(self):
+ """Test backup create using force flag.
+
+ Cinder allows to create a volume backup, whether the volume status
+ is "available" or "in-use".
+ """
+ # Create a server
+ volume = self.create_volume()
+ self.addCleanup(self.delete_volume, self.volumes_client, volume['id'])
+ server = self.create_server()
+ # Attach volume to instance
+ self.attach_volume(server['id'], volume['id'])
+ # Create backup using force flag
+ backup_name = data_utils.rand_name(
+ self.__class__.__name__ + '-Backup')
+ backup = self.create_backup(volume_id=volume['id'],
+ name=backup_name, force=True)
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume['id'], 'in-use')
+ self.assertEqual(backup_name, backup['name'])
+
+ @decorators.idempotent_id('2a8ba340-dff2-4511-9db7-646f07156b15')
+ @utils.services('image')
+ def test_bootable_volume_backup_and_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.assertTrue(volume_details['bootable'])
+
+ # Create a backup
+ backup = self.create_backup(volume_id=volume['id'])
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume['id'], 'available')
+
+ # 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.assertTrue(restored_volume_info['bootable'])
+
+
+class VolumesBackupsV39Test(base.BaseVolumeTest):
+
+ _api_version = 3
+ min_microversion = '3.9'
+ max_microversion = 'latest'
+
+ @classmethod
+ def skip_checks(cls):
+ super(VolumesBackupsV39Test, cls).skip_checks()
+ if not CONF.volume_feature_enabled.backup:
+ raise cls.skipException("Cinder backup feature disabled")
+
+ @decorators.idempotent_id('9b374cbc-be5f-4d37-8848-7efb8a873dcc')
+ def test_update_backup(self):
+ # Create volume and backup
+ volume = self.create_volume()
+ backup = self.create_backup(volume_id=volume['id'])
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume['id'], 'available')
+
+ # Update backup and assert response body for update_backup method
+ update_kwargs = {
+ 'name': data_utils.rand_name(self.__class__.__name__ + '-Backup'),
+ 'description': data_utils.rand_name("volume-backup-description")
+ }
+ update_backup = self.backups_client.update_backup(
+ backup['id'], **update_kwargs)['backup']
+ self.assertEqual(backup['id'], update_backup['id'])
+ self.assertEqual(update_kwargs['name'], update_backup['name'])
+ self.assertIn('links', update_backup)
+
+ # Assert response body for show_backup method
+ retrieved_backup = self.backups_client.show_backup(
+ backup['id'])['backup']
+ for key in update_kwargs:
+ self.assertEqual(update_kwargs[key], retrieved_backup[key])
diff --git a/tempest/api/volume/test_volumes_clone.py b/tempest/api/volume/test_volumes_clone.py
new file mode 100644
index 0000000..ea39a21
--- /dev/null
+++ b/tempest/api/volume/test_volumes_clone.py
@@ -0,0 +1,68 @@
+# Copyright 2016 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from tempest.api.volume import base
+from tempest.common import utils
+from tempest import config
+from tempest.lib import decorators
+
+
+CONF = config.CONF
+
+
+class VolumesCloneTest(base.BaseVolumeTest):
+
+ @classmethod
+ def skip_checks(cls):
+ super(VolumesCloneTest, cls).skip_checks()
+ if not CONF.volume_feature_enabled.clone:
+ raise cls.skipException("Cinder volume clones are disabled")
+
+ def _verify_volume_clone(self, source_volume, cloned_volume,
+ bootable='false', extra_size=0):
+
+ cloned_vol_details = self.volumes_client.show_volume(
+ cloned_volume['id'])['volume']
+
+ self.assertEqual(source_volume['id'],
+ cloned_vol_details['source_volid'])
+ self.assertEqual(source_volume['size'] + extra_size,
+ cloned_vol_details['size'])
+ self.assertEqual(bootable, cloned_vol_details['bootable'])
+
+ @decorators.idempotent_id('9adae371-a257-43a5-9555-dc7c88e66e0e')
+ def test_create_from_volume(self):
+ # Creates a volume from another volume passing a size different from
+ # the source volume.
+ src_size = CONF.volume.volume_size
+
+ src_vol = self.create_volume(size=src_size)
+ # Destination volume bigger than source
+ dst_vol = self.create_volume(source_volid=src_vol['id'],
+ size=src_size + 1)
+
+ self._verify_volume_clone(src_vol, dst_vol, extra_size=1)
+
+ @decorators.idempotent_id('cbbcd7c6-5a6c-481a-97ac-ca55ab715d16')
+ @utils.services('image')
+ def test_create_from_bootable_volume(self):
+ # Create volume from image
+ img_uuid = CONF.compute.image_ref
+ src_vol = self.create_volume(imageRef=img_uuid)
+
+ # Create a volume from the bootable volume
+ cloned_vol = self.create_volume(source_volid=src_vol['id'])
+
+ self._verify_volume_clone(src_vol, cloned_vol, bootable='true')
diff --git a/tempest/api/volume/test_volumes_clone_negative.py b/tempest/api/volume/test_volumes_clone_negative.py
new file mode 100644
index 0000000..bba7a0b
--- /dev/null
+++ b/tempest/api/volume/test_volumes_clone_negative.py
@@ -0,0 +1,44 @@
+# Copyright 2016 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from tempest.api.volume import base
+from tempest import config
+from tempest.lib import decorators
+from tempest.lib import exceptions
+
+CONF = config.CONF
+
+
+class VolumesCloneNegativeTest(base.BaseVolumeTest):
+
+ @classmethod
+ def skip_checks(cls):
+ super(VolumesCloneNegativeTest, cls).skip_checks()
+ if not CONF.volume_feature_enabled.clone:
+ raise cls.skipException("Cinder volume clones are disabled")
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('9adae371-a257-43a5-459a-dc7c88e66e0e')
+ def test_create_from_volume_decreasing_size(self):
+ # Creates a volume from another volume passing a size different from
+ # the source volume.
+ src_size = CONF.volume.volume_size + 1
+ src_vol = self.create_volume(size=src_size)
+
+ # Destination volume smaller than source
+ self.assertRaises(exceptions.BadRequest,
+ self.volumes_client.create_volume,
+ size=src_size - 1,
+ source_volid=src_vol['id'])
diff --git a/tempest/api/volume/test_volumes_extend.py b/tempest/api/volume/test_volumes_extend.py
index 78f5571..5d339c4 100644
--- a/tempest/api/volume/test_volumes_extend.py
+++ b/tempest/api/volume/test_volumes_extend.py
@@ -13,30 +13,168 @@
# License for the specific language governing permissions and limitations
# under the License.
+import time
+
+import testtools
+
from tempest.api.volume import base
+from tempest.common import utils
+from tempest.common import waiters
from tempest import config
-from tempest import test
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
CONF = config.CONF
-class VolumesV2ExtendTest(base.BaseVolumeTest):
+class VolumesExtendTest(base.BaseVolumeTest):
- @classmethod
- def setup_clients(cls):
- super(VolumesV2ExtendTest, cls).setup_clients()
- cls.client = cls.volumes_client
-
- @test.idempotent_id('9a36df71-a257-43a5-9555-dc7c88e66e0e')
+ @decorators.idempotent_id('9a36df71-a257-43a5-9555-dc7c88e66e0e')
def test_volume_extend(self):
# Extend Volume Test.
- self.volume = self.create_volume()
- extend_size = int(self.volume['size']) + 1
- self.client.extend_volume(self.volume['id'], extend_size)
- self.client.wait_for_volume_status(self.volume['id'], 'available')
- volume = self.client.show_volume(self.volume['id'])['volume']
- self.assertEqual(int(volume['size']), extend_size)
+ 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)
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume['id'], 'available')
+ volume = self.volumes_client.show_volume(volume['id'])['volume']
+ self.assertEqual(volume['size'], extend_size)
+
+ @decorators.idempotent_id('86be1cba-2640-11e5-9c82-635fb964c912')
+ @testtools.skipUnless(CONF.volume_feature_enabled.snapshot,
+ "Cinder volume snapshots are disabled")
+ def test_volume_extend_when_volume_has_snapshot(self):
+ volume = self.create_volume()
+ self.create_snapshot(volume['id'])
+
+ extend_size = volume['size'] + 1
+ self.volumes_client.extend_volume(volume['id'], new_size=extend_size)
+
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume['id'], 'available')
+ resized_volume = self.volumes_client.show_volume(
+ volume['id'])['volume']
+ self.assertEqual(extend_size, resized_volume['size'])
-class VolumesV1ExtendTest(VolumesV2ExtendTest):
- _api_version = 1
+class VolumesExtendAttachedTest(base.BaseVolumeTest):
+ """Tests extending the size of an attached volume."""
+
+ # We need admin credentials for getting instance action event details. By
+ # default a non-admin can list and show instance actions if they own the
+ # server instance, but since the event details can contain error messages
+ # and tracebacks, like an instance fault, those are not viewable by
+ # non-admins. This is obviously not a great user experience since the user
+ # may not know when the operation is actually complete. A microversion in
+ # the compute API will be added so that non-admins can see instance action
+ # events but will continue to hide the traceback field.
+ # TODO(mriedem): Change this to not rely on the admin user to get the event
+ # details once that microversion is available in Nova.
+ credentials = ['primary', 'admin']
+
+ _api_version = 3
+ # NOTE(mriedem): The minimum required volume API version is 3.42 and the
+ # minimum required compute API microversion is 2.51, but the compute call
+ # is implicit - Cinder calls Nova at that microversion, Tempest does not.
+ min_microversion = '3.42'
+
+ def _find_extend_volume_instance_action(self, server_id):
+ actions = self.servers_client.list_instance_actions(
+ server_id)['instanceActions']
+ for action in actions:
+ if action['action'] == 'extend_volume':
+ return action
+
+ def _find_extend_volume_instance_action_finish_event(self, action):
+ # This has to be called by an admin client otherwise
+ # the events don't show up.
+ action = self.os_admin.servers_client.show_instance_action(
+ action['instance_uuid'], action['request_id'])['instanceAction']
+ for event in action['events']:
+ if (event['event'] == 'compute_extend_volume' and
+ event['finish_time']):
+ return event
+
+ @decorators.idempotent_id('301f5a30-1c6f-4ea0-be1a-91fd28d44354')
+ @testtools.skipUnless(CONF.volume_feature_enabled.extend_attached_volume,
+ "Attached volume extend is disabled.")
+ @utils.services('compute')
+ def test_extend_attached_volume(self):
+ """This is a happy path test which does the following:
+
+ * Create a volume at the configured volume_size.
+ * Create a server instance.
+ * Attach the volume to the server.
+ * Wait for the volume status to be "in-use".
+ * Extend the size of the volume and wait for the volume status to go
+ back to "in-use".
+ * Assert the volume size change is reflected in the volume API.
+ * Wait for the "compute_extend_volume" instance action event to show
+ up in the compute API with the success or failure status. We fail
+ if we timeout waiting for the instance action event to show up, or
+ if the action on the server fails.
+ """
+ # Create a test volume. Will be automatically cleaned up on teardown.
+ volume = self.create_volume()
+ # Create a test server. Will be automatically cleaned up on teardown.
+ server = self.create_server()
+ # Attach the volume to the server and wait for the volume status to be
+ # "in-use".
+ self.attach_volume(server['id'], volume['id'])
+ # Extend the size of the volume. If this is successful, the volume API
+ # will change the status on the volume to "extending" before doing an
+ # RPC cast to the volume manager on the backend. Note that we multiply
+ # the size of the volume since certain Cinder backends, e.g. ScaleIO,
+ # require multiples of 8GB.
+ extend_size = volume['size'] * 2
+ self.volumes_client.extend_volume(volume['id'], new_size=extend_size)
+ # The volume status should go back to in-use since it is still attached
+ # to the server instance.
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume['id'], 'in-use')
+ # Assert that the volume size has changed in the volume API.
+ volume = self.volumes_client.show_volume(volume['id'])['volume']
+ self.assertEqual(extend_size, volume['size'])
+ # Now we wait for the "compute_extend_volume" instance action event
+ # to show up for the server instance. This is our indication that the
+ # asynchronous operation is complete on the compute side.
+ start_time = int(time.time())
+ timeout = self.servers_client.build_timeout
+ action = self._find_extend_volume_instance_action(server['id'])
+ while action is None and int(time.time()) - start_time < timeout:
+ time.sleep(self.servers_client.build_interval)
+ action = self._find_extend_volume_instance_action(server['id'])
+
+ if action is None:
+ msg = ("Timed out waiting to get 'extend_volume' instance action "
+ "record for server %(server)s after %(timeout)s seconds." %
+ {'server': server['id'], 'timeout': timeout})
+ raise lib_exc.TimeoutException(msg)
+
+ # Now that we found the extend_volume instance action, we can wait for
+ # the compute_extend_volume instance action event to show up to
+ # indicate the operation is complete.
+ start_time = int(time.time())
+ event = self._find_extend_volume_instance_action_finish_event(action)
+ while event is None and int(time.time()) - start_time < timeout:
+ time.sleep(self.servers_client.build_interval)
+ event = self._find_extend_volume_instance_action_finish_event(
+ action)
+
+ if event is None:
+ msg = ("Timed out waiting to get 'compute_extend_volume' instance "
+ "action event record for server %(server)s and request "
+ "%(request_id)s after %(timeout)s seconds." %
+ {'server': server['id'],
+ 'request_id': action['request_id'],
+ 'timeout': timeout})
+ raise lib_exc.TimeoutException(msg)
+
+ # Finally, assert that the action completed successfully.
+ self.assertTrue(
+ event['result'].lower() == 'success',
+ "Unexpected compute_extend_volume result '%(result)s' for request "
+ "%(request_id)s." %
+ {'result': event['result'],
+ 'request_id': action['request_id']})
diff --git a/tempest/api/volume/test_volumes_get.py b/tempest/api/volume/test_volumes_get.py
index 35c8898..71db95c 100644
--- a/tempest/api/volume/test_volumes_get.py
+++ b/tempest/api/volume/test_volumes_get.py
@@ -17,53 +17,39 @@
from testtools import matchers
from tempest.api.volume import base
-from tempest.common.utils import data_utils
+from tempest.common import utils
+from tempest.common import waiters
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
CONF = config.CONF
-class VolumesV2GetTest(base.BaseVolumeTest):
-
- @classmethod
- def setup_clients(cls):
- super(VolumesV2GetTest, cls).setup_clients()
- cls.client = cls.volumes_client
-
- @classmethod
- def resource_setup(cls):
- super(VolumesV2GetTest, cls).resource_setup()
-
- cls.name_field = cls.special_fields['name_field']
- cls.descrip_field = cls.special_fields['descrip_field']
-
- def _delete_volume(self, volume_id):
- self.client.delete_volume(volume_id)
- self.client.wait_for_resource_deletion(volume_id)
+class VolumesGetTest(base.BaseVolumeTest):
def _volume_create_get_update_delete(self, **kwargs):
# Create a volume, Get it's details and Delete the volume
- volume = {}
- v_name = data_utils.rand_name('Volume')
+ v_name = data_utils.rand_name(self.__class__.__name__ + '-Volume')
metadata = {'Type': 'Test'}
# Create a volume
- kwargs[self.name_field] = v_name
+ kwargs['name'] = v_name
kwargs['metadata'] = metadata
- volume = self.client.create_volume(**kwargs)['volume']
+ volume = self.volumes_client.create_volume(**kwargs)['volume']
self.assertIn('id', volume)
- self.addCleanup(self._delete_volume, volume['id'])
- self.client.wait_for_volume_status(volume['id'], 'available')
- self.assertIn(self.name_field, volume)
- self.assertEqual(volume[self.name_field], v_name,
+ self.addCleanup(self.delete_volume, self.volumes_client, volume['id'])
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume['id'], 'available')
+ self.assertIn('name', volume)
+ self.assertEqual(volume['name'], v_name,
"The created volume name is not equal "
"to the requested name")
- self.assertTrue(volume['id'] is not None,
- "Field volume id is empty or not found.")
+
# Get Volume information
- fetched_volume = self.client.show_volume(volume['id'])['volume']
+ fetched_volume = self.volumes_client.show_volume(
+ volume['id'])['volume']
self.assertEqual(v_name,
- fetched_volume[self.name_field],
+ fetched_volume['name'],
'The fetched Volume name is different '
'from the created Volume')
self.assertEqual(volume['id'],
@@ -77,75 +63,91 @@
if 'imageRef' in kwargs:
self.assertEqual('true', fetched_volume['bootable'])
- if 'imageRef' not in kwargs:
+ else:
self.assertEqual('false', fetched_volume['bootable'])
# Update Volume
# Test volume update when display_name is same with original value
- params = {self.name_field: v_name}
- self.client.update_volume(volume['id'], **params)
+ params = {'name': v_name}
+ self.volumes_client.update_volume(volume['id'], **params)
# Test volume update when display_name is new
- new_v_name = data_utils.rand_name('new-Volume')
+ new_v_name = data_utils.rand_name(
+ self.__class__.__name__ + '-new-Volume')
new_desc = 'This is the new description of volume'
- params = {self.name_field: new_v_name,
- self.descrip_field: new_desc}
- update_volume = self.client.update_volume(
+ params = {'name': new_v_name,
+ 'description': new_desc}
+ update_volume = self.volumes_client.update_volume(
volume['id'], **params)['volume']
# Assert response body for update_volume method
- self.assertEqual(new_v_name, update_volume[self.name_field])
- self.assertEqual(new_desc, update_volume[self.descrip_field])
+ self.assertEqual(new_v_name, update_volume['name'])
+ self.assertEqual(new_desc, update_volume['description'])
# Assert response body for show_volume method
- updated_volume = self.client.show_volume(volume['id'])['volume']
+ updated_volume = self.volumes_client.show_volume(
+ volume['id'])['volume']
self.assertEqual(volume['id'], updated_volume['id'])
- self.assertEqual(new_v_name, updated_volume[self.name_field])
- self.assertEqual(new_desc, updated_volume[self.descrip_field])
+ self.assertEqual(new_v_name, updated_volume['name'])
+ self.assertEqual(new_desc, updated_volume['description'])
self.assertThat(updated_volume['metadata'].items(),
matchers.ContainsAll(metadata.items()),
'The fetched Volume metadata misses data '
'from the created Volume')
+
# Test volume create when display_name is none and display_description
# contains specific characters,
# then test volume update if display_name is duplicated
- new_volume = {}
new_v_desc = data_utils.rand_name('@#$%^* description')
- params = {self.descrip_field: new_v_desc,
- 'availability_zone': volume['availability_zone']}
- new_volume = self.client.create_volume(**params)['volume']
+ params = {'description': new_v_desc,
+ 'availability_zone': volume['availability_zone'],
+ 'size': CONF.volume.volume_size}
+ new_volume = self.volumes_client.create_volume(**params)['volume']
self.assertIn('id', new_volume)
- self.addCleanup(self._delete_volume, new_volume['id'])
- self.client.wait_for_volume_status(new_volume['id'], 'available')
+ self.addCleanup(self.delete_volume, self.volumes_client,
+ new_volume['id'])
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ new_volume['id'], 'available')
- params = {self.name_field: volume[self.name_field],
- self.descrip_field: volume[self.descrip_field]}
- self.client.update_volume(new_volume['id'], **params)
+ params = {'name': volume['name'],
+ 'description': volume['description']}
+ self.volumes_client.update_volume(new_volume['id'], **params)
if 'imageRef' in kwargs:
self.assertEqual('true', updated_volume['bootable'])
- if 'imageRef' not in kwargs:
+ else:
self.assertEqual('false', updated_volume['bootable'])
- @test.attr(type='smoke')
- @test.idempotent_id('27fb0e9f-fb64-41dd-8bdb-1ffa762f0d51')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('27fb0e9f-fb64-41dd-8bdb-1ffa762f0d51')
def test_volume_create_get_update_delete(self):
- self._volume_create_get_update_delete()
+ self._volume_create_get_update_delete(size=CONF.volume.volume_size)
- @test.attr(type='smoke')
- @test.idempotent_id('54a01030-c7fc-447c-86ee-c1182beae638')
- @test.services('image')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('54a01030-c7fc-447c-86ee-c1182beae638')
+ @utils.services('image')
def test_volume_create_get_update_delete_from_image(self):
- image = self.images_client.show_image(CONF.compute.image_ref)['image']
- min_disk = image.get('minDisk')
+ image = self.images_client.show_image(CONF.compute.image_ref)
+ min_disk = image['min_disk']
disk_size = max(min_disk, CONF.volume.volume_size)
self._volume_create_get_update_delete(
imageRef=CONF.compute.image_ref, size=disk_size)
- @test.idempotent_id('3f591b4a-7dc6-444c-bd51-77469506b3a1')
+ @decorators.idempotent_id('3f591b4a-7dc6-444c-bd51-77469506b3a1')
@testtools.skipUnless(CONF.volume_feature_enabled.clone,
'Cinder volume clones are disabled')
def test_volume_create_get_update_delete_as_clone(self):
origin = self.create_volume()
- self._volume_create_get_update_delete(source_volid=origin['id'])
+ self._volume_create_get_update_delete(source_volid=origin['id'],
+ size=CONF.volume.volume_size)
-class VolumesV1GetTest(VolumesV2GetTest):
- _api_version = 1
+class VolumesSummaryTest(base.BaseVolumeTest):
+
+ _api_version = 3
+ min_microversion = '3.12'
+ max_microversion = 'latest'
+
+ @decorators.idempotent_id('c4f2431e-4920-4736-9e00-4040386b6feb')
+ def test_show_volume_summary(self):
+ volume_summary = \
+ self.volumes_client.show_volume_summary()['volume-summary']
+ for key in ['total_size', 'total_count']:
+ self.assertIn(key, volume_summary)
diff --git a/tempest/api/volume/test_volumes_list.py b/tempest/api/volume/test_volumes_list.py
index b776494..d5358ab 100644
--- a/tempest/api/volume/test_volumes_list.py
+++ b/tempest/api/volume/test_volumes_list.py
@@ -13,38 +13,59 @@
# 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 operator
-from oslo_log import log as logging
+import operator
+import random
+
+from six.moves.urllib import parse
from testtools import matchers
from tempest.api.volume import base
-from tempest.common.utils import data_utils
-from tempest import test
-
-LOG = logging.getLogger(__name__)
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
-class VolumesV2ListTestJSON(base.BaseVolumeTest):
- # NOTE: This test creates a number of 1G volumes. To run successfully,
- # ensure that the backing file for the volume group that Nova uses
+class VolumesListTestJSON(base.BaseVolumeTest):
+ # NOTE: This test creates a number of 1G volumes. To run it successfully,
+ # ensure that the backing file for the volume group that Cinder uses
# has space for at least 3 1G volumes!
# If you are running a Devstack environment, ensure that the
# VOLUME_BACKING_FILE_SIZE is at least 4G in your localrc
VOLUME_FIELDS = ('id', 'name')
- def assertVolumesIn(self, fetched_list, expected_list, fields=None):
- if fields:
- expected_list = map(operator.itemgetter(*fields), expected_list)
- fetched_list = map(operator.itemgetter(*fields), fetched_list)
+ @classmethod
+ def _remove_volatile_fields(cls, fetched_list):
+ """Remove fields that should not be compared.
+ This method makes sure that Tempest does not compare e.g.
+ the volume's "updated_at" field that may change for any reason
+ internal to the operation of Cinder.
+ """
+ for volume in fetched_list:
+ for field in ('updated_at',):
+ if field in volume:
+ del volume[field]
+
+ def _assert_volumes_in(self, fetched_list, expected_list, fields=None):
+ """Check out the list.
+
+ This function is aim at check out whether all of the volumes in
+ expected_list are in fetched_list.
+ """
+ if fields:
+ fieldsgetter = operator.itemgetter(*fields)
+ expected_list = map(fieldsgetter, expected_list)
+ fetched_list = [fieldsgetter(item) for item in fetched_list]
+
+ # Hopefully the expected_list has already been cleaned.
+ self._remove_volatile_fields(fetched_list)
missing_vols = [v for v in expected_list if v not in fetched_list]
- if len(missing_vols) == 0:
+ if not missing_vols:
return
def str_vol(vol):
- return "%s:%s" % (vol['id'], vol[self.name])
+ return "%s:%s" % (vol['id'], vol['name'])
raw_msg = "Could not find volumes %s in expected list %s; fetched %s"
self.fail(raw_msg % ([str_vol(v) for v in missing_vols],
@@ -52,48 +73,34 @@
[str_vol(v) for v in fetched_list]))
@classmethod
- def setup_clients(cls):
- super(VolumesV2ListTestJSON, cls).setup_clients()
- cls.client = cls.volumes_client
-
- @classmethod
def resource_setup(cls):
- super(VolumesV2ListTestJSON, cls).resource_setup()
- cls.name = cls.VOLUME_FIELDS[1]
+ super(VolumesListTestJSON, cls).resource_setup()
+
+ existing_volumes = cls.volumes_client.list_volumes()['volumes']
+ cls.volume_id_list = [vol['id'] for vol in existing_volumes]
# Create 3 test volumes
cls.volume_list = []
- cls.volume_id_list = []
cls.metadata = {'Type': 'work'}
- for i in range(3):
+ for _ in range(3):
volume = cls.create_volume(metadata=cls.metadata)
- volume = cls.client.show_volume(volume['id'])['volume']
+ volume = cls.volumes_client.show_volume(volume['id'])['volume']
cls.volume_list.append(volume)
cls.volume_id_list.append(volume['id'])
-
- @classmethod
- def resource_cleanup(cls):
- # Delete the created volumes
- for volid in cls.volume_id_list:
- cls.client.delete_volume(volid)
- cls.client.wait_for_resource_deletion(volid)
- super(VolumesV2ListTestJSON, cls).resource_cleanup()
+ cls._remove_volatile_fields(cls.volume_list)
def _list_by_param_value_and_assert(self, params, with_detail=False):
"""list or list_details with given params and validates result"""
if with_detail:
fetched_vol_list = \
- self.client.list_volumes(detail=True, params=params)['volumes']
+ self.volumes_client.list_volumes(detail=True,
+ params=params)['volumes']
else:
- fetched_vol_list = self.client.list_volumes(
+ fetched_vol_list = self.volumes_client.list_volumes(
params=params)['volumes']
# Validating params of fetched volumes
- # In v2, only list detail view includes items in params.
- # In v1, list view and list detail view are same. So the
- # following check should be run when 'with_detail' is True
- # or v1 tests.
- if with_detail or self._api_version == 1:
+ if with_detail:
for volume in fetched_vol_list:
for key in params:
msg = "Failed to list volumes %s by %s" % \
@@ -106,108 +113,281 @@
else:
self.assertEqual(params[key], volume[key], msg)
- @test.attr(type='smoke')
- @test.idempotent_id('0b6ddd39-b948-471f-8038-4787978747c4')
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('0b6ddd39-b948-471f-8038-4787978747c4')
def test_volume_list(self):
# Get a list of Volumes
# Fetch all volumes
- fetched_list = self.client.list_volumes()['volumes']
- self.assertVolumesIn(fetched_list, self.volume_list,
- fields=self.VOLUME_FIELDS)
+ fetched_list = self.volumes_client.list_volumes()['volumes']
+ self._assert_volumes_in(fetched_list, self.volume_list,
+ fields=self.VOLUME_FIELDS)
- @test.idempotent_id('adcbb5a7-5ad8-4b61-bd10-5380e111a877')
+ @decorators.idempotent_id('adcbb5a7-5ad8-4b61-bd10-5380e111a877')
def test_volume_list_with_details(self):
# Get a list of Volumes with details
# Fetch all Volumes
- fetched_list = self.client.list_volumes(detail=True)['volumes']
- self.assertVolumesIn(fetched_list, self.volume_list)
+ fetched_list = self.volumes_client.list_volumes(detail=True)['volumes']
+ self._assert_volumes_in(fetched_list, self.volume_list)
- @test.idempotent_id('a28e8da4-0b56-472f-87a8-0f4d3f819c02')
+ @decorators.idempotent_id('a28e8da4-0b56-472f-87a8-0f4d3f819c02')
def test_volume_list_by_name(self):
volume = self.volume_list[data_utils.rand_int_id(0, 2)]
- params = {self.name: volume[self.name]}
- fetched_vol = self.client.list_volumes(params=params)['volumes']
+ params = {'name': volume['name']}
+ fetched_vol = self.volumes_client.list_volumes(
+ params=params)['volumes']
self.assertEqual(1, len(fetched_vol), str(fetched_vol))
- self.assertEqual(fetched_vol[0][self.name],
- volume[self.name])
+ self.assertEqual(fetched_vol[0]['name'], volume['name'])
- @test.idempotent_id('2de3a6d4-12aa-403b-a8f2-fdeb42a89623')
+ @decorators.idempotent_id('2de3a6d4-12aa-403b-a8f2-fdeb42a89623')
def test_volume_list_details_by_name(self):
volume = self.volume_list[data_utils.rand_int_id(0, 2)]
- params = {self.name: volume[self.name]}
- fetched_vol = self.client.list_volumes(
+ params = {'name': volume['name']}
+ fetched_vol = self.volumes_client.list_volumes(
detail=True, params=params)['volumes']
self.assertEqual(1, len(fetched_vol), str(fetched_vol))
- self.assertEqual(fetched_vol[0][self.name],
- volume[self.name])
+ self.assertEqual(fetched_vol[0]['name'], volume['name'])
- @test.idempotent_id('39654e13-734c-4dab-95ce-7613bf8407ce')
+ @decorators.idempotent_id('39654e13-734c-4dab-95ce-7613bf8407ce')
def test_volumes_list_by_status(self):
params = {'status': 'available'}
- fetched_list = self.client.list_volumes(params=params)['volumes']
+ fetched_list = self.volumes_client.list_volumes(
+ params=params)['volumes']
self._list_by_param_value_and_assert(params)
- self.assertVolumesIn(fetched_list, self.volume_list,
- fields=self.VOLUME_FIELDS)
+ self._assert_volumes_in(fetched_list, self.volume_list,
+ fields=self.VOLUME_FIELDS)
- @test.idempotent_id('2943f712-71ec-482a-bf49-d5ca06216b9f')
+ @decorators.idempotent_id('2943f712-71ec-482a-bf49-d5ca06216b9f')
def test_volumes_list_details_by_status(self):
params = {'status': 'available'}
- fetched_list = self.client.list_volumes(
+ fetched_list = self.volumes_client.list_volumes(
detail=True, params=params)['volumes']
for volume in fetched_list:
self.assertEqual('available', volume['status'])
- self.assertVolumesIn(fetched_list, self.volume_list)
+ self._assert_volumes_in(fetched_list, self.volume_list)
- @test.idempotent_id('c0cfa863-3020-40d7-b587-e35f597d5d87')
+ @decorators.idempotent_id('2016a942-3020-40d7-95ce-7613bf8407ce')
+ def test_volumes_list_by_bootable(self):
+ """Check out volumes.
+
+ This test function is aim at check out whether all of the volumes
+ in volume_list are not a bootable volume.
+ """
+ params = {'bootable': 'false'}
+ fetched_list = self.volumes_client.list_volumes(
+ params=params)['volumes']
+ self._list_by_param_value_and_assert(params)
+ self._assert_volumes_in(fetched_list, self.volume_list,
+ fields=self.VOLUME_FIELDS)
+
+ @decorators.idempotent_id('2016a939-72ec-482a-bf49-d5ca06216b9f')
+ def test_volumes_list_details_by_bootable(self):
+ params = {'bootable': 'false'}
+ fetched_list = self.volumes_client.list_volumes(
+ detail=True, params=params)['volumes']
+ for volume in fetched_list:
+ self.assertEqual('false', volume['bootable'])
+ self._assert_volumes_in(fetched_list, self.volume_list)
+
+ @decorators.idempotent_id('c0cfa863-3020-40d7-b587-e35f597d5d87')
def test_volumes_list_by_availability_zone(self):
volume = self.volume_list[data_utils.rand_int_id(0, 2)]
zone = volume['availability_zone']
params = {'availability_zone': zone}
- fetched_list = self.client.list_volumes(params=params)['volumes']
+ fetched_list = self.volumes_client.list_volumes(
+ params=params)['volumes']
self._list_by_param_value_and_assert(params)
- self.assertVolumesIn(fetched_list, self.volume_list,
- fields=self.VOLUME_FIELDS)
+ self._assert_volumes_in(fetched_list, self.volume_list,
+ fields=self.VOLUME_FIELDS)
- @test.idempotent_id('e1b80d13-94f0-4ba2-a40e-386af29f8db1')
+ @decorators.idempotent_id('e1b80d13-94f0-4ba2-a40e-386af29f8db1')
def test_volumes_list_details_by_availability_zone(self):
volume = self.volume_list[data_utils.rand_int_id(0, 2)]
zone = volume['availability_zone']
params = {'availability_zone': zone}
- fetched_list = self.client.list_volumes(
+ fetched_list = self.volumes_client.list_volumes(
detail=True, params=params)['volumes']
for volume in fetched_list:
self.assertEqual(zone, volume['availability_zone'])
- self.assertVolumesIn(fetched_list, self.volume_list)
+ self._assert_volumes_in(fetched_list, self.volume_list)
- @test.idempotent_id('b5ebea1b-0603-40a0-bb41-15fcd0a53214')
+ @decorators.idempotent_id('b5ebea1b-0603-40a0-bb41-15fcd0a53214')
def test_volume_list_with_param_metadata(self):
# Test to list volumes when metadata param is given
params = {'metadata': self.metadata}
self._list_by_param_value_and_assert(params)
- @test.idempotent_id('1ca92d3c-4a8e-4b43-93f5-e4c7fb3b291d')
+ @decorators.idempotent_id('1ca92d3c-4a8e-4b43-93f5-e4c7fb3b291d')
def test_volume_list_with_detail_param_metadata(self):
# Test to list volumes details when metadata param is given
params = {'metadata': self.metadata}
self._list_by_param_value_and_assert(params, with_detail=True)
- @test.idempotent_id('777c87c1-2fc4-4883-8b8e-5c0b951d1ec8')
+ @decorators.idempotent_id('777c87c1-2fc4-4883-8b8e-5c0b951d1ec8')
def test_volume_list_param_display_name_and_status(self):
# Test to list volume when display name and status param is given
volume = self.volume_list[data_utils.rand_int_id(0, 2)]
- params = {self.name: volume[self.name],
+ params = {'name': volume['name'],
'status': 'available'}
self._list_by_param_value_and_assert(params)
- @test.idempotent_id('856ab8ca-6009-4c37-b691-be1065528ad4')
+ @decorators.idempotent_id('856ab8ca-6009-4c37-b691-be1065528ad4')
def test_volume_list_with_detail_param_display_name_and_status(self):
# Test to list volume when name and status param is given
volume = self.volume_list[data_utils.rand_int_id(0, 2)]
- params = {self.name: volume[self.name],
+ params = {'name': volume['name'],
'status': 'available'}
self._list_by_param_value_and_assert(params, with_detail=True)
+ @decorators.idempotent_id('2a7064eb-b9c3-429b-b888-33928fc5edd3')
+ def test_volume_list_details_with_multiple_params(self):
+ # List volumes detail using combined condition
+ def _list_details_with_multiple_params(limit=2,
+ status='available',
+ sort_dir='asc',
+ sort_key='id'):
+ params = {'limit': limit,
+ 'status': status,
+ 'sort_dir': sort_dir,
+ 'sort_key': sort_key
+ }
+ fetched_volume = self.volumes_client.list_volumes(
+ detail=True, params=params)['volumes']
+ self.assertEqual(limit, len(fetched_volume),
+ "The count of volumes is %s, expected:%s " %
+ (len(fetched_volume), limit))
+ self.assertEqual(status, fetched_volume[0]['status'])
+ self.assertEqual(status, fetched_volume[1]['status'])
+ val0 = fetched_volume[0][sort_key]
+ val1 = fetched_volume[1][sort_key]
+ if sort_dir == 'asc':
+ self.assertLess(val0, val1,
+ "list is not in asc order with sort_key: %s."
+ " %s" % (sort_key, fetched_volume))
+ elif sort_dir == 'desc':
+ self.assertGreater(val0, val1,
+ "list is not in desc order with sort_key: "
+ "%s. %s" % (sort_key, fetched_volume))
-class VolumesV1ListTestJSON(VolumesV2ListTestJSON):
- _api_version = 1
- VOLUME_FIELDS = ('id', 'display_name')
+ _list_details_with_multiple_params()
+ _list_details_with_multiple_params(sort_dir='desc')
+
+ def _test_pagination(self, resource, ids=None, limit=1, **kwargs):
+ """Check list pagination functionality for a resource.
+
+ This method requests the list of resources and follows pagination
+ links.
+
+ If an iterable is supplied in ids it will check that all ids are
+ retrieved and that only those are listed, that we will get a next
+ link for an empty page if the number of items is divisible by used
+ limit (this is expected behavior).
+
+ We can specify number of items per request using limit argument.
+ """
+
+ # Get list method for the type of resource from the client
+ client = getattr(self, resource + '_client')
+ method = getattr(client, 'list_' + resource)
+
+ # Include limit in params for list request
+ params = kwargs.pop('params', {})
+ params['limit'] = limit
+
+ # Store remaining items we are expecting from list
+ if ids is not None:
+ remaining = list(ids)
+ else:
+ remaining = None
+
+ # Mark that the current iteration is not from a 'next' link
+ next = None
+
+ while True:
+ # Get a list page
+ response = method(params=params, **kwargs)
+
+ # If we have to check ids
+ if remaining is not None:
+ # Confirm we receive expected number of elements
+ num_expected = min(len(remaining), limit)
+ self.assertEqual(num_expected, len(response[resource]),
+ 'Requested %(#expect)d but got %(#received)d '
+ % {'#expect': num_expected,
+ '#received': len(response[resource])})
+
+ # For each received element
+ for element in response[resource]:
+ element_id = element['id']
+ # Check it's one of expected ids
+ self.assertIn(element_id,
+ ids,
+ 'Id %(id)s is not in expected ids %(ids)s' %
+ {'id': element_id, 'ids': ids})
+ # If not in remaining, we have received it twice
+ self.assertIn(element_id,
+ remaining,
+ 'Id %s was received twice' % element_id)
+ # We no longer expect it
+ remaining.remove(element_id)
+
+ # If the current iteration is from a 'next' link, check that the
+ # absolute url is the same as the one used for this request
+ if next:
+ self.assertEqual(next, response.response['content-location'])
+
+ # Get next from response
+ next = None
+ for link in response.get(resource + '_links', ()):
+ if link['rel'] == 'next':
+ next = link['href']
+ break
+
+ # Check if we have next and we shouldn't or the other way around
+ if remaining is not None:
+ if remaining or (num_expected and len(ids) % limit == 0):
+ self.assertIsNotNone(next, 'Missing link to next page')
+ else:
+ self.assertIsNone(next, 'Unexpected link to next page')
+
+ # If we can follow to the next page, get params from url to make
+ # request in the form of a relative URL
+ if next:
+ params = parse.urlparse(next).query
+
+ # If cannot follow make sure it's because we have finished
+ else:
+ self.assertEmpty(remaining or [],
+ 'No more pages reported, but still '
+ 'missing ids %s' % remaining)
+ break
+
+ @decorators.idempotent_id('e9138a2c-f67b-4796-8efa-635c196d01de')
+ def test_volume_list_details_pagination(self):
+ self._test_pagination('volumes', ids=self.volume_id_list, detail=True)
+
+ @decorators.idempotent_id('af55e775-8e4b-4feb-8719-215c43b0238c')
+ def test_volume_list_pagination(self):
+ self._test_pagination('volumes', ids=self.volume_id_list, detail=False)
+
+ @decorators.idempotent_id('46eff077-100b-427f-914e-3db2abcdb7e2')
+ def test_volume_list_with_detail_param_marker(self):
+ # Choosing a random volume from a list of volumes for 'marker'
+ # parameter
+ marker = random.choice(self.volume_id_list)
+
+ # Though Cinder volumes are returned sorted by ID by default
+ # this is implicit. Let make this explicit in case Cinder
+ # folks change their minds.
+ params = {'marker': marker, 'sort': 'id:asc'}
+
+ # Running volume list using marker parameter
+ vol_with_marker = self.volumes_client.list_volumes(
+ detail=True, params=params)['volumes']
+
+ expected_volumes_id = {
+ id for id in self.volume_id_list if id > marker
+ }
+
+ self.assertEqual(
+ expected_volumes_id, {v['id'] for v in vol_with_marker}
+ )
diff --git a/tempest/api/volume/test_volumes_negative.py b/tempest/api/volume/test_volumes_negative.py
index 0af40ea..f139283 100644
--- a/tempest/api/volume/test_volumes_negative.py
+++ b/tempest/api/volume/test_volumes_negative.py
@@ -13,292 +13,309 @@
# License for the specific language governing permissions and limitations
# under the License.
-import uuid
-
-from tempest_lib import exceptions as lib_exc
+import six
from tempest.api.volume import base
-from tempest.common.utils import data_utils
+from tempest.common import utils
from tempest.common import waiters
-from tempest import test
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+CONF = config.CONF
-class VolumesV2NegativeTest(base.BaseVolumeTest):
-
- @classmethod
- def setup_clients(cls):
- super(VolumesV2NegativeTest, cls).setup_clients()
- cls.client = cls.volumes_client
+class VolumesNegativeTest(base.BaseVolumeTest):
@classmethod
def resource_setup(cls):
- super(VolumesV2NegativeTest, cls).resource_setup()
-
- cls.name_field = cls.special_fields['name_field']
+ super(VolumesNegativeTest, cls).resource_setup()
# Create a test shared instance and volume for attach/detach tests
cls.volume = cls.create_volume()
- cls.mountpoint = "/dev/vdc"
- @test.attr(type=['negative'])
- @test.idempotent_id('f131c586-9448-44a4-a8b0-54ca838aa43e')
+ def create_image(self):
+ # Create image
+ image_name = data_utils.rand_name(self.__class__.__name__ + "-image")
+ image = self.images_client.create_image(
+ name=image_name,
+ container_format=CONF.image.container_formats[0],
+ disk_format=CONF.image.disk_formats[0],
+ visibility='private',
+ min_disk=CONF.volume.volume_size + 1)
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.images_client.delete_image, image['id'])
+
+ # Upload image with 1KB data
+ image_file = six.BytesIO(data_utils.random_bytes())
+ self.images_client.store_image_file(image['id'], image_file)
+ waiters.wait_for_image_status(self.images_client,
+ image['id'], 'active')
+ return image
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('f131c586-9448-44a4-a8b0-54ca838aa43e')
def test_volume_get_nonexistent_volume_id(self):
# Should not be able to get a non-existent volume
- self.assertRaises(lib_exc.NotFound, self.client.show_volume,
- str(uuid.uuid4()))
+ self.assertRaises(lib_exc.NotFound, self.volumes_client.show_volume,
+ data_utils.rand_uuid())
- @test.attr(type=['negative'])
- @test.idempotent_id('555efa6e-efcd-44ef-8a3b-4a7ca4837a29')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('555efa6e-efcd-44ef-8a3b-4a7ca4837a29')
def test_volume_delete_nonexistent_volume_id(self):
# Should not be able to delete a non-existent Volume
- self.assertRaises(lib_exc.NotFound, self.client.delete_volume,
- str(uuid.uuid4()))
+ self.assertRaises(lib_exc.NotFound, self.volumes_client.delete_volume,
+ data_utils.rand_uuid())
- @test.attr(type=['negative'])
- @test.idempotent_id('1ed83a8a-682d-4dfb-a30e-ee63ffd6c049')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('1ed83a8a-682d-4dfb-a30e-ee63ffd6c049')
def test_create_volume_with_invalid_size(self):
- # Should not be able to create volume with invalid size
- # in request
- v_name = data_utils.rand_name('Volume')
- metadata = {'Type': 'work'}
- self.assertRaises(lib_exc.BadRequest, self.client.create_volume,
- size='#$%', display_name=v_name, metadata=metadata)
+ # Should not be able to create volume with invalid size in request
+ self.assertRaises(lib_exc.BadRequest,
+ self.volumes_client.create_volume, size='#$%')
- @test.attr(type=['negative'])
- @test.idempotent_id('9387686f-334f-4d31-a439-33494b9e2683')
- def test_create_volume_with_out_passing_size(self):
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('9387686f-334f-4d31-a439-33494b9e2683')
+ def test_create_volume_without_passing_size(self):
# Should not be able to create volume without passing size
# in request
- v_name = data_utils.rand_name('Volume')
- metadata = {'Type': 'work'}
- self.assertRaises(lib_exc.BadRequest, self.client.create_volume,
- size='', display_name=v_name, metadata=metadata)
+ self.assertRaises(lib_exc.BadRequest,
+ self.volumes_client.create_volume, size='')
- @test.attr(type=['negative'])
- @test.idempotent_id('41331caa-eaf4-4001-869d-bc18c1869360')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('41331caa-eaf4-4001-869d-bc18c1869360')
def test_create_volume_with_size_zero(self):
# Should not be able to create volume with size zero
- v_name = data_utils.rand_name('Volume')
- metadata = {'Type': 'work'}
- self.assertRaises(lib_exc.BadRequest, self.client.create_volume,
- size='0', display_name=v_name, metadata=metadata)
+ self.assertRaises(lib_exc.BadRequest,
+ self.volumes_client.create_volume, size='0')
- @test.attr(type=['negative'])
- @test.idempotent_id('8b472729-9eba-446e-a83b-916bdb34bef7')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('8b472729-9eba-446e-a83b-916bdb34bef7')
def test_create_volume_with_size_negative(self):
# Should not be able to create volume with size negative
- v_name = data_utils.rand_name('Volume')
- metadata = {'Type': 'work'}
- self.assertRaises(lib_exc.BadRequest, self.client.create_volume,
- size='-1', display_name=v_name, metadata=metadata)
+ self.assertRaises(lib_exc.BadRequest,
+ self.volumes_client.create_volume, size='-1')
- @test.attr(type=['negative'])
- @test.idempotent_id('10254ed8-3849-454e-862e-3ab8e6aa01d2')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('10254ed8-3849-454e-862e-3ab8e6aa01d2')
def test_create_volume_with_nonexistent_volume_type(self):
# Should not be able to create volume with non-existent volume type
- v_name = data_utils.rand_name('Volume')
- metadata = {'Type': 'work'}
- self.assertRaises(lib_exc.NotFound, self.client.create_volume,
- size='1', volume_type=str(uuid.uuid4()),
- display_name=v_name, metadata=metadata)
+ self.assertRaises(lib_exc.NotFound, self.volumes_client.create_volume,
+ size='1', volume_type=data_utils.rand_uuid())
- @test.attr(type=['negative'])
- @test.idempotent_id('0c36f6ae-4604-4017-b0a9-34fdc63096f9')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('0c36f6ae-4604-4017-b0a9-34fdc63096f9')
def test_create_volume_with_nonexistent_snapshot_id(self):
# Should not be able to create volume with non-existent snapshot
- v_name = data_utils.rand_name('Volume')
- metadata = {'Type': 'work'}
- self.assertRaises(lib_exc.NotFound, self.client.create_volume,
- size='1', snapshot_id=str(uuid.uuid4()),
- display_name=v_name, metadata=metadata)
+ self.assertRaises(lib_exc.NotFound, self.volumes_client.create_volume,
+ size='1', snapshot_id=data_utils.rand_uuid())
- @test.attr(type=['negative'])
- @test.idempotent_id('47c73e08-4be8-45bb-bfdf-0c4e79b88344')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('47c73e08-4be8-45bb-bfdf-0c4e79b88344')
def test_create_volume_with_nonexistent_source_volid(self):
# Should not be able to create volume with non-existent source volume
- v_name = data_utils.rand_name('Volume')
- metadata = {'Type': 'work'}
- self.assertRaises(lib_exc.NotFound, self.client.create_volume,
- size='1', source_volid=str(uuid.uuid4()),
- display_name=v_name, metadata=metadata)
+ self.assertRaises(lib_exc.NotFound, self.volumes_client.create_volume,
+ size='1', source_volid=data_utils.rand_uuid())
- @test.attr(type=['negative'])
- @test.idempotent_id('0186422c-999a-480e-a026-6a665744c30c')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('0186422c-999a-480e-a026-6a665744c30c')
def test_update_volume_with_nonexistent_volume_id(self):
- v_name = data_utils.rand_name('Volume')
- metadata = {'Type': 'work'}
- self.assertRaises(lib_exc.NotFound, self.client.update_volume,
- volume_id=str(uuid.uuid4()), display_name=v_name,
- metadata=metadata)
+ self.assertRaises(lib_exc.NotFound, self.volumes_client.update_volume,
+ volume_id=data_utils.rand_uuid())
- @test.attr(type=['negative'])
- @test.idempotent_id('e66e40d6-65e6-4e75-bdc7-636792fa152d')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('e66e40d6-65e6-4e75-bdc7-636792fa152d')
def test_update_volume_with_invalid_volume_id(self):
- v_name = data_utils.rand_name('Volume')
- metadata = {'Type': 'work'}
- self.assertRaises(lib_exc.NotFound, self.client.update_volume,
- volume_id='#$%%&^&^', display_name=v_name,
- metadata=metadata)
+ self.assertRaises(lib_exc.NotFound, self.volumes_client.update_volume,
+ volume_id=data_utils.rand_name('invalid'))
- @test.attr(type=['negative'])
- @test.idempotent_id('72aeca85-57a5-4c1f-9057-f320f9ea575b')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('72aeca85-57a5-4c1f-9057-f320f9ea575b')
def test_update_volume_with_empty_volume_id(self):
- v_name = data_utils.rand_name('Volume')
- metadata = {'Type': 'work'}
- self.assertRaises(lib_exc.NotFound, self.client.update_volume,
- volume_id='', display_name=v_name,
- metadata=metadata)
+ self.assertRaises(lib_exc.NotFound, self.volumes_client.update_volume,
+ volume_id='')
- @test.attr(type=['negative'])
- @test.idempotent_id('30799cfd-7ee4-446c-b66c-45b383ed211b')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('30799cfd-7ee4-446c-b66c-45b383ed211b')
def test_get_invalid_volume_id(self):
# Should not be able to get volume with invalid id
- self.assertRaises(lib_exc.NotFound, self.client.show_volume,
- '#$%%&^&^')
+ self.assertRaises(lib_exc.NotFound, self.volumes_client.show_volume,
+ data_utils.rand_name('invalid'))
- @test.attr(type=['negative'])
- @test.idempotent_id('c6c3db06-29ad-4e91-beb0-2ab195fe49e3')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('c6c3db06-29ad-4e91-beb0-2ab195fe49e3')
def test_get_volume_without_passing_volume_id(self):
# Should not be able to get volume when empty ID is passed
- self.assertRaises(lib_exc.NotFound, self.client.show_volume, '')
+ self.assertRaises(lib_exc.NotFound,
+ self.volumes_client.show_volume, '')
- @test.attr(type=['negative'])
- @test.idempotent_id('1f035827-7c32-4019-9240-b4ec2dbd9dfd')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('1f035827-7c32-4019-9240-b4ec2dbd9dfd')
def test_delete_invalid_volume_id(self):
# Should not be able to delete volume when invalid ID is passed
- self.assertRaises(lib_exc.NotFound, self.client.delete_volume,
- '!@#$%^&*()')
+ self.assertRaises(lib_exc.NotFound, self.volumes_client.delete_volume,
+ data_utils.rand_name('invalid'))
- @test.attr(type=['negative'])
- @test.idempotent_id('441a1550-5d44-4b30-af0f-a6d402f52026')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('441a1550-5d44-4b30-af0f-a6d402f52026')
def test_delete_volume_without_passing_volume_id(self):
# Should not be able to delete volume when empty ID is passed
- self.assertRaises(lib_exc.NotFound, self.client.delete_volume, '')
+ self.assertRaises(lib_exc.NotFound,
+ self.volumes_client.delete_volume, '')
- @test.attr(type=['negative'])
- @test.idempotent_id('f5e56b0a-5d02-43c1-a2a7-c9b792c2e3f6')
- @test.services('compute')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('f5e56b0a-5d02-43c1-a2a7-c9b792c2e3f6')
+ @utils.services('compute')
def test_attach_volumes_with_nonexistent_volume_id(self):
- srv_name = data_utils.rand_name('Instance')
- server = self.create_server(
- name=srv_name,
- wait_until='ACTIVE')
- self.addCleanup(waiters.wait_for_server_termination,
- self.servers_client, server['id'])
- self.addCleanup(self.servers_client.delete_server, server['id'])
+ server = self.create_server()
self.assertRaises(lib_exc.NotFound,
- self.client.attach_volume,
- str(uuid.uuid4()),
- server['id'],
- self.mountpoint)
+ self.volumes_client.attach_volume,
+ data_utils.rand_uuid(),
+ instance_uuid=server['id'],
+ mountpoint="/dev/vdc")
- @test.attr(type=['negative'])
- @test.idempotent_id('9f9c24e4-011d-46b5-b992-952140ce237a')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('9f9c24e4-011d-46b5-b992-952140ce237a')
def test_detach_volumes_with_invalid_volume_id(self):
self.assertRaises(lib_exc.NotFound,
- self.client.detach_volume,
+ self.volumes_client.detach_volume,
'xxx')
- @test.attr(type=['negative'])
- @test.idempotent_id('e0c75c74-ee34-41a9-9288-2a2051452854')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('e0c75c74-ee34-41a9-9288-2a2051452854')
def test_volume_extend_with_size_smaller_than_original_size(self):
# Extend volume with smaller size than original size.
extend_size = 0
- self.assertRaises(lib_exc.BadRequest, self.client.extend_volume,
- self.volume['id'], extend_size)
+ self.assertRaises(lib_exc.BadRequest,
+ self.volumes_client.extend_volume,
+ self.volume['id'], new_size=extend_size)
- @test.attr(type=['negative'])
- @test.idempotent_id('5d0b480d-e833-439f-8a5a-96ad2ed6f22f')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('5d0b480d-e833-439f-8a5a-96ad2ed6f22f')
def test_volume_extend_with_non_number_size(self):
# Extend volume when size is non number.
extend_size = 'abc'
- self.assertRaises(lib_exc.BadRequest, self.client.extend_volume,
- self.volume['id'], extend_size)
+ self.assertRaises(lib_exc.BadRequest,
+ self.volumes_client.extend_volume,
+ self.volume['id'], new_size=extend_size)
- @test.attr(type=['negative'])
- @test.idempotent_id('355218f1-8991-400a-a6bb-971239287d92')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('355218f1-8991-400a-a6bb-971239287d92')
def test_volume_extend_with_None_size(self):
# Extend volume with None size.
extend_size = None
- self.assertRaises(lib_exc.BadRequest, self.client.extend_volume,
- self.volume['id'], extend_size)
+ self.assertRaises(lib_exc.BadRequest,
+ self.volumes_client.extend_volume,
+ self.volume['id'], new_size=extend_size)
- @test.attr(type=['negative'])
- @test.idempotent_id('8f05a943-013c-4063-ac71-7baf561e82eb')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('8f05a943-013c-4063-ac71-7baf561e82eb')
def test_volume_extend_with_nonexistent_volume_id(self):
# Extend volume size when volume is nonexistent.
- extend_size = int(self.volume['size']) + 1
- self.assertRaises(lib_exc.NotFound, self.client.extend_volume,
- str(uuid.uuid4()), extend_size)
+ extend_size = self.volume['size'] + 1
+ self.assertRaises(lib_exc.NotFound, self.volumes_client.extend_volume,
+ data_utils.rand_uuid(), new_size=extend_size)
- @test.attr(type=['negative'])
- @test.idempotent_id('aff8ba64-6d6f-4f2e-bc33-41a08ee9f115')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('aff8ba64-6d6f-4f2e-bc33-41a08ee9f115')
def test_volume_extend_without_passing_volume_id(self):
# Extend volume size when passing volume id is None.
- extend_size = int(self.volume['size']) + 1
- self.assertRaises(lib_exc.NotFound, self.client.extend_volume,
- None, extend_size)
+ extend_size = self.volume['size'] + 1
+ self.assertRaises(lib_exc.NotFound, self.volumes_client.extend_volume,
+ None, new_size=extend_size)
- @test.attr(type=['negative'])
- @test.idempotent_id('ac6084c0-0546-45f9-b284-38a367e0e0e2')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('ac6084c0-0546-45f9-b284-38a367e0e0e2')
def test_reserve_volume_with_nonexistent_volume_id(self):
self.assertRaises(lib_exc.NotFound,
- self.client.reserve_volume,
- str(uuid.uuid4()))
+ self.volumes_client.reserve_volume,
+ data_utils.rand_uuid())
- @test.attr(type=['negative'])
- @test.idempotent_id('eb467654-3dc1-4a72-9b46-47c29d22654c')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('eb467654-3dc1-4a72-9b46-47c29d22654c')
def test_unreserve_volume_with_nonexistent_volume_id(self):
self.assertRaises(lib_exc.NotFound,
- self.client.unreserve_volume,
- str(uuid.uuid4()))
+ self.volumes_client.unreserve_volume,
+ data_utils.rand_uuid())
- @test.attr(type=['negative'])
- @test.idempotent_id('449c4ed2-ecdd-47bb-98dc-072aeccf158c')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('449c4ed2-ecdd-47bb-98dc-072aeccf158c')
def test_reserve_volume_with_negative_volume_status(self):
# Mark volume as reserved.
- self.client.reserve_volume(self.volume['id'])
+ self.volumes_client.reserve_volume(self.volume['id'])
# Mark volume which is marked as reserved before
self.assertRaises(lib_exc.BadRequest,
- self.client.reserve_volume,
+ self.volumes_client.reserve_volume,
self.volume['id'])
# Unmark volume as reserved.
- self.client.unreserve_volume(self.volume['id'])
+ self.volumes_client.unreserve_volume(self.volume['id'])
- @test.attr(type=['negative'])
- @test.idempotent_id('0f4aa809-8c7b-418f-8fb3-84c7a5dfc52f')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('0f4aa809-8c7b-418f-8fb3-84c7a5dfc52f')
def test_list_volumes_with_nonexistent_name(self):
- v_name = data_utils.rand_name('Volume')
- params = {self.name_field: v_name}
- fetched_volume = self.client.list_volumes(params=params)['volumes']
- self.assertEqual(0, len(fetched_volume))
+ v_name = data_utils.rand_name(self.__class__.__name__ + '-Volume')
+ params = {'name': v_name}
+ fetched_volume = self.volumes_client.list_volumes(
+ params=params)['volumes']
+ self.assertEmpty(fetched_volume)
- @test.attr(type=['negative'])
- @test.idempotent_id('9ca17820-a0e7-4cbd-a7fa-f4468735e359')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('9ca17820-a0e7-4cbd-a7fa-f4468735e359')
def test_list_volumes_detail_with_nonexistent_name(self):
- v_name = data_utils.rand_name('Volume')
- params = {self.name_field: v_name}
+ v_name = data_utils.rand_name(self.__class__.__name__ + '-Volume')
+ params = {'name': v_name}
fetched_volume = \
- self.client.list_volumes(detail=True, params=params)['volumes']
- self.assertEqual(0, len(fetched_volume))
+ self.volumes_client.list_volumes(
+ detail=True, params=params)['volumes']
+ self.assertEmpty(fetched_volume)
- @test.attr(type=['negative'])
- @test.idempotent_id('143b279b-7522-466b-81be-34a87d564a7c')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('143b279b-7522-466b-81be-34a87d564a7c')
def test_list_volumes_with_invalid_status(self):
params = {'status': 'null'}
- fetched_volume = self.client.list_volumes(params=params)['volumes']
- self.assertEqual(0, len(fetched_volume))
+ fetched_volume = self.volumes_client.list_volumes(
+ params=params)['volumes']
+ self.assertEmpty(fetched_volume)
- @test.attr(type=['negative'])
- @test.idempotent_id('ba94b27b-be3f-496c-a00e-0283b373fa75')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('ba94b27b-be3f-496c-a00e-0283b373fa75')
def test_list_volumes_detail_with_invalid_status(self):
params = {'status': 'null'}
fetched_volume = \
- self.client.list_volumes(detail=True, params=params)['volumes']
- self.assertEqual(0, len(fetched_volume))
+ self.volumes_client.list_volumes(detail=True,
+ params=params)['volumes']
+ self.assertEmpty(fetched_volume)
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('5b810c91-0ad1-47ce-aee8-615f789be78f')
+ @utils.services('image')
+ def test_create_volume_from_image_with_decreasing_size(self):
+ # Create image
+ image = self.create_image()
-class VolumesV1NegativeTest(VolumesV2NegativeTest):
- _api_version = 1
- _name = 'display_name'
+ # Note(jeremyZ): To shorten the test time (uploading a big size image
+ # is time-consuming), here just consider the scenario that volume size
+ # is smaller than the min_disk of image.
+ self.assertRaises(lib_exc.BadRequest,
+ self.volumes_client.create_volume,
+ size=CONF.volume.volume_size,
+ imageRef=image['id'])
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('d15e7f35-2cfc-48c8-9418-c8223a89bcbb')
+ @utils.services('image')
+ def test_create_volume_from_deactivated_image(self):
+ # Create image
+ image = self.create_image()
+
+ # Deactivate the image
+ self.images_client.deactivate_image(image['id'])
+ body = self.images_client.show_image(image['id'])
+ self.assertEqual("deactivated", body['status'])
+ # Try creating a volume from deactivated image
+ self.assertRaises(lib_exc.BadRequest,
+ self.create_volume,
+ imageRef=image['id'])
diff --git a/tempest/api/volume/test_volumes_snapshots.py b/tempest/api/volume/test_volumes_snapshots.py
index 856adcc..93638b8 100644
--- a/tempest/api/volume/test_volumes_snapshots.py
+++ b/tempest/api/volume/test_volumes_snapshots.py
@@ -10,87 +10,87 @@
# License for the specific language governing permissions and limitations
# under the License.
-from oslo_log import log as logging
+import testtools
+from testtools import matchers
from tempest.api.volume import base
-from tempest.common.utils import data_utils
+from tempest.common import utils
+from tempest.common import waiters
from tempest import config
-from tempest import test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
-LOG = logging.getLogger(__name__)
CONF = config.CONF
-class VolumesV2SnapshotTestJSON(base.BaseVolumeTest):
+class VolumesSnapshotTestJSON(base.BaseVolumeTest):
@classmethod
def skip_checks(cls):
- super(VolumesV2SnapshotTestJSON, cls).skip_checks()
+ super(VolumesSnapshotTestJSON, cls).skip_checks()
if not CONF.volume_feature_enabled.snapshot:
raise cls.skipException("Cinder volume snapshots are disabled")
@classmethod
def resource_setup(cls):
- super(VolumesV2SnapshotTestJSON, cls).resource_setup()
+ super(VolumesSnapshotTestJSON, cls).resource_setup()
cls.volume_origin = cls.create_volume()
- cls.name_field = cls.special_fields['name_field']
- cls.descrip_field = cls.special_fields['descrip_field']
-
- def _detach(self, volume_id):
- """Detach volume."""
- self.volumes_client.detach_volume(volume_id)
- self.volumes_client.wait_for_volume_status(volume_id, 'available')
-
- def _list_by_param_values_and_assert(self, params, with_detail=False):
- """list or list_details with given params and validates result."""
-
- if with_detail:
- fetched_snap_list = self.snapshots_client.list_snapshots(
- detail=True, params=params)['snapshots']
- else:
- fetched_snap_list = self.snapshots_client.list_snapshots(
- params=params)['snapshots']
-
- # Validating params of fetched snapshots
- for snap in fetched_snap_list:
- for key in params:
- msg = "Failed to list snapshots %s by %s" % \
- ('details' if with_detail else '', key)
- self.assertEqual(params[key], snap[key], msg)
-
- @test.idempotent_id('b467b54c-07a4-446d-a1cf-651dedcc3ff1')
- @test.services('compute')
- def test_snapshot_create_with_volume_in_use(self):
- # Create a snapshot when volume status is in-use
+ @decorators.idempotent_id('8567b54c-4455-446d-a1cf-651ddeaa3ff2')
+ @utils.services('compute')
+ def test_snapshot_create_delete_with_volume_in_use(self):
# Create a test instance
- server_name = data_utils.rand_name('instance')
- server = self.create_server(
- name=server_name,
- wait_until='ACTIVE')
- self.addCleanup(self.servers_client.delete_server, server['id'])
- mountpoint = '/dev/%s' % CONF.compute.volume_device_name
- self.servers_client.attach_volume(
- server['id'], volumeId=self.volume_origin['id'],
- device=mountpoint)
- self.volumes_client.wait_for_volume_status(self.volume_origin['id'],
- 'in-use')
- self.addCleanup(self.volumes_client.wait_for_volume_status,
- self.volume_origin['id'], 'available')
- self.addCleanup(self.servers_client.detach_volume, server['id'],
- self.volume_origin['id'])
- # Snapshot a volume even if it's attached to an instance
- snapshot = self.create_snapshot(self.volume_origin['id'],
- force=True)
- # Delete the snapshot
- self.cleanup_snapshot(snapshot)
+ server = self.create_server()
+ # 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'])
- @test.idempotent_id('2a8abbe4-d871-46db-b049-c41f5af8216e')
+ # Snapshot a volume which attached to an instance with force=False
+ self.assertRaises(lib_exc.BadRequest, self.create_snapshot,
+ volume['id'], force=False)
+
+ # Snapshot a volume attached to an instance
+ 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.
+ self.delete_snapshot(snapshot1['id'])
+ self.delete_snapshot(snapshot3['id'])
+ self.delete_snapshot(snapshot2['id'])
+
+ @decorators.idempotent_id('5210a1de-85a0-11e6-bb21-641c676a5d61')
+ @utils.services('compute')
+ def test_snapshot_create_offline_delete_online(self):
+
+ # Create a snapshot while it is not attached
+ snapshot1 = self.create_snapshot(self.volume_origin['id'])
+
+ # Create a server and attach it
+ server = self.create_server()
+ self.attach_volume(server['id'], self.volume_origin['id'])
+
+ # Now that the volume is attached, create another snapshots
+ snapshot2 = self.create_snapshot(self.volume_origin['id'], force=True)
+ snapshot3 = self.create_snapshot(self.volume_origin['id'], force=True)
+
+ # Delete the snapshots. Some snapshot implementations can take
+ # different paths according to order they are deleted.
+ self.delete_snapshot(snapshot3['id'])
+ self.delete_snapshot(snapshot1['id'])
+ self.delete_snapshot(snapshot2['id'])
+
+ @decorators.idempotent_id('2a8abbe4-d871-46db-b049-c41f5af8216e')
def test_snapshot_create_get_list_update_delete(self):
- # Create a snapshot
- s_name = data_utils.rand_name('snap')
- params = {self.name_field: s_name}
- snapshot = self.create_snapshot(self.volume_origin['id'], **params)
+ # Create a snapshot with metadata
+ metadata = {"snap-meta1": "value1",
+ "snap-meta2": "value2",
+ "snap-meta3": "value3"}
+ snapshot = self.create_snapshot(self.volume_origin['id'],
+ metadata=metadata)
# Get the snap and check for some of its details
snap_get = self.snapshots_client.show_snapshot(
@@ -98,93 +98,74 @@
self.assertEqual(self.volume_origin['id'],
snap_get['volume_id'],
"Referred volume origin mismatch")
+ self.assertEqual(self.volume_origin['size'], snap_get['size'])
+
+ # Verify snapshot metadata
+ self.assertThat(snap_get['metadata'].items(),
+ matchers.ContainsAll(metadata.items()))
# Compare also with the output from the list action
- tracking_data = (snapshot['id'], snapshot[self.name_field])
+ tracking_data = (snapshot['id'], snapshot['name'])
snaps_list = self.snapshots_client.list_snapshots()['snapshots']
- snaps_data = [(f['id'], f[self.name_field]) for f in snaps_list]
+ snaps_data = [(f['id'], f['name']) for f in snaps_list]
self.assertIn(tracking_data, snaps_data)
# Updates snapshot with new values
- new_s_name = data_utils.rand_name('new-snap')
+ new_s_name = data_utils.rand_name(
+ self.__class__.__name__ + '-new-snap')
new_desc = 'This is the new description of snapshot.'
- params = {self.name_field: new_s_name,
- self.descrip_field: new_desc}
+ params = {'name': new_s_name,
+ 'description': new_desc}
update_snapshot = self.snapshots_client.update_snapshot(
snapshot['id'], **params)['snapshot']
# Assert response body for update_snapshot method
- self.assertEqual(new_s_name, update_snapshot[self.name_field])
- self.assertEqual(new_desc, update_snapshot[self.descrip_field])
+ self.assertEqual(new_s_name, update_snapshot['name'])
+ self.assertEqual(new_desc, update_snapshot['description'])
# Assert response body for show_snapshot method
updated_snapshot = self.snapshots_client.show_snapshot(
snapshot['id'])['snapshot']
- self.assertEqual(new_s_name, updated_snapshot[self.name_field])
- self.assertEqual(new_desc, updated_snapshot[self.descrip_field])
+ self.assertEqual(new_s_name, updated_snapshot['name'])
+ self.assertEqual(new_desc, updated_snapshot['description'])
# Delete the snapshot
- self.cleanup_snapshot(snapshot)
+ self.delete_snapshot(snapshot['id'])
- @test.idempotent_id('59f41f43-aebf-48a9-ab5d-d76340fab32b')
- def test_snapshots_list_with_params(self):
- """list snapshots with params."""
- # Create a snapshot
- display_name = data_utils.rand_name('snap')
- params = {self.name_field: display_name}
- snapshot = self.create_snapshot(self.volume_origin['id'], **params)
- self.addCleanup(self.cleanup_snapshot, snapshot)
-
- # Verify list snapshots by display_name filter
- params = {self.name_field: snapshot[self.name_field]}
- self._list_by_param_values_and_assert(params)
-
- # Verify list snapshots by status filter
- params = {'status': 'available'}
- self._list_by_param_values_and_assert(params)
-
- # Verify list snapshots by status and display name filter
- params = {'status': 'available',
- self.name_field: snapshot[self.name_field]}
- self._list_by_param_values_and_assert(params)
-
- @test.idempotent_id('220a1022-1fcd-4a74-a7bd-6b859156cda2')
- def test_snapshots_list_details_with_params(self):
- """list snapshot details with params."""
- # Create a snapshot
- display_name = data_utils.rand_name('snap')
- params = {self.name_field: display_name}
- snapshot = self.create_snapshot(self.volume_origin['id'], **params)
- self.addCleanup(self.cleanup_snapshot, snapshot)
-
- # Verify list snapshot details by display_name filter
- params = {self.name_field: snapshot[self.name_field]}
- self._list_by_param_values_and_assert(params, with_detail=True)
- # Verify list snapshot details by status filter
- params = {'status': 'available'}
- self._list_by_param_values_and_assert(params, with_detail=True)
- # Verify list snapshot details by status and display name filter
- params = {'status': 'available',
- self.name_field: snapshot[self.name_field]}
- self._list_by_param_values_and_assert(params, with_detail=True)
-
- @test.idempotent_id('677863d1-3142-456d-b6ac-9924f667a7f4')
+ @decorators.idempotent_id('677863d1-3142-456d-b6ac-9924f667a7f4')
def test_volume_from_snapshot(self):
- # Create a temporary snap using wrapper method from base, then
- # create a snap based volume and deletes it
- snapshot = self.create_snapshot(self.volume_origin['id'])
- # NOTE(gfidente): size is required also when passing snapshot_id
- volume = self.volumes_client.create_volume(
- snapshot_id=snapshot['id'])['volume']
- self.volumes_client.wait_for_volume_status(volume['id'], 'available')
- self.volumes_client.delete_volume(volume['id'])
- self.volumes_client.wait_for_resource_deletion(volume['id'])
- self.cleanup_snapshot(snapshot)
+ # Creates a volume from a snapshot passing a size
+ # different from the source
+ src_size = CONF.volume.volume_size
- def cleanup_snapshot(self, snapshot):
- # Delete the snapshot
- self.snapshots_client.delete_snapshot(snapshot['id'])
- self.snapshots_client.wait_for_resource_deletion(snapshot['id'])
- self.snapshots.remove(snapshot)
+ src_vol = self.create_volume(size=src_size)
+ src_snap = self.create_snapshot(src_vol['id'])
+ # Destination volume bigger than source snapshot
+ dst_vol = self.create_volume(snapshot_id=src_snap['id'],
+ size=src_size + 1)
+ # NOTE(zhufl): dst_vol is created based on snapshot, so dst_vol
+ # should be deleted before deleting snapshot, otherwise deleting
+ # snapshot will end with status 'error-deleting'. This depends on
+ # the implementation mechanism of vendors, generally speaking,
+ # some verdors will use "virtual disk clone" which will promote
+ # disk clone speed, and in this situation the "disk clone"
+ # is just a relationship between volume and snapshot.
+ self.addCleanup(self.delete_volume, self.volumes_client, dst_vol['id'])
+ volume = self.volumes_client.show_volume(dst_vol['id'])['volume']
+ # Should allow
+ self.assertEqual(volume['snapshot_id'], src_snap['id'])
+ self.assertEqual(volume['size'], src_size + 1)
-class VolumesV1SnapshotTestJSON(VolumesV2SnapshotTestJSON):
- _api_version = 1
+ @decorators.idempotent_id('bbcfa285-af7f-479e-8c1a-8c34fc16543c')
+ @testtools.skipUnless(CONF.volume_feature_enabled.backup,
+ "Cinder backup is disabled")
+ def test_snapshot_backup(self):
+ # Create a snapshot
+ snapshot = self.create_snapshot(volume_id=self.volume_origin['id'])
+
+ backup = self.create_backup(volume_id=self.volume_origin['id'],
+ snapshot_id=snapshot['id'])
+ waiters.wait_for_volume_resource_status(self.snapshots_client,
+ snapshot['id'], 'available')
+ backup_info = self.backups_client.show_backup(backup['id'])['backup']
+ self.assertEqual(self.volume_origin['id'], backup_info['volume_id'])
+ self.assertEqual(snapshot['id'], backup_info['snapshot_id'])
diff --git a/tempest/api/volume/test_volumes_snapshots_list.py b/tempest/api/volume/test_volumes_snapshots_list.py
new file mode 100644
index 0000000..f12bfd8
--- /dev/null
+++ b/tempest/api/volume/test_volumes_snapshots_list.py
@@ -0,0 +1,162 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from tempest.api.volume import base
+from tempest import config
+from tempest.lib import decorators
+
+CONF = config.CONF
+
+
+class VolumesSnapshotListTestJSON(base.BaseVolumeTest):
+
+ @classmethod
+ def skip_checks(cls):
+ super(VolumesSnapshotListTestJSON, cls).skip_checks()
+ if not CONF.volume_feature_enabled.snapshot:
+ raise cls.skipException("Cinder volume snapshots are disabled")
+
+ @classmethod
+ def resource_setup(cls):
+ super(VolumesSnapshotListTestJSON, cls).resource_setup()
+ volume_origin = cls.create_volume()
+
+ # Create snapshots with params
+ for _ in range(3):
+ snapshot = cls.create_snapshot(volume_origin['id'])
+ cls.snapshot = snapshot
+
+ def _list_by_param_values_and_assert(self, with_detail=False, **params):
+ """list or list_details with given params and validates result."""
+
+ fetched_snap_list = self.snapshots_client.list_snapshots(
+ detail=with_detail, **params)['snapshots']
+
+ # Validating params of fetched snapshots
+ for snap in fetched_snap_list:
+ for key in params:
+ msg = "Failed to list snapshots %s by %s" % \
+ ('details' if with_detail else '', key)
+ self.assertEqual(params[key], snap[key], msg)
+
+ def _list_snapshots_by_param_limit(self, limit, expected_elements):
+ """list snapshots by limit param"""
+ # Get snapshots list using limit parameter
+ fetched_snap_list = self.snapshots_client.list_snapshots(
+ limit=limit)['snapshots']
+ # Validating filtered snapshots length equals to expected_elements
+ self.assertEqual(expected_elements, len(fetched_snap_list))
+
+ @decorators.idempotent_id('59f41f43-aebf-48a9-ab5d-d76340fab32b')
+ def test_snapshots_list_with_params(self):
+ """list snapshots with params."""
+ # Verify list snapshots by display_name filter
+ params = {'name': self.snapshot['name']}
+ self._list_by_param_values_and_assert(**params)
+
+ # Verify list snapshots by status filter
+ params = {'status': 'available'}
+ self._list_by_param_values_and_assert(**params)
+
+ # Verify list snapshots by status and display name filter
+ params = {'status': 'available',
+ 'name': self.snapshot['name']}
+ self._list_by_param_values_and_assert(**params)
+
+ @decorators.idempotent_id('220a1022-1fcd-4a74-a7bd-6b859156cda2')
+ def test_snapshots_list_details_with_params(self):
+ """list snapshot details with params."""
+ # Verify list snapshot details by display_name filter
+ params = {'name': self.snapshot['name']}
+ self._list_by_param_values_and_assert(with_detail=True, **params)
+ # Verify list snapshot details by status filter
+ params = {'status': 'available'}
+ self._list_by_param_values_and_assert(with_detail=True, **params)
+ # Verify list snapshot details by status and display name filter
+ params = {'status': 'available',
+ 'name': self.snapshot['name']}
+ self._list_by_param_values_and_assert(with_detail=True, **params)
+
+ @decorators.idempotent_id('db4d8e0a-7a2e-41cc-a712-961f6844e896')
+ def test_snapshot_list_param_limit(self):
+ # List returns limited elements
+ self._list_snapshots_by_param_limit(limit=1, expected_elements=1)
+
+ @decorators.idempotent_id('a1427f61-420e-48a5-b6e3-0b394fa95400')
+ def test_snapshot_list_param_limit_equals_infinite(self):
+ # List returns all elements when request limit exceeded
+ # snapshots number
+ snap_list = self.snapshots_client.list_snapshots()['snapshots']
+ self._list_snapshots_by_param_limit(limit=100000,
+ expected_elements=len(snap_list))
+
+ @decorators.idempotent_id('e3b44b7f-ae87-45b5-8a8c-66110eb24d0a')
+ def test_snapshot_list_param_limit_equals_zero(self):
+ # List returns zero elements
+ self._list_snapshots_by_param_limit(limit=0, expected_elements=0)
+
+ def _list_snapshots_param_sort(self, sort_key, sort_dir):
+ """list snapshots by sort param"""
+ snap_list = self.snapshots_client.list_snapshots(
+ sort_key=sort_key, sort_dir=sort_dir)['snapshots']
+ self.assertNotEmpty(snap_list)
+ if sort_key is 'display_name':
+ sort_key = 'name'
+ # Note: On Cinder API, 'display_name' works as a sort key
+ # on a request, a volume name appears as 'name' on the response.
+ # So Tempest needs to change the key name here for this inconsistent
+ # API behavior.
+ sorted_list = [snapshot[sort_key] for snapshot in snap_list]
+ msg = 'The list of snapshots was not sorted correctly.'
+ self.assertEqual(sorted(sorted_list, reverse=(sort_dir == 'desc')),
+ sorted_list, msg)
+
+ @decorators.idempotent_id('c5513ada-64c1-4d28-83b9-af3307ec1388')
+ def test_snapshot_list_param_sort_id_asc(self):
+ self._list_snapshots_param_sort(sort_key='id', sort_dir='asc')
+
+ @decorators.idempotent_id('8a7fe058-0b41-402a-8afd-2dbc5a4a718b')
+ def test_snapshot_list_param_sort_id_desc(self):
+ self._list_snapshots_param_sort(sort_key='id', sort_dir='desc')
+
+ @decorators.idempotent_id('4052c3a0-2415-440a-a8cc-305a875331b0')
+ def test_snapshot_list_param_sort_created_at_asc(self):
+ self._list_snapshots_param_sort(sort_key='created_at', sort_dir='asc')
+
+ @decorators.idempotent_id('dcbbe24a-f3c0-4ec8-9274-55d48db8d1cf')
+ def test_snapshot_list_param_sort_created_at_desc(self):
+ self._list_snapshots_param_sort(sort_key='created_at', sort_dir='desc')
+
+ @decorators.idempotent_id('d58b5fed-0c37-42d3-8c5d-39014ac13c00')
+ def test_snapshot_list_param_sort_name_asc(self):
+ self._list_snapshots_param_sort(sort_key='display_name',
+ sort_dir='asc')
+
+ @decorators.idempotent_id('96ba6f4d-1f18-47e1-b4bc-76edc6c21250')
+ def test_snapshot_list_param_sort_name_desc(self):
+ self._list_snapshots_param_sort(sort_key='display_name',
+ sort_dir='desc')
+
+ @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
+ 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(snapshot_id_list[:1], fetched_list_id)
diff --git a/tempest/api/volume/test_volumes_snapshots_negative.py b/tempest/api/volume/test_volumes_snapshots_negative.py
index b604360..ea5f036 100644
--- a/tempest/api/volume/test_volumes_snapshots_negative.py
+++ b/tempest/api/volume/test_volumes_snapshots_negative.py
@@ -10,44 +10,74 @@
# License for the specific language governing permissions and limitations
# under the License.
-import uuid
-
-from tempest_lib import exceptions as lib_exc
-
from tempest.api.volume import base
-from tempest.common.utils import data_utils
from tempest import config
-from tempest import test
+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 VolumesV2SnapshotNegativeTestJSON(base.BaseVolumeTest):
+class VolumesSnapshotNegativeTestJSON(base.BaseVolumeTest):
@classmethod
def skip_checks(cls):
- super(VolumesV2SnapshotNegativeTestJSON, cls).skip_checks()
+ super(VolumesSnapshotNegativeTestJSON, cls).skip_checks()
if not CONF.volume_feature_enabled.snapshot:
raise cls.skipException("Cinder volume snapshots are disabled")
- @test.attr(type=['negative'])
- @test.idempotent_id('e3e466af-70ab-4f4b-a967-ab04e3532ea7')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('e3e466af-70ab-4f4b-a967-ab04e3532ea7')
def test_create_snapshot_with_nonexistent_volume_id(self):
# Create a snapshot with nonexistent volume id
- s_name = data_utils.rand_name('snap')
+ s_name = data_utils.rand_name(self.__class__.__name__ + '-snap')
self.assertRaises(lib_exc.NotFound,
self.snapshots_client.create_snapshot,
- str(uuid.uuid4()), display_name=s_name)
+ volume_id=data_utils.rand_uuid(),
+ display_name=s_name)
- @test.attr(type=['negative'])
- @test.idempotent_id('bb9da53e-d335-4309-9c15-7e76fd5e4d6d')
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('bb9da53e-d335-4309-9c15-7e76fd5e4d6d')
def test_create_snapshot_without_passing_volume_id(self):
# Create a snapshot without passing volume id
- s_name = data_utils.rand_name('snap')
+ s_name = data_utils.rand_name(self.__class__.__name__ + '-snap')
self.assertRaises(lib_exc.NotFound,
self.snapshots_client.create_snapshot,
- None, display_name=s_name)
+ volume_id=None, display_name=s_name)
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('677863d1-34f9-456d-b6ac-9924f667a7f4')
+ def test_volume_from_snapshot_decreasing_size(self):
+ # Creates a volume a snapshot passing a size different from the source
+ src_size = CONF.volume.volume_size + 1
-class VolumesV1SnapshotNegativeTestJSON(VolumesV2SnapshotNegativeTestJSON):
- _api_version = 1
+ src_vol = self.create_volume(size=src_size)
+ src_snap = self.create_snapshot(src_vol['id'])
+
+ # Destination volume smaller than source
+ self.assertRaises(lib_exc.BadRequest,
+ self.volumes_client.create_volume,
+ size=src_size - 1,
+ snapshot_id=src_snap['id'])
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('8fd92339-e22f-4591-86b4-1e2215372a40')
+ def test_list_snapshot_invalid_param_limit(self):
+ self.assertRaises(lib_exc.BadRequest,
+ self.snapshots_client.list_snapshots,
+ limit='invalid')
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('27b5f37f-bf69-4e8c-986e-c44f3d6819b8')
+ def test_list_snapshots_invalid_param_sort(self):
+ self.assertRaises(lib_exc.BadRequest,
+ self.snapshots_client.list_snapshots,
+ sort_key='invalid')
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('b68deeda-ca79-4a32-81af-5c51179e553a')
+ def test_list_snapshots_invalid_param_marker(self):
+ self.assertRaises(lib_exc.NotFound,
+ self.snapshots_client.list_snapshots,
+ marker=data_utils.rand_uuid())
diff --git a/tempest/api/volume/v2/__init__.py b/tempest/api/volume/v2/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/api/volume/v2/__init__.py
+++ /dev/null
diff --git a/tempest/api/volume/v2/test_volumes_list.py b/tempest/api/volume/v2/test_volumes_list.py
deleted file mode 100644
index 6568627..0000000
--- a/tempest/api/volume/v2/test_volumes_list.py
+++ /dev/null
@@ -1,187 +0,0 @@
-# Copyright 2012 OpenStack Foundation
-# Copyright 2013 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 six.moves.urllib import parse
-
-from tempest.api.volume import base
-from tempest import test
-
-
-class VolumesV2ListTestJSON(base.BaseVolumeTest):
- """volumes v2 specific tests.
-
- This test creates a number of 1G volumes. To run successfully,
- ensure that the backing file for the volume group that Nova uses
- has space for at least 3 1G volumes!
- If you are running a Devstack environment, ensure that the
- VOLUME_BACKING_FILE_SIZE is at least 4G in your localrc
- """
-
- @classmethod
- def setup_clients(cls):
- super(VolumesV2ListTestJSON, cls).setup_clients()
- cls.client = cls.volumes_client
-
- @classmethod
- def resource_setup(cls):
- super(VolumesV2ListTestJSON, cls).resource_setup()
-
- # Create 3 test volumes
- cls.volume_list = []
- cls.volume_id_list = []
- cls.metadata = {'Type': 'work'}
- for i in range(3):
- volume = cls.create_volume(metadata=cls.metadata)
- volume = cls.client.show_volume(volume['id'])['volume']
- cls.volume_list.append(volume)
- cls.volume_id_list.append(volume['id'])
-
- @classmethod
- def resource_cleanup(cls):
- # Delete the created volumes
- for volid in cls.volume_id_list:
- cls.client.delete_volume(volid)
- cls.client.wait_for_resource_deletion(volid)
- super(VolumesV2ListTestJSON, cls).resource_cleanup()
-
- @test.idempotent_id('2a7064eb-b9c3-429b-b888-33928fc5edd3')
- def test_volume_list_details_with_multiple_params(self):
- # List volumes detail using combined condition
- def _list_details_with_multiple_params(limit=2,
- status='available',
- sort_dir='asc',
- sort_key='id'):
- params = {'limit': limit,
- 'status': status,
- 'sort_dir': sort_dir,
- 'sort_key': sort_key
- }
- fetched_volume = self.client.list_volumes(
- detail=True, params=params)['volumes']
- self.assertEqual(limit, len(fetched_volume),
- "The count of volumes is %s, expected:%s " %
- (len(fetched_volume), limit))
- self.assertEqual(status, fetched_volume[0]['status'])
- self.assertEqual(status, fetched_volume[1]['status'])
- val0 = fetched_volume[0][sort_key]
- val1 = fetched_volume[1][sort_key]
- if sort_dir == 'asc':
- self.assertTrue(val0 < val1,
- "%s < %s" % (val0, val1))
- elif sort_dir == 'desc':
- self.assertTrue(val0 > val1,
- "%s > %s" % (val0, val1))
-
- _list_details_with_multiple_params()
- _list_details_with_multiple_params(sort_dir='desc')
-
- def _test_pagination(self, resource, ids=None, limit=1, **kwargs):
- """Check list pagination functionality for a resource.
-
- This method requests the list of resources and follows pagination
- links.
-
- If an iterable is supplied in ids it will check that all ids are
- retrieved and that only those are listed, that we will get a next
- link for an empty page if the number of items is divisible by used
- limit (this is expected behavior).
-
- We can specify number of items per request using limit argument.
- """
-
- # Get list method for the type of resource from the client
- client = getattr(self, resource + '_client')
- method = getattr(client, 'list_' + resource)
-
- # Include limit in params for list request
- params = kwargs.pop('params', {})
- params['limit'] = limit
-
- # Store remaining items we are expecting from list
- if ids is not None:
- remaining = list(ids)
- else:
- remaining = None
-
- # Mark that the current iteration is not from a 'next' link
- next = None
-
- while True:
- # Get a list page
- response = method(params=params, **kwargs)
-
- # If we have to check ids
- if remaining is not None:
- # Confirm we receive expected number of elements
- num_expected = min(len(remaining), limit)
- self.assertEqual(num_expected, len(response[resource]),
- 'Requested %(#expect)d but got %(#received)d '
- % {'#expect': num_expected,
- '#received': len(response[resource])})
-
- # For each received element
- for element in response[resource]:
- element_id = element['id']
- # Check it's one of expected ids
- self.assertIn(element_id,
- ids,
- 'Id %(id)s is not in expected ids %(ids)s' %
- {'id': element_id, 'ids': ids})
- # If not in remaining, we have received it twice
- self.assertIn(element_id,
- remaining,
- 'Id %s was received twice' % element_id)
- # We no longer expect it
- remaining.remove(element_id)
-
- # If the current iteration is from a 'next' link, check that the
- # absolute url is the same as the one used for this request
- if next:
- self.assertEqual(next, response.response['content-location'])
-
- # Get next from response
- next = None
- for link in response.get(resource + '_links', ()):
- if link['rel'] == 'next':
- next = link['href']
- break
-
- # Check if we have next and we shouldn't or the other way around
- if remaining is not None:
- if remaining or (num_expected and len(ids) % limit == 0):
- self.assertIsNotNone(next, 'Missing link to next page')
- else:
- self.assertIsNone(next, 'Unexpected link to next page')
-
- # If we can follow to the next page, get params from url to make
- # request in the form of a relative URL
- if next:
- params = parse.urlparse(next).query
-
- # If cannot follow make sure it's because we have finished
- else:
- self.assertListEqual([], remaining or [],
- 'No more pages reported, but still '
- 'missing ids %s' % remaining)
- break
-
- @test.idempotent_id('e9138a2c-f67b-4796-8efa-635c196d01de')
- def test_volume_list_details_pagination(self):
- self._test_pagination('volumes', ids=self.volume_id_list, detail=True)
-
- @test.idempotent_id('af55e775-8e4b-4feb-8719-215c43b0238c')
- def test_volume_list_pagination(self):
- self._test_pagination('volumes', ids=self.volume_id_list, detail=False)
diff --git a/tempest/api_schema/request/__init__.py b/tempest/api_schema/request/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/api_schema/request/__init__.py
+++ /dev/null
diff --git a/tempest/api_schema/request/compute/__init__.py b/tempest/api_schema/request/compute/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/api_schema/request/compute/__init__.py
+++ /dev/null
diff --git a/tempest/api_schema/request/compute/flavors.py b/tempest/api_schema/request/compute/flavors.py
deleted file mode 100644
index adaaf27..0000000
--- a/tempest/api_schema/request/compute/flavors.py
+++ /dev/null
@@ -1,58 +0,0 @@
-# (c) 2014 Deutsche Telekom AG
-# 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.
-
-common_flavor_details = {
- "name": "get-flavor-details",
- "http-method": "GET",
- "url": "flavors/%s",
- "resources": [
- {"name": "flavor", "expected_result": 404}
- ]
-}
-
-common_flavor_list = {
- "name": "list-flavors-with-detail",
- "http-method": "GET",
- "url": "flavors/detail",
- "json-schema": {
- "type": "object",
- "properties": {
- }
- }
-}
-
-common_admin_flavor_create = {
- "name": "flavor-create",
- "http-method": "POST",
- "admin_client": True,
- "url": "flavors",
- "default_result_code": 400,
- "json-schema": {
- "type": "object",
- "properties": {
- "flavor": {
- "type": "object",
- "properties": {
- "name": {"type": "string",
- "exclude_tests": ["gen_str_min_length"]},
- "ram": {"type": "integer", "minimum": 1},
- "vcpus": {"type": "integer", "minimum": 1},
- "disk": {"type": "integer"},
- "id": {"type": "integer",
- "exclude_tests": ["gen_none", "gen_string"]
- },
- }
- }
- }
- }
-}
diff --git a/tempest/api_schema/request/compute/v2/__init__.py b/tempest/api_schema/request/compute/v2/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/api_schema/request/compute/v2/__init__.py
+++ /dev/null
diff --git a/tempest/api_schema/request/compute/v2/flavors.py b/tempest/api_schema/request/compute/v2/flavors.py
deleted file mode 100644
index bc459ad..0000000
--- a/tempest/api_schema/request/compute/v2/flavors.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# (c) 2014 Deutsche Telekom AG
-# 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.api_schema.request.compute import flavors
-
-flavors_details = copy.deepcopy(flavors.common_flavor_details)
-
-flavor_list = copy.deepcopy(flavors.common_flavor_list)
-
-flavor_create = copy.deepcopy(flavors.common_admin_flavor_create)
-
-flavor_list["json-schema"]["properties"] = {
- "minRam": {
- "type": "integer",
- "results": {
- "gen_none": 400,
- "gen_string": 400
- }
- },
- "minDisk": {
- "type": "integer",
- "results": {
- "gen_none": 400,
- "gen_string": 400
- }
- }
-}
diff --git a/tempest/api_schema/response/compute/v2_1/keypairs.py b/tempest/api_schema/response/compute/v2_1/keypairs.py
deleted file mode 100644
index 9c04c79..0000000
--- a/tempest/api_schema/response/compute/v2_1/keypairs.py
+++ /dev/null
@@ -1,107 +0,0 @@
-# Copyright 2014 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.
-
-get_keypair = {
- 'status_code': [200],
- 'response_body': {
- 'type': 'object',
- 'properties': {
- 'keypair': {
- 'type': 'object',
- 'properties': {
- 'public_key': {'type': 'string'},
- 'name': {'type': 'string'},
- 'fingerprint': {'type': 'string'},
- 'user_id': {'type': 'string'},
- 'deleted': {'type': 'boolean'},
- 'created_at': {'type': 'string'},
- 'updated_at': {'type': ['string', 'null']},
- 'deleted_at': {'type': ['string', 'null']},
- 'id': {'type': 'integer'}
-
- },
- 'additionalProperties': False,
- # When we run the get keypair API, response body includes
- # all the above mentioned attributes.
- # But in Nova API sample file, response body includes only
- # 'public_key', 'name' & 'fingerprint'. So only 'public_key',
- # 'name' & 'fingerprint' are defined as 'required'.
- 'required': ['public_key', 'name', 'fingerprint']
- }
- },
- 'additionalProperties': False,
- 'required': ['keypair']
- }
-}
-
-create_keypair = {
- 'status_code': [200],
- 'response_body': {
- 'type': 'object',
- 'properties': {
- 'keypair': {
- 'type': 'object',
- 'properties': {
- 'fingerprint': {'type': 'string'},
- 'name': {'type': 'string'},
- 'public_key': {'type': 'string'},
- 'user_id': {'type': 'string'},
- 'private_key': {'type': 'string'}
- },
- 'additionalProperties': False,
- # When create keypair API is being called with 'Public key'
- # (Importing keypair) then, response body does not contain
- # 'private_key' So it is not defined as 'required'
- 'required': ['fingerprint', 'name', 'public_key', 'user_id']
- }
- },
- 'additionalProperties': False,
- 'required': ['keypair']
- }
-}
-
-delete_keypair = {
- 'status_code': [202],
-}
-
-list_keypairs = {
- 'status_code': [200],
- 'response_body': {
- 'type': 'object',
- 'properties': {
- 'keypairs': {
- 'type': 'array',
- 'items': {
- 'type': 'object',
- 'properties': {
- 'keypair': {
- 'type': 'object',
- 'properties': {
- 'public_key': {'type': 'string'},
- 'name': {'type': 'string'},
- 'fingerprint': {'type': 'string'}
- },
- 'additionalProperties': False,
- 'required': ['public_key', 'name', 'fingerprint']
- }
- },
- 'additionalProperties': False,
- 'required': ['keypair']
- }
- }
- },
- 'additionalProperties': False,
- 'required': ['keypairs']
- }
-}
diff --git a/tempest/api_schema/response/messaging/__init__.py b/tempest/api_schema/response/messaging/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/api_schema/response/messaging/__init__.py
+++ /dev/null
diff --git a/tempest/api_schema/response/messaging/v1/__init__.py b/tempest/api_schema/response/messaging/v1/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/api_schema/response/messaging/v1/__init__.py
+++ /dev/null
diff --git a/tempest/api_schema/response/messaging/v1/queues.py b/tempest/api_schema/response/messaging/v1/queues.py
deleted file mode 100644
index 09e0147..0000000
--- a/tempest/api_schema/response/messaging/v1/queues.py
+++ /dev/null
@@ -1,239 +0,0 @@
-
-# Copyright (c) 2014 Rackspace, Inc.
-#
-# 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.
-
-
-list_link = {
- 'type': 'object',
- 'properties': {
- 'rel': {'type': 'string'},
- 'href': {
- 'type': 'string',
- 'format': 'uri'
- }
- },
- 'required': ['href', 'rel']
-}
-
-list_queue = {
- 'type': 'object',
- 'properties': {
- 'name': {'type': 'string'},
- 'href': {
- 'type': 'string',
- 'format': 'uri'
- },
- 'metadata': {'type': 'object'}
- },
- 'required': ['name', 'href']
-}
-
-list_queues = {
- 'status_code': [200, 204],
- 'response_body': {
- 'type': 'object',
- 'properties': {
- 'links': {
- 'type': 'array',
- 'items': list_link,
- 'maxItems': 1
- },
- 'queues': {
- 'type': 'array',
- 'items': list_queue
- }
- },
- 'required': ['links', 'queues']
- }
-}
-
-age = {
- 'type': 'number',
- 'minimum': 0
-}
-
-message_link = {
- 'type': 'object',
- 'properties': {
- 'href': {
- 'type': 'string',
- 'format': 'uri'
- },
- 'age': age,
- 'created': {
- 'type': 'string',
- 'format': 'date-time'
- }
- },
- 'required': ['href', 'age', 'created']
-}
-
-messages = {
- 'type': 'object',
- 'properties': {
- 'free': {'type': 'number'},
- 'claimed': {'type': 'number'},
- 'total': {'type': 'number'},
- 'oldest': message_link,
- 'newest': message_link
- },
- 'required': ['free', 'claimed', 'total']
-}
-
-queue_stats = {
- 'status_code': [200],
- 'response_body': {
- 'type': 'object',
- 'properties': {
- 'messages': messages
- },
- 'required': ['messages']
- }
-}
-
-resource_schema = {
- 'type': 'array',
- 'items': {
- 'type': 'string'
- },
- 'minItems': 1
-}
-
-post_messages = {
- 'status_code': [201],
- 'response_body': {
- 'type': 'object',
- 'properties': {
- 'resources': resource_schema,
- 'partial': {'type': 'boolean'}
- }
- },
- 'required': ['resources', 'partial']
-}
-
-message_ttl = {
- 'type': 'number',
- 'minimum': 1
-}
-
-list_messages_links = {
- 'type': 'array',
- 'maxItems': 1,
- 'minItems': 1,
- 'items': {
- 'type': 'object',
- 'properties': {
- 'rel': {'type': 'string'},
- 'href': {'type': 'string'}
- },
- 'required': ['rel', 'href']
- }
-}
-
-list_messages_response = {
- 'type': 'array',
- 'minItems': 1,
- 'items': {
- 'type': 'object',
- 'properties': {
- 'href': {'type': 'string'},
- 'ttl': message_ttl,
- 'age': age,
- 'body': {'type': 'object'}
- },
- 'required': ['href', 'ttl', 'age', 'body']
- }
-}
-
-list_messages = {
- 'status_code': [200, 204],
- 'response_body': {
- 'type': 'object',
- 'properties': {
- 'links': list_messages_links,
- 'messages': list_messages_response
- }
- },
- 'required': ['links', 'messages']
-}
-
-single_message = {
- 'type': 'object',
- 'properties': {
- 'href': {'type': 'string'},
- 'ttl': message_ttl,
- 'age': age,
- 'body': {'type': 'object'}
- },
- 'required': ['href', 'ttl', 'age', 'body']
-}
-
-get_single_message = {
- 'status_code': [200],
- 'response_body': single_message
-}
-
-get_multiple_messages = {
- 'status_code': [200],
- 'response_body': {
- 'type': 'array',
- 'items': single_message,
- 'minItems': 1
- }
-}
-
-messages_claimed = {
- 'type': 'object',
- 'properties': {
- 'href': {
- 'type': 'string',
- 'format': 'uri'
- },
- 'ttl': message_ttl,
- 'age': {'type': 'number'},
- 'body': {'type': 'object'}
- },
- 'required': ['href', 'ttl', 'age', 'body']
-}
-
-claim_messages = {
- 'status_code': [201, 204],
- 'response_body': {
- 'type': 'array',
- 'items': messages_claimed,
- 'minItems': 1
- }
-}
-
-claim_ttl = {
- 'type': 'number',
- 'minimum': 1
-}
-
-query_claim = {
- 'status_code': [200],
- 'response_body': {
- 'type': 'object',
- 'properties': {
- 'age': {'type': 'number'},
- 'ttl': claim_ttl,
- 'messages': {
- 'type': 'array',
- 'minItems': 1
- }
- },
- 'required': ['ttl', 'age', 'messages']
- }
-}
diff --git a/tempest/clients.py b/tempest/clients.py
index 772ddfc..2a07be9 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -13,591 +13,308 @@
# License for the specific language governing permissions and limitations
# under the License.
-import copy
-
-from oslo_log import log as logging
-from tempest_lib.services.compute.agents_client import AgentsClient
-from tempest_lib.services.compute.aggregates_client import AggregatesClient
-from tempest_lib.services.compute.availability_zone_client import \
- AvailabilityZoneClient
-from tempest_lib.services.compute.baremetal_nodes_client import \
- BaremetalNodesClient
-from tempest_lib.services.compute.certificates_client import \
- CertificatesClient
-from tempest_lib.services.compute.extensions_client import \
- ExtensionsClient
-from tempest_lib.services.compute.fixed_ips_client import FixedIPsClient
-from tempest_lib.services.compute.flavors_client import FlavorsClient
-from tempest_lib.services.compute.floating_ip_pools_client import \
- FloatingIPPoolsClient
-from tempest_lib.services.compute.floating_ips_bulk_client import \
- FloatingIPsBulkClient
-from tempest_lib.services.compute.floating_ips_client import \
- FloatingIPsClient as ComputeFloatingIPsClient
-from tempest_lib.services.compute.hosts_client import HostsClient
-from tempest_lib.services.compute.hypervisor_client import \
- HypervisorClient
-from tempest_lib.services.compute.images_client import ImagesClient \
- as ComputeImagesClient
-from tempest_lib.services.compute.instance_usage_audit_log_client import \
- InstanceUsagesAuditLogClient
-from tempest_lib.services.compute.interfaces_client import InterfacesClient
-from tempest_lib.services.compute.limits_client import LimitsClient
-from tempest_lib.services.compute.migrations_client import MigrationsClient
-from tempest_lib.services.compute.networks_client import NetworksClient \
- as ComputeNetworksClient
-from tempest_lib.services.compute.quota_classes_client import \
- QuotaClassesClient
-from tempest_lib.services.compute.quotas_client import QuotasClient
-from tempest_lib.services.compute.security_group_default_rules_client import \
- SecurityGroupDefaultRulesClient
-from tempest_lib.services.compute.security_group_rules_client import \
- SecurityGroupRulesClient
-from tempest_lib.services.compute.security_groups_client import \
- SecurityGroupsClient as ComputeSecurityGroupsClient
-from tempest_lib.services.compute.server_groups_client import \
- ServerGroupsClient
-from tempest_lib.services.compute.servers_client import ServersClient
-from tempest_lib.services.compute.services_client import ServicesClient
-from tempest_lib.services.compute.snapshots_client import \
- SnapshotsClient as ComputeSnapshotsClient
-from tempest_lib.services.compute.tenant_networks_client import \
- TenantNetworksClient
-from tempest_lib.services.compute.tenant_usages_client import \
- TenantUsagesClient
-from tempest_lib.services.compute.versions_client import VersionsClient
-from tempest_lib.services.compute.volumes_client import \
- VolumesClient as ComputeVolumesClient
-from tempest_lib.services.identity.v2.token_client import TokenClient
-from tempest_lib.services.identity.v3.token_client import V3TokenClient
-
-from tempest.common import negative_rest_client
from tempest import config
-from tempest import exceptions
-from tempest import manager
-from tempest.services.baremetal.v1.json.baremetal_client import \
- BaremetalClient
-from tempest.services.compute.json.keypairs_client import KeyPairsClient
-from tempest.services.data_processing.v1_1.data_processing_client import \
- DataProcessingClient
-from tempest.services.database.json.flavors_client import \
- DatabaseFlavorsClient
-from tempest.services.database.json.limits_client import \
- DatabaseLimitsClient
-from tempest.services.database.json.versions_client import \
- DatabaseVersionsClient
-from tempest.services.identity.v2.json.identity_client import \
- IdentityClient
-from tempest.services.identity.v2.json.roles_client import \
- RolesClient
-from tempest.services.identity.v2.json.tenants_client import \
- TenantsClient
-from tempest.services.identity.v3.json.credentials_client import \
- CredentialsClient as CredentialsV3Client
-from tempest.services.identity.v3.json.endpoints_client import \
- EndPointClient as EndPointV3Client
-from tempest.services.identity.v3.json.groups_client import \
- GroupsClient as GroupsV3Client
-from tempest.services.identity.v3.json.identity_client import IdentityV3Client
-from tempest.services.identity.v3.json.policies_client import \
- PoliciesClient as PoliciesV3Client
-from tempest.services.identity.v3.json.regions_client import \
- RegionsClient as RegionsV3Client
-from tempest.services.identity.v3.json.services_client import \
- ServicesClient as IdentityServicesV3Client
-from tempest.services.image.v1.json.images_client import ImagesClient
-from tempest.services.image.v2.json.images_client import ImagesClientV2
-from tempest.services.messaging.json.messaging_client import \
- MessagingClient
-from tempest.services.network.json.agents_client import AgentsClient \
- as NetworkAgentsClient
-from tempest.services.network.json.floating_ips_client import FloatingIPsClient
-from tempest.services.network.json.metering_label_rules_client import \
- MeteringLabelRulesClient
-from tempest.services.network.json.metering_labels_client import \
- MeteringLabelsClient
-from tempest.services.network.json.network_client import NetworkClient
-from tempest.services.network.json.networks_client import NetworksClient
-from tempest.services.network.json.ports_client import PortsClient
-from tempest.services.network.json.quotas_client import QuotasClient \
- as NetworkQuotasClient
-from tempest.services.network.json.security_groups_client import \
- SecurityGroupsClient
-from tempest.services.network.json.subnetpools_client import SubnetpoolsClient
-from tempest.services.network.json.subnets_client import SubnetsClient
-from tempest.services.object_storage.account_client import AccountClient
-from tempest.services.object_storage.container_client import ContainerClient
-from tempest.services.object_storage.object_client import ObjectClient
-from tempest.services.orchestration.json.orchestration_client import \
- OrchestrationClient
-from tempest.services.telemetry.json.alarming_client import AlarmingClient
-from tempest.services.telemetry.json.telemetry_client import \
- TelemetryClient
-from tempest.services.volume.v1.json.admin.hosts_client import \
- HostsClient as VolumeHostsClient
-from tempest.services.volume.v1.json.admin.quotas_client import \
- QuotasClient as VolumeQuotasClient
-from tempest.services.volume.v1.json.admin.services_client import \
- ServicesClient as VolumeServicesClient
-from tempest.services.volume.v1.json.admin.types_client import \
- TypesClient as VolumeTypesClient
-from tempest.services.volume.v1.json.availability_zone_client import \
- AvailabilityZoneClient as VolumeAvailabilityZoneClient
-from tempest.services.volume.v1.json.backups_client import BackupsClient
-from tempest.services.volume.v1.json.extensions_client import \
- ExtensionsClient as VolumeExtensionsClient
-from tempest.services.volume.v1.json.qos_client import QosSpecsClient
-from tempest.services.volume.v1.json.snapshots_client import SnapshotsClient
-from tempest.services.volume.v1.json.volumes_client import VolumesClient
-from tempest.services.volume.v2.json.admin.hosts_client import \
- HostsClient as VolumeHostsV2Client
-from tempest.services.volume.v2.json.admin.quotas_client import \
- QuotasClient as VolumeQuotasV2Client
-from tempest.services.volume.v2.json.admin.services_client import \
- ServicesClient as VolumeServicesV2Client
-from tempest.services.volume.v2.json.admin.types_client import \
- TypesClient as VolumeTypesV2Client
-from tempest.services.volume.v2.json.availability_zone_client import \
- AvailabilityZoneClient as VolumeAvailabilityZoneV2Client
-from tempest.services.volume.v2.json.backups_client import \
- BackupsClient as BackupsV2Client
-from tempest.services.volume.v2.json.extensions_client import \
- ExtensionsClient as VolumeExtensionsV2Client
-from tempest.services.volume.v2.json.qos_client import \
- QosSpecsClient as QosSpecsV2Client
-from tempest.services.volume.v2.json.snapshots_client import \
- SnapshotsClient as SnapshotsV2Client
-from tempest.services.volume.v2.json.volumes_client import \
- VolumesClient as VolumesV2Client
+from tempest.lib import auth
+from tempest.lib import exceptions as lib_exc
+from tempest.lib.services import clients
CONF = config.CONF
-LOG = logging.getLogger(__name__)
-class Manager(manager.Manager):
+class Manager(clients.ServiceClients):
"""Top level manager for OpenStack tempest clients"""
- default_params = {
- 'disable_ssl_certificate_validation':
- CONF.identity.disable_ssl_certificate_validation,
- 'ca_certs': CONF.identity.ca_certificates_file,
- 'trace_requests': CONF.debug.trace_requests
- }
-
- # NOTE: Tempest uses timeout values of compute API if project specific
- # timeout values don't exist.
- default_params_with_timeout_values = {
- 'build_interval': CONF.compute.build_interval,
- 'build_timeout': CONF.compute.build_timeout
- }
- default_params_with_timeout_values.update(default_params)
-
- def __init__(self, credentials, service=None, api_microversions=None):
+ def __init__(self, credentials, scope='project'):
"""Initialization of Manager class.
Setup all services clients and make them available for tests cases.
:param credentials: type Credentials or TestResources
- :param service: Service name
- :param api_microversions: This is dict of services catalog type
- and their microversion which will be set on respective
- services clients.
- {<service catalog type>: request_microversion}
- Example :
- {'compute': request_microversion}
- - request_microversion will be set on all compute
- service clients.
- OR
- {'compute': request_microversion,
- 'volume': request_microversion}
- - request_microversion of compute will be set on all
- compute service clients.
- - request_microversion of volume will be set on all
- volume service clients.
+ :param scope: default scope for tokens produced by the auth provider
"""
- super(Manager, self).__init__(credentials=credentials)
- self.api_microversions = api_microversions or {}
+ _, identity_uri = get_auth_provider_class(credentials)
+ super(Manager, self).__init__(
+ credentials=credentials, identity_uri=identity_uri, scope=scope,
+ region=CONF.identity.region)
+ # TODO(andreaf) When clients are initialised without the right
+ # parameters available, the calls below will trigger a KeyError.
+ # We should catch that and raise a better error.
self._set_compute_clients()
- self._set_database_clients()
self._set_identity_clients()
self._set_volume_clients()
self._set_object_storage_clients()
+ self._set_image_clients()
+ self._set_network_clients()
+ # TODO(andreaf) This is maintained for backward compatibility
+ # with plugins, but it should removed eventually, since it was
+ # never a stable interface and it's not useful anyways
+ self.default_params = config.service_client_config()
- self.baremetal_client = BaremetalClient(
- self.auth_provider,
- CONF.baremetal.catalog_type,
- CONF.identity.region,
- endpoint_type=CONF.baremetal.endpoint_type,
- **self.default_params_with_timeout_values)
- self.network_agents_client = NetworkAgentsClient(
- self.auth_provider,
- CONF.network.catalog_type,
- CONF.network.region or CONF.identity.region,
- endpoint_type=CONF.network.endpoint_type,
- build_interval=CONF.network.build_interval,
- build_timeout=CONF.network.build_timeout,
- **self.default_params)
- self.network_client = NetworkClient(
- self.auth_provider,
- CONF.network.catalog_type,
- CONF.network.region or CONF.identity.region,
- endpoint_type=CONF.network.endpoint_type,
- build_interval=CONF.network.build_interval,
- build_timeout=CONF.network.build_timeout,
- **self.default_params)
- self.networks_client = NetworksClient(
- self.auth_provider,
- CONF.network.catalog_type,
- CONF.network.region or CONF.identity.region,
- endpoint_type=CONF.network.endpoint_type,
- build_interval=CONF.network.build_interval,
- build_timeout=CONF.network.build_timeout,
- **self.default_params)
- self.subnetpools_client = SubnetpoolsClient(
- self.auth_provider,
- CONF.network.catalog_type,
- CONF.network.region or CONF.identity.region,
- endpoint_type=CONF.network.endpoint_type,
- build_interval=CONF.network.build_interval,
- build_timeout=CONF.network.build_timeout,
- **self.default_params)
- self.subnets_client = SubnetsClient(
- self.auth_provider,
- CONF.network.catalog_type,
- CONF.network.region or CONF.identity.region,
- endpoint_type=CONF.network.endpoint_type,
- build_interval=CONF.network.build_interval,
- build_timeout=CONF.network.build_timeout,
- **self.default_params)
- self.ports_client = PortsClient(
- self.auth_provider,
- CONF.network.catalog_type,
- CONF.network.region or CONF.identity.region,
- endpoint_type=CONF.network.endpoint_type,
- build_interval=CONF.network.build_interval,
- build_timeout=CONF.network.build_timeout,
- **self.default_params)
- self.network_quotas_client = NetworkQuotasClient(
- self.auth_provider,
- CONF.network.catalog_type,
- CONF.network.region or CONF.identity.region,
- endpoint_type=CONF.network.endpoint_type,
- build_interval=CONF.network.build_interval,
- build_timeout=CONF.network.build_timeout,
- **self.default_params)
- self.floating_ips_client = FloatingIPsClient(
- self.auth_provider,
- CONF.network.catalog_type,
- CONF.network.region or CONF.identity.region,
- endpoint_type=CONF.network.endpoint_type,
- build_interval=CONF.network.build_interval,
- build_timeout=CONF.network.build_timeout,
- **self.default_params)
- self.metering_labels_client = MeteringLabelsClient(
- self.auth_provider,
- CONF.network.catalog_type,
- CONF.network.region or CONF.identity.region,
- endpoint_type=CONF.network.endpoint_type,
- build_interval=CONF.network.build_interval,
- build_timeout=CONF.network.build_timeout,
- **self.default_params)
- self.metering_label_rules_client = MeteringLabelRulesClient(
- self.auth_provider,
- CONF.network.catalog_type,
- CONF.network.region or CONF.identity.region,
- endpoint_type=CONF.network.endpoint_type,
- build_interval=CONF.network.build_interval,
- build_timeout=CONF.network.build_timeout,
- **self.default_params)
- self.security_groups_client = SecurityGroupsClient(
- self.auth_provider,
- CONF.network.catalog_type,
- CONF.network.region or CONF.identity.region,
- endpoint_type=CONF.network.endpoint_type,
- build_interval=CONF.network.build_interval,
- build_timeout=CONF.network.build_timeout,
- **self.default_params)
- self.messaging_client = MessagingClient(
- self.auth_provider,
- CONF.messaging.catalog_type,
- CONF.identity.region,
- **self.default_params_with_timeout_values)
- if CONF.service_available.ceilometer:
- self.telemetry_client = TelemetryClient(
- self.auth_provider,
- CONF.telemetry.catalog_type,
- CONF.identity.region,
- endpoint_type=CONF.telemetry.endpoint_type,
- **self.default_params_with_timeout_values)
- if CONF.service_available.aodh:
- self.alarming_client = AlarmingClient(
- self.auth_provider,
- CONF.alarming.catalog_type,
- CONF.identity.region,
- endpoint_type=CONF.alarming.endpoint_type,
- **self.default_params_with_timeout_values)
+ def _set_network_clients(self):
+ self.network_agents_client = self.network.AgentsClient()
+ self.network_extensions_client = self.network.ExtensionsClient()
+ self.networks_client = self.network.NetworksClient()
+ self.subnetpools_client = self.network.SubnetpoolsClient()
+ self.subnets_client = self.network.SubnetsClient()
+ self.ports_client = self.network.PortsClient()
+ self.network_quotas_client = self.network.QuotasClient()
+ self.floating_ips_client = self.network.FloatingIPsClient()
+ self.metering_labels_client = self.network.MeteringLabelsClient()
+ self.metering_label_rules_client = (
+ self.network.MeteringLabelRulesClient())
+ self.routers_client = self.network.RoutersClient()
+ self.security_group_rules_client = (
+ self.network.SecurityGroupRulesClient())
+ self.security_groups_client = self.network.SecurityGroupsClient()
+ self.network_versions_client = self.network.NetworkVersionsClient()
+ self.service_providers_client = self.network.ServiceProvidersClient()
+ self.tags_client = self.network.TagsClient()
+
+ def _set_image_clients(self):
if CONF.service_available.glance:
- self.image_client = ImagesClient(
- self.auth_provider,
- CONF.image.catalog_type,
- CONF.image.region or CONF.identity.region,
- endpoint_type=CONF.image.endpoint_type,
- build_interval=CONF.image.build_interval,
- build_timeout=CONF.image.build_timeout,
- **self.default_params)
- self.image_client_v2 = ImagesClientV2(
- self.auth_provider,
- CONF.image.catalog_type,
- CONF.image.region or CONF.identity.region,
- endpoint_type=CONF.image.endpoint_type,
- build_interval=CONF.image.build_interval,
- build_timeout=CONF.image.build_timeout,
- **self.default_params)
- self.orchestration_client = OrchestrationClient(
- self.auth_provider,
- CONF.orchestration.catalog_type,
- CONF.orchestration.region or CONF.identity.region,
- endpoint_type=CONF.orchestration.endpoint_type,
- build_interval=CONF.orchestration.build_interval,
- build_timeout=CONF.orchestration.build_timeout,
- **self.default_params)
- self.data_processing_client = DataProcessingClient(
- self.auth_provider,
- CONF.data_processing.catalog_type,
- CONF.identity.region,
- endpoint_type=CONF.data_processing.endpoint_type,
- **self.default_params_with_timeout_values)
- self.negative_client = negative_rest_client.NegativeRestClient(
- self.auth_provider, service, **self.default_params)
-
- self._set_api_microversions()
+ self.image_client = self.image_v1.ImagesClient()
+ self.image_member_client = self.image_v1.ImageMembersClient()
+ self.image_client_v2 = self.image_v2.ImagesClient()
+ self.image_member_client_v2 = self.image_v2.ImageMembersClient()
+ self.namespaces_client = self.image_v2.NamespacesClient()
+ self.resource_types_client = self.image_v2.ResourceTypesClient()
+ self.namespace_objects_client = \
+ self.image_v2.NamespaceObjectsClient()
+ self.schemas_client = self.image_v2.SchemasClient()
+ self.namespace_properties_client = \
+ self.image_v2.NamespacePropertiesClient()
+ self.namespace_tags_client = \
+ self.image_v2.NamespaceTagsClient()
+ self.image_versions_client = \
+ self.image_v2.VersionsClient()
def _set_compute_clients(self):
- params = {
- 'service': CONF.compute.catalog_type,
- 'region': CONF.compute.region or CONF.identity.region,
- 'endpoint_type': CONF.compute.endpoint_type,
- 'build_interval': CONF.compute.build_interval,
- 'build_timeout': CONF.compute.build_timeout
- }
- params.update(self.default_params)
-
- self.agents_client = AgentsClient(self.auth_provider, **params)
- self.compute_networks_client = ComputeNetworksClient(
- self.auth_provider, **params)
- self.migrations_client = MigrationsClient(self.auth_provider,
- **params)
+ self.agents_client = self.compute.AgentsClient()
+ self.compute_networks_client = self.compute.NetworksClient()
+ self.migrations_client = self.compute.MigrationsClient()
self.security_group_default_rules_client = (
- SecurityGroupDefaultRulesClient(self.auth_provider, **params))
- self.certificates_client = CertificatesClient(self.auth_provider,
- **params)
- self.servers_client = ServersClient(
- self.auth_provider,
- enable_instance_password=CONF.compute_feature_enabled
- .enable_instance_password,
- **params)
- self.server_groups_client = ServerGroupsClient(
- self.auth_provider, **params)
- self.limits_client = LimitsClient(self.auth_provider, **params)
- self.images_client = ComputeImagesClient(self.auth_provider, **params)
- self.keypairs_client = KeyPairsClient(self.auth_provider, **params)
- self.quotas_client = QuotasClient(self.auth_provider, **params)
- self.quota_classes_client = QuotaClassesClient(self.auth_provider,
- **params)
- self.flavors_client = FlavorsClient(self.auth_provider, **params)
- self.extensions_client = ExtensionsClient(self.auth_provider,
- **params)
- self.floating_ip_pools_client = FloatingIPPoolsClient(
- self.auth_provider, **params)
- self.floating_ips_bulk_client = FloatingIPsBulkClient(
- self.auth_provider, **params)
- self.compute_floating_ips_client = ComputeFloatingIPsClient(
- self.auth_provider, **params)
- self.security_group_rules_client = SecurityGroupRulesClient(
- self.auth_provider, **params)
- self.compute_security_groups_client = ComputeSecurityGroupsClient(
- self.auth_provider, **params)
- self.interfaces_client = InterfacesClient(self.auth_provider,
- **params)
- self.fixed_ips_client = FixedIPsClient(self.auth_provider,
- **params)
- self.availability_zone_client = AvailabilityZoneClient(
- self.auth_provider, **params)
- self.aggregates_client = AggregatesClient(self.auth_provider,
- **params)
- self.services_client = ServicesClient(self.auth_provider, **params)
- self.tenant_usages_client = TenantUsagesClient(self.auth_provider,
- **params)
- self.hosts_client = HostsClient(self.auth_provider, **params)
- self.hypervisor_client = HypervisorClient(self.auth_provider,
- **params)
- self.instance_usages_audit_log_client = \
- InstanceUsagesAuditLogClient(self.auth_provider, **params)
- self.tenant_networks_client = \
- TenantNetworksClient(self.auth_provider, **params)
- self.baremetal_nodes_client = BaremetalNodesClient(
- self.auth_provider, **params)
+ self.compute.SecurityGroupDefaultRulesClient())
+ self.certificates_client = self.compute.CertificatesClient()
+ eip = CONF.compute_feature_enabled.enable_instance_password
+ self.servers_client = self.compute.ServersClient(
+ enable_instance_password=eip)
+ self.server_groups_client = self.compute.ServerGroupsClient()
+ self.limits_client = self.compute.LimitsClient()
+ self.compute_images_client = self.compute.ImagesClient()
+ self.keypairs_client = self.compute.KeyPairsClient()
+ self.quotas_client = self.compute.QuotasClient()
+ self.quota_classes_client = self.compute.QuotaClassesClient()
+ self.flavors_client = self.compute.FlavorsClient()
+ self.extensions_client = self.compute.ExtensionsClient()
+ self.floating_ip_pools_client = self.compute.FloatingIPPoolsClient()
+ self.floating_ips_bulk_client = self.compute.FloatingIPsBulkClient()
+ self.compute_floating_ips_client = self.compute.FloatingIPsClient()
+ self.compute_security_group_rules_client = (
+ self.compute.SecurityGroupRulesClient())
+ self.compute_security_groups_client = (
+ self.compute.SecurityGroupsClient())
+ self.interfaces_client = self.compute.InterfacesClient()
+ self.fixed_ips_client = self.compute.FixedIPsClient()
+ self.availability_zone_client = self.compute.AvailabilityZoneClient()
+ self.aggregates_client = self.compute.AggregatesClient()
+ self.services_client = self.compute.ServicesClient()
+ self.tenant_usages_client = self.compute.TenantUsagesClient()
+ self.hosts_client = self.compute.HostsClient()
+ self.hypervisor_client = self.compute.HypervisorClient()
+ self.instance_usages_audit_log_client = (
+ self.compute.InstanceUsagesAuditLogClient())
+ self.tenant_networks_client = self.compute.TenantNetworksClient()
# NOTE: The following client needs special timeout values because
# the API is a proxy for the other component.
- params_volume = copy.deepcopy(params)
- params_volume.update({
+ params_volume = {
'build_interval': CONF.volume.build_interval,
'build_timeout': CONF.volume.build_timeout
- })
- self.volumes_extensions_client = ComputeVolumesClient(
- self.auth_provider, **params_volume)
- self.compute_versions_client = VersionsClient(self.auth_provider,
- **params_volume)
- self.snapshots_extensions_client = ComputeSnapshotsClient(
- self.auth_provider, **params_volume)
-
- def _set_database_clients(self):
- self.database_flavors_client = DatabaseFlavorsClient(
- self.auth_provider,
- CONF.database.catalog_type,
- CONF.identity.region,
- **self.default_params_with_timeout_values)
- self.database_limits_client = DatabaseLimitsClient(
- self.auth_provider,
- CONF.database.catalog_type,
- CONF.identity.region,
- **self.default_params_with_timeout_values)
- self.database_versions_client = DatabaseVersionsClient(
- self.auth_provider,
- CONF.database.catalog_type,
- CONF.identity.region,
- **self.default_params_with_timeout_values)
+ }
+ self.volumes_extensions_client = self.compute.VolumesClient(
+ **params_volume)
+ self.compute_versions_client = self.compute.VersionsClient(
+ **params_volume)
+ self.snapshots_extensions_client = self.compute.SnapshotsClient(
+ **params_volume)
def _set_identity_clients(self):
- params = {
- 'service': CONF.identity.catalog_type,
- 'region': CONF.identity.region
- }
- params.update(self.default_params_with_timeout_values)
- params_v2_admin = params.copy()
- params_v2_admin['endpoint_type'] = CONF.identity.v2_admin_endpoint_type
- # Client uses admin endpoint type of Keystone API v2
- self.identity_client = IdentityClient(self.auth_provider,
- **params_v2_admin)
- self.tenants_client = TenantsClient(self.auth_provider,
- **params_v2_admin)
- self.roles_client = RolesClient(self.auth_provider,
- **params_v2_admin)
- params_v2_public = params.copy()
- params_v2_public['endpoint_type'] = (
- CONF.identity.v2_public_endpoint_type)
- # Client uses public endpoint type of Keystone API v2
- self.identity_public_client = IdentityClient(self.auth_provider,
- **params_v2_public)
- self.tenants_public_client = TenantsClient(self.auth_provider,
- **params_v2_public)
- self.roles_public_client = RolesClient(self.auth_provider,
- **params_v2_public)
- params_v3 = params.copy()
- params_v3['endpoint_type'] = CONF.identity.v3_endpoint_type
- # Clients below use the endpoint type of Keystone API v3
- self.identity_v3_client = IdentityV3Client(self.auth_provider,
- **params_v3)
- self.endpoints_client = EndPointV3Client(self.auth_provider,
- **params_v3)
- self.identity_services_client = IdentityServicesV3Client(
- self.auth_provider, **params_v3)
- self.policies_client = PoliciesV3Client(self.auth_provider,
- **params_v3)
- self.regions_client = RegionsV3Client(self.auth_provider, **params_v3)
- self.credentials_client = CredentialsV3Client(self.auth_provider,
- **params_v3)
- self.groups_client = GroupsV3Client(self.auth_provider, **params_v3)
+ # Clients below use the admin endpoint type of Keystone API v2
+ params_v2_admin = {
+ 'endpoint_type': CONF.identity.v2_admin_endpoint_type}
+ self.endpoints_client = self.identity_v2.EndpointsClient(
+ **params_v2_admin)
+ self.identity_client = self.identity_v2.IdentityClient(
+ **params_v2_admin)
+ self.tenants_client = self.identity_v2.TenantsClient(
+ **params_v2_admin)
+ self.roles_client = self.identity_v2.RolesClient(**params_v2_admin)
+ self.users_client = self.identity_v2.UsersClient(**params_v2_admin)
+ self.identity_services_client = self.identity_v2.ServicesClient(
+ **params_v2_admin)
+
+ # Clients below use the public endpoint type of Keystone API v2
+ params_v2_public = {
+ 'endpoint_type': CONF.identity.v2_public_endpoint_type}
+ self.identity_public_client = self.identity_v2.IdentityClient(
+ **params_v2_public)
+ self.tenants_public_client = self.identity_v2.TenantsClient(
+ **params_v2_public)
+ self.users_public_client = self.identity_v2.UsersClient(
+ **params_v2_public)
+
+ # Clients below use the endpoint type of Keystone API v3, which is set
+ # in endpoint_type
+ params_v3 = {'endpoint_type': CONF.identity.v3_endpoint_type}
+ self.domains_client = self.identity_v3.DomainsClient(**params_v3)
+ self.identity_v3_client = self.identity_v3.IdentityClient(**params_v3)
+ self.trusts_client = self.identity_v3.TrustsClient(**params_v3)
+ self.users_v3_client = self.identity_v3.UsersClient(**params_v3)
+ self.endpoints_v3_client = self.identity_v3.EndPointsClient(
+ **params_v3)
+ self.roles_v3_client = self.identity_v3.RolesClient(**params_v3)
+ self.inherited_roles_client = self.identity_v3.InheritedRolesClient(
+ **params_v3)
+ self.role_assignments_client = self.identity_v3.RoleAssignmentsClient(
+ **params_v3)
+ self.identity_services_v3_client = self.identity_v3.ServicesClient(
+ **params_v3)
+ self.policies_client = self.identity_v3.PoliciesClient(**params_v3)
+ self.projects_client = self.identity_v3.ProjectsClient(**params_v3)
+ self.regions_client = self.identity_v3.RegionsClient(**params_v3)
+ self.credentials_client = self.identity_v3.CredentialsClient(
+ **params_v3)
+ self.groups_client = self.identity_v3.GroupsClient(**params_v3)
+ self.identity_versions_v3_client = self.identity_v3.VersionsClient(
+ **params_v3)
+ self.oauth_consumers_client = self.identity_v3.OAUTHConsumerClient(
+ **params_v3)
+ self.oauth_token_client = self.identity_v3.OAUTHTokenClient(
+ **params_v3)
+ self.domain_config_client = self.identity_v3.DomainConfigurationClient(
+ **params_v3)
+ self.endpoint_filter_client = \
+ self.identity_v3.EndPointsFilterClient(**params_v3)
+ 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
# API version is marked as enabled
if CONF.identity_feature_enabled.api_v2:
if CONF.identity.uri:
- self.token_client = TokenClient(
- CONF.identity.uri, **self.default_params)
+ self.token_client = self.identity_v2.TokenClient(
+ auth_url=CONF.identity.uri)
else:
msg = 'Identity v2 API enabled, but no identity.uri set'
- raise exceptions.InvalidConfiguration(msg)
+ raise lib_exc.InvalidConfiguration(msg)
if CONF.identity_feature_enabled.api_v3:
if CONF.identity.uri_v3:
- self.token_v3_client = V3TokenClient(
- CONF.identity.uri_v3, **self.default_params)
+ self.token_v3_client = self.identity_v3.V3TokenClient(
+ auth_url=CONF.identity.uri_v3)
else:
msg = 'Identity v3 API enabled, but no identity.uri_v3 set'
- raise exceptions.InvalidConfiguration(msg)
+ raise lib_exc.InvalidConfiguration(msg)
def _set_volume_clients(self):
- params = {
- 'service': CONF.volume.catalog_type,
- 'region': CONF.volume.region or CONF.identity.region,
- 'endpoint_type': CONF.volume.endpoint_type,
- 'build_interval': CONF.volume.build_interval,
- 'build_timeout': CONF.volume.build_timeout
- }
- params.update(self.default_params)
- self.volume_qos_client = QosSpecsClient(self.auth_provider,
- **params)
- self.volume_qos_v2_client = QosSpecsV2Client(
- self.auth_provider, **params)
- self.volume_services_client = VolumeServicesClient(
- self.auth_provider, **params)
- self.volume_services_v2_client = VolumeServicesV2Client(
- self.auth_provider, **params)
- self.backups_client = BackupsClient(self.auth_provider, **params)
- self.backups_v2_client = BackupsV2Client(self.auth_provider,
- **params)
- self.snapshots_client = SnapshotsClient(self.auth_provider,
- **params)
- self.snapshots_v2_client = SnapshotsV2Client(self.auth_provider,
- **params)
- self.volumes_client = VolumesClient(
- self.auth_provider, default_volume_size=CONF.volume.volume_size,
- **params)
- self.volumes_v2_client = VolumesV2Client(
- self.auth_provider, default_volume_size=CONF.volume.volume_size,
- **params)
- self.volume_types_client = VolumeTypesClient(self.auth_provider,
- **params)
- self.volume_types_v2_client = VolumeTypesV2Client(
- self.auth_provider, **params)
- self.volume_hosts_client = VolumeHostsClient(self.auth_provider,
- **params)
- self.volume_hosts_v2_client = VolumeHostsV2Client(
- self.auth_provider, **params)
- self.volume_quotas_client = VolumeQuotasClient(self.auth_provider,
- **params)
- self.volume_quotas_v2_client = VolumeQuotasV2Client(self.auth_provider,
- **params)
- self.volumes_extension_client = VolumeExtensionsClient(
- self.auth_provider, **params)
- self.volumes_v2_extension_client = VolumeExtensionsV2Client(
- self.auth_provider, **params)
- self.volume_availability_zone_client = \
- VolumeAvailabilityZoneClient(self.auth_provider, **params)
- self.volume_v2_availability_zone_client = \
- VolumeAvailabilityZoneV2Client(self.auth_provider, **params)
+ if CONF.volume_feature_enabled.api_v1:
+ self.backups_client = self.volume_v1.BackupsClient()
+ self.encryption_types_client = \
+ self.volume_v1.EncryptionTypesClient()
+ self.snapshots_client = self.volume_v1.SnapshotsClient()
+ self.volume_availability_zone_client = \
+ self.volume_v1.AvailabilityZoneClient()
+ self.volume_hosts_client = self.volume_v1.HostsClient()
+ self.volume_limits_client = self.volume_v1.LimitsClient()
+ self.volume_qos_client = self.volume_v1.QosSpecsClient()
+ self.volume_quotas_client = self.volume_v1.QuotasClient()
+ self.volume_services_client = self.volume_v1.ServicesClient()
+ self.volume_types_client = self.volume_v1.TypesClient()
+ self.volumes_client = self.volume_v1.VolumesClient()
+ self.volumes_extension_client = self.volume_v1.ExtensionsClient()
+
+ # if only api_v3 is enabled, all these clients should be available
+ if (CONF.volume_feature_enabled.api_v2 or
+ CONF.volume_feature_enabled.api_v3):
+ self.backups_v2_client = self.volume_v3.BackupsClient()
+ self.encryption_types_v2_client = \
+ self.volume_v3.EncryptionTypesClient()
+ self.snapshot_manage_v2_client = \
+ self.volume_v3.SnapshotManageClient()
+ self.snapshots_v2_client = self.volume_v3.SnapshotsClient()
+ self.volume_capabilities_v2_client = \
+ self.volume_v3.CapabilitiesClient()
+ self.volume_manage_v2_client = self.volume_v3.VolumeManageClient()
+ self.volume_qos_v2_client = self.volume_v3.QosSpecsClient()
+ self.volume_services_v2_client = self.volume_v3.ServicesClient()
+ self.volume_types_v2_client = self.volume_v3.TypesClient()
+ self.volume_hosts_v2_client = self.volume_v3.HostsClient()
+ self.volume_quotas_v2_client = self.volume_v3.QuotasClient()
+ self.volume_quota_classes_v2_client = \
+ self.volume_v3.QuotaClassesClient()
+ self.volume_scheduler_stats_v2_client = \
+ self.volume_v3.SchedulerStatsClient()
+ self.volume_transfers_v2_client = \
+ self.volume_v3.TransfersClient()
+ self.volume_v2_availability_zone_client = \
+ self.volume_v3.AvailabilityZoneClient()
+ self.volume_v2_limits_client = self.volume_v3.LimitsClient()
+ self.volumes_v2_client = self.volume_v3.VolumesClient()
+ self.volumes_v2_extension_client = \
+ self.volume_v3.ExtensionsClient()
+
+ # 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()
+ self.group_types_v3_client = self.volume_v3.GroupTypesClient()
+ self.groups_v3_client = self.volume_v3.GroupsClient()
+ self.group_snapshots_v3_client = \
+ self.volume_v3.GroupSnapshotsClient()
+ self.snapshots_v3_client = self.volume_v3.SnapshotsClient()
+ self.volume_v3_messages_client = self.volume_v3.MessagesClient()
+ self.volume_v3_versions_client = self.volume_v3.VersionsClient()
+ self.volumes_v3_client = self.volume_v3.VolumesClient()
+
+ # 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):
- params = {
- 'service': CONF.object_storage.catalog_type,
- 'region': CONF.object_storage.region or CONF.identity.region,
- 'endpoint_type': CONF.object_storage.endpoint_type
- }
- params.update(self.default_params_with_timeout_values)
+ self.account_client = self.object_storage.AccountClient()
+ self.bulk_client = self.object_storage.BulkMiddlewareClient()
+ self.capabilities_client = self.object_storage.CapabilitiesClient()
+ self.container_client = self.object_storage.ContainerClient()
+ self.object_client = self.object_storage.ObjectClient()
- self.account_client = AccountClient(self.auth_provider, **params)
- self.container_client = ContainerClient(self.auth_provider, **params)
- self.object_client = ObjectClient(self.auth_provider, **params)
- def _set_api_microversions(self):
- service_clients = [x for x in self.__dict__ if x.endswith('_client')]
- for client in service_clients:
- client_obj = getattr(self, client)
- microversion = self.api_microversions.get(client_obj.service)
- if microversion:
- if hasattr(client_obj, 'set_api_microversion'):
- client_obj.set_api_microversion(microversion)
- else:
- LOG.debug("Need to implement set_api_microversion on %s"
- % client)
+def get_auth_provider_class(credentials):
+ if isinstance(credentials, auth.KeystoneV3Credentials):
+ return auth.KeystoneV3AuthProvider, CONF.identity.uri_v3
+ else:
+ return auth.KeystoneV2AuthProvider, CONF.identity.uri
+
+
+def get_auth_provider(credentials, pre_auth=False, scope='project'):
+ # kwargs for auth provider match the common ones used by service clients
+ default_params = config.service_client_config()
+ if credentials is None:
+ raise lib_exc.InvalidCredentials(
+ 'Credentials must be specified')
+ auth_provider_class, auth_url = get_auth_provider_class(
+ credentials)
+ _auth_provider = auth_provider_class(credentials, auth_url,
+ scope=scope,
+ **default_params)
+ if pre_auth:
+ _auth_provider.set_auth()
+ return _auth_provider
diff --git a/tempest/cmd/account_generator.py b/tempest/cmd/account_generator.py
index 5a2713b..1c671ec 100755
--- a/tempest/cmd/account_generator.py
+++ b/tempest/cmd/account_generator.py
@@ -15,18 +15,18 @@
# under the License.
"""
-Utility for creating **accounts.yaml** file for concurrent test runs.
+Utility for creating ``accounts.yaml`` file for concurrent test runs.
Creates one primary user, one alt user, one swift admin, one stack owner
and one admin (optionally) for each concurrent thread. The utility creates
-user for each tenant. The **accounts.yaml** file will be valid and contain
+user for each tenant. The ``accounts.yaml`` file will be valid and contain
credentials for created users, so each user will be in separate tenant and
have the username, tenant_name, password and roles.
-**Usage:** ``tempest-account-generator [-h] [OPTIONS] accounts_file.yaml``.
+**Usage:** ``tempest account-generator [-h] [OPTIONS] accounts_file.yaml``
Positional Arguments
--------------------
-**accounts_file.yaml** (Required) Provide an output accounts yaml file. Utility
+``accounts_file.yaml`` (Required) Provide an output accounts yaml file. Utility
creates a .yaml file in the directory where the command is ran. The appropriate
name for the file is *accounts.yaml* and it should be placed in *tempest/etc*
directory.
@@ -38,72 +38,88 @@
of your cloud to operate properly. The corresponding info can be given either
through CLI options or environment variables.
-You're probably familiar with these, but just to remind::
+You're probably familiar with these, but just to remind:
- +----------+------------------+----------------------+
- | Param | CLI | Environment Variable |
- +----------+------------------+----------------------+
- | Username | --os-username | OS_USERNAME |
- | Password | --os-password | OS_PASSWORD |
- | Tenant | --os-tenant-name | OS_TENANT_NAME |
- +----------+------------------+----------------------+
+======== ============================ ====================
+Param CLI Environment Variable
+======== ============================ ====================
+Username ``--os-username`` OS_USERNAME
+Password ``--os-password`` OS_PASSWORD
+Project ``--os-project-name`` OS_PROJECT_NAME
+Tenant ``--os-tenant-name`` (depr.) OS_TENANT_NAME
+Domain ``--os-domain-name`` OS_DOMAIN_NAME
+======== ============================ ====================
Optional Arguments
------------------
-**-h**, **--help** (Optional) Shows help message with the description of
-utility and its arguments, and exits.
+* ``-h, --help`` (Optional) Shows help message with the description of
+ utility and its arguments, and exits.
-**c /etc/tempest.conf**, **--config-file /etc/tempest.conf** (Optional) Path to
-tempest config file.
+* ``-c, --config-file /etc/tempest.conf`` (Optional) Path
+ to tempest config file. If not specified, it searches for tempest.conf in
+ these locations:
-**--os-username <auth-user-name>** (Optional) Name used for authentication with
-the OpenStack Identity service. Defaults to env[OS_USERNAME]. Note: User should
-have permissions to create new user accounts and tenants.
+ - ./etc/
+ - /etc/tempest
+ - ~/.tempest/
+ - ~/
+ - /etc/
-**--os-password <auth-password>** (Optional) Password used for authentication
-with the OpenStack Identity service. Defaults to env[OS_PASSWORD].
+* ``--os-username <auth-user-name>`` (Optional) Name used for authentication
+ with the OpenStack Identity service. Defaults to env[OS_USERNAME]. Note: User
+ should have permissions to create new user accounts and tenants.
-**--os-tenant-name <auth-tenant-name>** (Optional) Tenant to request
-authorization on. Defaults to env[OS_TENANT_NAME].
+* ``--os-password <auth-password>`` (Optional) Password used for authentication
+ with the OpenStack Identity service. Defaults to env[OS_PASSWORD].
-**--tag TAG** (Optional) Resources tag. Each created resource (user, project)
-will have the prefix with the given TAG in its name. Using tag is recommended
-for the further using, cleaning resources.
+* ``--os-project-name <auth-project-name>`` (Optional) Project to request
+ authorization on. Defaults to env[OS_PROJECT_NAME].
-**-r CONCURRENCY**, **--concurrency CONCURRENCY** (Required) Concurrency count
-(default: 1). The number of accounts required can be estimated as
-CONCURRENCY x 2. Each user provided in *accounts.yaml* file will be in
-a different tenant. This is required to provide isolation between test for
-running in parallel.
+* ``--os-tenant-name <auth-tenant-name>`` (Optional, deprecated) Tenant to
+ request authorization on. Defaults to env[OS_TENANT_NAME].
-**--with-admin** (Optional) Creates admin for each concurrent group
-(default: False).
+* ``--os-domain-name <auth-domain-name>`` (Optional) Domain the user and
+ project belong to. Defaults to env[OS_DOMAIN_NAME].
-To see help on specific argument, please do: ``tempest-account-generator
+* ``--tag TAG`` (Optional) Resources tag. Each created resource (user, project)
+ will have the prefix with the given TAG in its name. Using tag is recommended
+ for the further using, cleaning resources.
+
+* ``-r, --concurrency CONCURRENCY`` (Optional) Concurrency count
+ (default: 1). The number of accounts required can be estimated as
+ CONCURRENCY x 2. Each user provided in *accounts.yaml* file will be in
+ a different tenant. This is required to provide isolation between test for
+ running in parallel.
+
+* ``--with-admin`` (Optional) Creates admin for each concurrent group
+ (default: False).
+
+* ``-i, --identity-version VERSION`` (Optional) Provisions accounts
+ using the specified version of the identity API. (default: '3').
+
+To see help on specific argument, please do: ``tempest account-generator
[OPTIONS] <accounts_file.yaml> -h``.
"""
import argparse
-import netaddr
import os
+import traceback
+from cliff import command
from oslo_log import log as logging
import yaml
-from tempest.common import identity
+from tempest.common import credentials_factory
from tempest import config
-from tempest import exceptions as exc
-from tempest.services.identity.v2.json import identity_client
-from tempest.services.identity.v2.json import roles_client
-from tempest.services.identity.v2.json import tenants_client
-from tempest.services.network.json import network_client
-from tempest.services.network.json import networks_client
-from tempest.services.network.json import subnets_client
-import tempest_lib.auth
-from tempest_lib.common.utils import data_utils
-import tempest_lib.exceptions
+from tempest.lib.common import dynamic_creds
+
LOG = None
CONF = config.CONF
+DESCRIPTION = ('Create accounts.yaml file for concurrent test runs.%s'
+ 'One primary user, one alt user, '
+ 'one swift admin, one stack owner '
+ 'and one admin (optionally) will be created '
+ 'for each concurrent thread.' % os.linesep)
def setup_logging():
@@ -112,296 +128,84 @@
LOG = logging.getLogger(__name__)
-def get_admin_clients(opts):
- _creds = tempest_lib.auth.KeystoneV2Credentials(
- username=opts.os_username,
- password=opts.os_password,
- tenant_name=opts.os_tenant_name)
- auth_params = {
- 'disable_ssl_certificate_validation':
- CONF.identity.disable_ssl_certificate_validation,
- 'ca_certs': CONF.identity.ca_certificates_file,
- 'trace_requests': CONF.debug.trace_requests
- }
- _auth = tempest_lib.auth.KeystoneV2AuthProvider(
- _creds, CONF.identity.uri, **auth_params)
- params = {
- 'disable_ssl_certificate_validation':
- CONF.identity.disable_ssl_certificate_validation,
- 'ca_certs': CONF.identity.ca_certificates_file,
- 'trace_requests': CONF.debug.trace_requests,
- 'build_interval': CONF.compute.build_interval,
- 'build_timeout': CONF.compute.build_timeout
- }
- identity_admin = identity_client.IdentityClient(
- _auth,
- CONF.identity.catalog_type,
- CONF.identity.region,
- endpoint_type='adminURL',
- **params
- )
- tenants_admin = tenants_client.TenantsClient(
- _auth,
- CONF.identity.catalog_type,
- CONF.identity.region,
- endpoint_type='adminURL',
- **params
- )
- roles_admin = roles_client.RolesClient(
- _auth,
- CONF.identity.catalog_type,
- CONF.identity.region,
- endpoint_type='adminURL',
- **params
- )
- network_admin = None
- networks_admin = None
- subnets_admin = None
- neutron_iso_networks = False
- if (CONF.service_available.neutron and
- CONF.auth.create_isolated_networks):
- neutron_iso_networks = True
- network_admin = network_client.NetworkClient(
- _auth,
- CONF.network.catalog_type,
- CONF.network.region or CONF.identity.region,
- endpoint_type='adminURL',
- **params)
- networks_admin = networks_client.NetworksClient(
- _auth,
- CONF.network.catalog_type,
- CONF.network.region or CONF.identity.region,
- endpoint_type='adminURL',
- **params)
- subnets_admin = subnets_client.SubnetsClient(
- _auth,
- CONF.network.catalog_type,
- CONF.network.region or CONF.identity.region,
- endpoint_type='adminURL',
- **params)
- return (identity_admin, tenants_admin, roles_admin, neutron_iso_networks,
- network_admin, networks_admin, subnets_admin)
+def get_credential_provider(opts):
+ identity_version = "".join(['v', str(opts.identity_version)])
+ # NOTE(andreaf) For now tempest.conf controls whether resources will
+ # actually be created. Once we remove the dependency from tempest.conf
+ # we will need extra CLI option(s) to control this.
+ network_resources = {'router': True,
+ 'network': True,
+ 'subnet': True,
+ 'dhcp': True}
+ admin_creds_dict = {'username': opts.os_username,
+ 'password': opts.os_password}
+ _project_name = opts.os_project_name or opts.os_tenant_name
+ if opts.identity_version == 3:
+ admin_creds_dict['project_name'] = _project_name
+ admin_creds_dict['domain_name'] = opts.os_domain_name or 'Default'
+ elif opts.identity_version == 2:
+ admin_creds_dict['tenant_name'] = _project_name
+ admin_creds = credentials_factory.get_credentials(
+ fill_in=False, identity_version=identity_version, **admin_creds_dict)
+ return dynamic_creds.DynamicCredentialProvider(
+ name=opts.tag,
+ network_resources=network_resources,
+ **credentials_factory.get_dynamic_provider_params(
+ identity_version, admin_creds=admin_creds))
-def create_resources(opts, resources):
- (identity_admin, tenants_admin, roles_admin, neutron_iso_networks,
- network_admin, networks_admin, subnets_admin) = get_admin_clients(opts)
- roles = roles_admin.list_roles()['roles']
- for u in resources['users']:
- u['role_ids'] = []
- for r in u.get('roles', ()):
- try:
- role = filter(lambda r_: r_['name'] == r, roles)[0]
- except IndexError:
- msg = "Role: %s doesn't exist" % r
- raise exc.InvalidConfiguration(msg)
- u['role_ids'] += [role['id']]
- existing = [x['name'] for x in tenants_admin.list_tenants()['tenants']]
- for tenant in resources['tenants']:
- if tenant not in existing:
- tenants_admin.create_tenant(tenant)
- else:
- LOG.warning("Tenant '%s' already exists in this environment"
- % tenant)
- LOG.info('Tenants created')
- for u in resources['users']:
- try:
- tenant = identity.get_tenant_by_name(tenants_admin, u['tenant'])
- except tempest_lib.exceptions.NotFound:
- LOG.error("Tenant: %s - not found" % u['tenant'])
- continue
- while True:
- try:
- identity.get_user_by_username(tenants_admin,
- tenant['id'], u['name'])
- except tempest_lib.exceptions.NotFound:
- identity_admin.create_user(
- u['name'], u['pass'], tenant['id'],
- "%s@%s" % (u['name'], tenant['id']),
- enabled=True)
- break
- else:
- LOG.warning("User '%s' already exists in this environment. "
- "New name generated" % u['name'])
- u['name'] = random_user_name(opts.tag, u['prefix'])
-
- LOG.info('Users created')
- if neutron_iso_networks:
- for u in resources['users']:
- tenant = identity.get_tenant_by_name(tenants_admin, u['tenant'])
- network_name, router_name = create_network_resources(
- network_admin, networks_admin, subnets_admin, tenant['id'],
- u['name'])
- u['network'] = network_name
- u['router'] = router_name
- LOG.info('Networks created')
- for u in resources['users']:
- try:
- tenant = identity.get_tenant_by_name(tenants_admin, u['tenant'])
- except tempest_lib.exceptions.NotFound:
- LOG.error("Tenant: %s - not found" % u['tenant'])
- continue
- try:
- user = identity.get_user_by_username(tenants_admin,
- tenant['id'], u['name'])
- except tempest_lib.exceptions.NotFound:
- LOG.error("User: %s - not found" % u['user'])
- continue
- for r in u['role_ids']:
- try:
- roles_admin.assign_user_role(tenant['id'], user['id'], r)
- except tempest_lib.exceptions.Conflict:
- # don't care if it's already assigned
- pass
- LOG.info('Roles assigned')
- LOG.info('Resources deployed successfully!')
-
-
-def create_network_resources(network_admin_client, networks_admin_client,
- subnets_admin_client, tenant_id, name):
-
- def _create_network(name):
- resp_body = networks_admin_client.create_network(
- name=name, tenant_id=tenant_id)
- return resp_body['network']
-
- def _create_subnet(subnet_name, network_id):
- base_cidr = netaddr.IPNetwork(CONF.network.tenant_network_cidr)
- mask_bits = CONF.network.tenant_network_mask_bits
- for subnet_cidr in base_cidr.subnet(mask_bits):
- try:
- resp_body = subnets_admin_client.\
- create_subnet(
- network_id=network_id, cidr=str(subnet_cidr),
- name=subnet_name,
- tenant_id=tenant_id,
- enable_dhcp=True,
- ip_version=4)
- break
- except tempest_lib.exceptions.BadRequest as e:
- if 'overlaps with another subnet' not in str(e):
- raise
- else:
- message = 'Available CIDR for subnet creation could not be found'
- raise Exception(message)
- return resp_body['subnet']
-
- def _create_router(router_name):
- external_net_id = dict(
- network_id=CONF.network.public_network_id)
- resp_body = network_admin_client.create_router(
- router_name,
- external_gateway_info=external_net_id,
- tenant_id=tenant_id)
- return resp_body['router']
-
- def _add_router_interface(router_id, subnet_id):
- network_admin_client.add_router_interface_with_subnet_id(
- router_id, subnet_id)
-
- network_name = name + "-network"
- network = _create_network(network_name)
- subnet_name = name + "-subnet"
- subnet = _create_subnet(subnet_name, network['id'])
- router_name = name + "-router"
- router = _create_router(router_name)
- _add_router_interface(router['id'], subnet['id'])
- return network_name, router_name
-
-
-def random_user_name(tag, prefix):
- if tag:
- return data_utils.rand_name('-'.join((tag, prefix)))
- else:
- return data_utils.rand_name(prefix)
-
-
-def generate_resources(opts):
- spec = [{'number': 1,
- 'prefix': 'primary',
- 'roles': (CONF.auth.tempest_roles +
- [CONF.object_storage.operator_role])},
- {'number': 1,
- 'prefix': 'alt',
- 'roles': (CONF.auth.tempest_roles +
- [CONF.object_storage.operator_role])}]
+def generate_resources(cred_provider, admin):
+ # Create the list of resources to be provisioned for each process
+ # NOTE(andreaf) get_credentials expects a string for types or a list for
+ # roles. Adding all required inputs to the spec list.
+ spec = ['primary', 'alt']
if CONF.service_available.swift:
- spec.append({'number': 1,
- 'prefix': 'swift_operator',
- 'roles': (CONF.auth.tempest_roles +
- [CONF.object_storage.operator_role])})
- spec.append({'number': 1,
- 'prefix': 'swift_reseller_admin',
- 'roles': (CONF.auth.tempest_roles +
- [CONF.object_storage.reseller_admin_role])})
- if CONF.service_available.heat:
- spec.append({'number': 1,
- 'prefix': 'stack_owner',
- 'roles': (CONF.auth.tempest_roles +
- [CONF.orchestration.stack_owner_role])})
- if opts.admin:
- spec.append({
- 'number': 1,
- 'prefix': 'admin',
- 'roles': (CONF.auth.tempest_roles +
- [CONF.identity.admin_role])
- })
- resources = {'tenants': [],
- 'users': []}
- for count in range(opts.concurrency):
- for user_group in spec:
- users = [random_user_name(opts.tag, user_group['prefix'])
- for _ in range(user_group['number'])]
- for user in users:
- tenant = '-'.join((user, 'tenant'))
- resources['tenants'].append(tenant)
- resources['users'].append({
- 'tenant': tenant,
- 'name': user,
- 'pass': data_utils.rand_password(),
- 'prefix': user_group['prefix'],
- 'roles': user_group['roles']
- })
+ spec.append([CONF.object_storage.operator_role])
+ spec.append([CONF.object_storage.reseller_admin_role])
+ spec.append([CONF.object_storage.operator_role])
+ if admin:
+ spec.append('admin')
+ resources = []
+ for cred_type in spec:
+ resources.append((cred_type, cred_provider.get_credentials(
+ credential_type=cred_type)))
return resources
-def dump_accounts(opts, resources):
+def dump_accounts(resources, identity_version, account_file):
accounts = []
- for user in resources['users']:
+ for resource in resources:
+ cred_type, test_resource = resource
account = {
- 'username': user['name'],
- 'tenant_name': user['tenant'],
- 'password': user['pass'],
- 'roles': user['roles']
+ 'username': test_resource.username,
+ 'password': test_resource.password
}
- if 'network' or 'router' in user:
+ if identity_version == 3:
+ account['project_name'] = test_resource.project_name
+ account['domain_name'] = test_resource.domain_name
+ else:
+ account['project_name'] = test_resource.tenant_name
+
+ # If the spec includes 'admin' credentials are defined via type,
+ # else they are defined via list of roles.
+ if cred_type == 'admin':
+ account['types'] = [cred_type]
+ elif cred_type not in ['primary', 'alt']:
+ account['roles'] = cred_type
+
+ if test_resource.network:
account['resources'] = {}
- if 'network' in user:
- account['resources']['network'] = user['network']
- if 'router' in user:
- account['resources']['router'] = user['router']
+ if test_resource.network:
+ account['resources']['network'] = test_resource.network['name']
accounts.append(account)
- if os.path.exists(opts.accounts):
- os.rename(opts.accounts, '.'.join((opts.accounts, 'bak')))
- with open(opts.accounts, 'w') as f:
- yaml.dump(accounts, f, default_flow_style=False)
- LOG.info('%s generated successfully!' % opts.accounts)
+ if os.path.exists(account_file):
+ os.rename(account_file, '.'.join((account_file, 'bak')))
+ with open(account_file, 'w') as f:
+ yaml.safe_dump(accounts, f, default_flow_style=False)
+ LOG.info('%s generated successfully!', account_file)
-def get_options():
- usage_string = ('tempest-account-generator [-h] <ARG> ...\n\n'
- 'To see help on specific argument, do:\n'
- 'tempest-account-generator <ARG> -h')
- parser = argparse.ArgumentParser(
- description='Create accounts.yaml file for concurrent test runs. '
- 'One primary user, one alt user, '
- 'one swift admin, one stack owner '
- 'and one admin (optionally) will be created '
- 'for each concurrent thread.',
- formatter_class=argparse.ArgumentDefaultsHelpFormatter,
- usage=usage_string
- )
-
+def _parser_add_args(parser):
parser.add_argument('-c', '--config-file',
metavar='/etc/tempest.conf',
help='path to tempest config file')
@@ -415,10 +219,18 @@
metavar='<auth-password>',
default=os.environ.get('OS_PASSWORD'),
help='Defaults to env[OS_PASSWORD].')
+ parser.add_argument('--os-project-name',
+ metavar='<auth-project-name>',
+ default=os.environ.get('OS_PROJECT_NAME'),
+ help='Defaults to env[OS_PROJECT_NAME].')
parser.add_argument('--os-tenant-name',
metavar='<auth-tenant-name>',
default=os.environ.get('OS_TENANT_NAME'),
help='Defaults to env[OS_TENANT_NAME].')
+ parser.add_argument('--os-domain-name',
+ metavar='<auth-domain-name>',
+ default=os.environ.get('OS_DOMAIN_NAME'),
+ help='Defaults to env[OS_DOMAIN_NAME].')
parser.add_argument('--tag',
default='',
required=False,
@@ -427,30 +239,77 @@
parser.add_argument('-r', '--concurrency',
default=1,
type=int,
- required=True,
+ required=False,
dest='concurrency',
help='Concurrency count')
parser.add_argument('--with-admin',
action='store_true',
dest='admin',
help='Creates admin for each concurrent group')
+ parser.add_argument('-i', '--identity-version',
+ default=3,
+ choices=[2, 3],
+ type=int,
+ required=False,
+ dest='identity_version',
+ help='Version of the Identity API to use')
parser.add_argument('accounts',
metavar='accounts_file.yaml',
help='Output accounts yaml file')
+
+def get_options():
+ usage_string = ('tempest account-generator [-h] <ARG> ...\n\n'
+ 'To see help on specific argument, do:\n'
+ 'tempest account-generator <ARG> -h')
+ parser = argparse.ArgumentParser(
+ description=DESCRIPTION,
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+ usage=usage_string
+ )
+
+ _parser_add_args(parser)
opts = parser.parse_args()
- if opts.config_file:
- config.CONF.set_config_path(opts.config_file)
return opts
+class TempestAccountGenerator(command.Command):
+
+ def get_parser(self, prog_name):
+ parser = super(TempestAccountGenerator, self).get_parser(prog_name)
+ _parser_add_args(parser)
+ return parser
+
+ def take_action(self, parsed_args):
+ try:
+ main(parsed_args)
+ except Exception:
+ LOG.exception("Failure generating test accounts.")
+ traceback.print_exc()
+ raise
+
+ def get_description(self):
+ return DESCRIPTION
+
+
def main(opts=None):
- if not opts:
- opts = get_options()
setup_logging()
- resources = generate_resources(opts)
- create_resources(opts, resources)
- dump_accounts(opts, resources)
+ if not opts:
+ LOG.warning("Use of: 'tempest-account-generator' is deprecated, "
+ "please use: 'tempest account-generator'")
+ opts = get_options()
+ if opts.config_file:
+ config.CONF.set_config_path(opts.config_file)
+ if opts.os_tenant_name:
+ LOG.warning("'os-tenant-name' and 'OS_TENANT_NAME' are both "
+ "deprecated, please use 'os-project-name' or "
+ "'OS_PROJECT_NAME' instead")
+ resources = []
+ for count in range(opts.concurrency):
+ # Use N different cred_providers to obtain different sets of creds
+ cred_provider = get_credential_provider(opts)
+ resources.extend(generate_resources(cred_provider, opts.admin))
+ dump_accounts(resources, opts.identity_version, opts.accounts)
if __name__ == "__main__":
main()
diff --git a/tempest/cmd/cleanup.py b/tempest/cmd/cleanup.py
index 7b73a61..29abd49 100644
--- a/tempest/cmd/cleanup.py
+++ b/tempest/cmd/cleanup.py
@@ -14,41 +14,64 @@
# under the License.
"""
-Utility for cleaning up environment after Tempest run
+Utility for cleaning up environment after Tempest test run
+
+**Usage:** ``tempest cleanup [--help] [OPTIONS]``
+
+If run with no arguments, ``tempest cleanup`` will query your OpenStack
+deployment and build a list of resources to delete and destroy them. This list
+will exclude the resources from ``saved_state.json`` and will include the
+configured admin account if the ``--delete-tempest-conf-objects`` flag is
+specified. By default the admin project is not deleted and the admin user
+specified in ``tempest.conf`` is never deleted.
+
+Example Run
+-----------
+
+.. warning::
+
+ If step 1 is skipped in the example below, the cleanup procedure
+ may delete resources that existed in the cloud before the test run. This
+ may cause an unwanted destruction of cloud resources, so use caution with
+ this command.
+
+ Examples::
+
+ $ tempest cleanup --init-saved-state
+ $ # Actual running of Tempest tests
+ $ tempest cleanup
Runtime Arguments
-----------------
-**--init-saved-state**: Before you can execute cleanup you must initialize
-the saved state by running it with the **--init-saved-state** flag
-(creating ./saved_state.json), which protects your deployment from
-cleanup deleting objects you want to keep. Typically you would run
-cleanup with **--init-saved-state** prior to a tempest run. If this is not
-the case saved_state.json must be edited, removing objects you want
-cleanup to delete.
+* ``--init-saved-state``: Initializes the saved state of the OpenStack
+ deployment and will output a ``saved_state.json`` file containing resources
+ from your deployment that will be preserved from the cleanup command. This
+ should be done prior to running Tempest tests.
-**--dry-run**: Creates a report (dry_run.json) of the tenants that will be
-cleaned up (in the "_tenants_to_clean" array), and the global objects
-that will be removed (tenants, users, flavors and images). Once
-cleanup is executed in normal mode, running it again with **--dry-run**
-should yield an empty report.
+* ``--delete-tempest-conf-objects``: If option is present, then the command
+ will delete the admin project in addition to the resources associated with
+ them on clean up. If option is not present, the command will delete the
+ resources associated with the Tempest and alternate Tempest users and
+ projects but will not delete the projects themselves.
-**NOTE**: The _tenants_to_clean array in dry-run.json lists the
-tenants that cleanup will loop through and delete child objects, not
-delete the tenant itself. This may differ from the tenants array as you
-can clean the tempest and alternate tempest tenants but by default,
-cleanup deletes the objects in the tempest and alternate tempest tenants
-but does not delete those tenants unless the **--delete-tempest-conf-objects**
-flag is used to force their deletion.
+* ``--dry-run``: Creates a report (``./dry_run.json``) of the projects that
+ will be cleaned up (in the ``_projects_to_clean`` dictionary [1]_) and the
+ global objects that will be removed (domains, flavors, images, roles,
+ projects, and users). Once the cleanup command is executed (e.g. run without
+ parameters), running it again with ``--dry-run`` should yield an empty
+ report.
-**Normal mode**: running with no arguments, will query your deployment and
-build a list of objects to delete after filtering out the objects found in
-saved_state.json and based on the **--delete-tempest-conf-objects** flag.
+* ``--help``: Print the help text for the command and parameters.
-By default the tempest and alternate tempest users and tenants are not
-deleted and the admin user specified in tempest.conf is never deleted.
+.. [1] The ``_projects_to_clean`` dictionary in ``dry_run.json`` lists the
+ projects that ``tempest cleanup`` will loop through to delete child
+ objects, but the command will, by default, not delete the projects
+ themselves. This may differ from the ``projects`` list as you can clean
+ the Tempest and alternate Tempest users and projects but they will not be
+ deleted unless the ``--delete-tempest-conf-objects`` flag is used to
+ force their deletion.
-Please run with **--help** to see full list of options.
"""
import sys
import traceback
@@ -71,35 +94,33 @@
class TempestCleanup(command.Command):
- def __init__(self, app, cmd):
- super(TempestCleanup, self).__init__(app, cmd)
-
def take_action(self, parsed_args):
try:
self.init(parsed_args)
- self._cleanup()
+ if not parsed_args.init_saved_state:
+ self._cleanup()
except Exception:
LOG.exception("Failure during cleanup")
traceback.print_exc()
raise
- return 0
def init(self, parsed_args):
cleanup_service.init_conf()
self.options = parsed_args
- self.admin_mgr = credentials.AdminManager()
+ self.admin_mgr = clients.Manager(
+ credentials.get_configured_admin_credentials())
self.dry_run_data = {}
self.json_data = {}
self.admin_id = ""
self.admin_role_id = ""
- self.admin_tenant_id = ""
+ self.admin_project_id = ""
self._init_admin_ids()
self.admin_role_added = []
# available services
- self.tenant_services = cleanup_service.get_tenant_cleanup_services()
+ self.project_services = cleanup_service.get_project_cleanup_services()
self.global_services = cleanup_service.get_global_cleanup_services()
if parsed_args.init_saved_state:
@@ -109,31 +130,30 @@
self._load_json()
def _cleanup(self):
- print ("Begin cleanup")
+ print("Begin cleanup")
is_dry_run = self.options.dry_run
is_preserve = not self.options.delete_tempest_conf_objects
is_save_state = False
if is_dry_run:
- self.dry_run_data["_tenants_to_clean"] = {}
- f = open(DRY_RUN_JSON, 'w+')
+ self.dry_run_data["_projects_to_clean"] = {}
admin_mgr = self.admin_mgr
- # Always cleanup tempest and alt tempest tenants unless
+ # Always cleanup tempest and alt tempest projects unless
# they are in saved state json. Therefore is_preserve is False
kwargs = {'data': self.dry_run_data,
'is_dry_run': is_dry_run,
'saved_state_json': self.json_data,
'is_preserve': False,
'is_save_state': is_save_state}
- tenant_service = cleanup_service.TenantService(admin_mgr, **kwargs)
- tenants = tenant_service.list()
- print ("Process %s tenants" % len(tenants))
+ project_service = cleanup_service.ProjectService(admin_mgr, **kwargs)
+ projects = project_service.list()
+ print("Process %s projects" % len(projects))
- # Loop through list of tenants and clean them up.
- for tenant in tenants:
- self._add_admin(tenant['id'])
- self._clean_tenant(tenant)
+ # Loop through list of projects and clean them up.
+ for project in projects:
+ self._add_admin(project['id'])
+ self._clean_project(project)
kwargs = {'data': self.dry_run_data,
'is_dry_run': is_dry_run,
@@ -145,56 +165,58 @@
svc.run()
if is_dry_run:
- f.write(json.dumps(self.dry_run_data, sort_keys=True,
- indent=2, separators=(',', ': ')))
- f.close()
+ with open(DRY_RUN_JSON, 'w+') as f:
+ f.write(json.dumps(self.dry_run_data, sort_keys=True,
+ indent=2, separators=(',', ': ')))
self._remove_admin_user_roles()
def _remove_admin_user_roles(self):
- tenant_ids = self.admin_role_added
- LOG.debug("Removing admin user roles where needed for tenants: %s"
- % tenant_ids)
- for tenant_id in tenant_ids:
- self._remove_admin_role(tenant_id)
+ project_ids = self.admin_role_added
+ LOG.debug("Removing admin user roles where needed for projects: %s",
+ project_ids)
+ for project_id in project_ids:
+ self._remove_admin_role(project_id)
- def _clean_tenant(self, tenant):
- print ("Cleaning tenant: %s " % tenant['name'])
+ def _clean_project(self, project):
+ print("Cleaning project: %s " % project['name'])
is_dry_run = self.options.dry_run
dry_run_data = self.dry_run_data
is_preserve = not self.options.delete_tempest_conf_objects
- tenant_id = tenant['id']
- tenant_name = tenant['name']
- tenant_data = None
+ project_id = project['id']
+ project_name = project['name']
+ project_data = None
if is_dry_run:
- tenant_data = dry_run_data["_tenants_to_clean"][tenant_id] = {}
- tenant_data['name'] = tenant_name
+ project_data = dry_run_data["_projects_to_clean"][project_id] = {}
+ project_data['name'] = project_name
kwargs = {"username": CONF.auth.admin_username,
"password": CONF.auth.admin_password,
- "tenant_name": tenant['name']}
+ "project_name": project['name']}
mgr = clients.Manager(credentials=credentials.get_credentials(
**kwargs))
- kwargs = {'data': tenant_data,
+ kwargs = {'data': project_data,
'is_dry_run': is_dry_run,
'saved_state_json': None,
'is_preserve': is_preserve,
'is_save_state': False,
- 'tenant_id': tenant_id}
- for service in self.tenant_services:
+ 'project_id': project_id}
+ for service in self.project_services:
svc = service(mgr, **kwargs)
svc.run()
def _init_admin_ids(self):
- tn_cl = self.admin_mgr.tenants_client
- rl_cl = self.admin_mgr.roles_client
+ pr_cl = self.admin_mgr.projects_client
+ rl_cl = self.admin_mgr.roles_v3_client
+ rla_cl = self.admin_mgr.role_assignments_client
+ us_cl = self.admin_mgr.users_v3_client
- tenant = identity.get_tenant_by_name(tn_cl,
- CONF.auth.admin_tenant_name)
- self.admin_tenant_id = tenant['id']
-
- user = identity.get_user_by_username(tn_cl, self.admin_tenant_id,
- CONF.auth.admin_username)
+ project = identity.get_project_by_name(pr_cl,
+ CONF.auth.admin_project_name)
+ self.admin_project_id = project['id']
+ user = identity.get_user_by_project(us_cl, rla_cl,
+ self.admin_project_id,
+ CONF.auth.admin_username)
self.admin_id = user['id']
roles = rl_cl.list_roles()['roles']
@@ -219,7 +241,7 @@
dest='delete_tempest_conf_objects',
default=False,
help="Force deletion of the tempest and "
- "alternate tempest users and tenants.")
+ "alternate tempest users and projects.")
parser.add_argument('--dry-run', action="store_true",
dest='dry_run', default=False,
help="Generate JSON file:" + DRY_RUN_JSON +
@@ -230,46 +252,48 @@
def get_description(self):
return 'Cleanup after tempest run'
- def _add_admin(self, tenant_id):
- id_cl = self.admin_mgr.identity_client
- rl_cl = self.admin_mgr.roles_client
+ def _add_admin(self, project_id):
+ rl_cl = self.admin_mgr.roles_v3_client
needs_role = True
- roles = rl_cl.list_user_roles(tenant_id, self.admin_id)['roles']
+ roles = rl_cl.list_user_roles_on_project(project_id,
+ self.admin_id)['roles']
for role in roles:
if role['id'] == self.admin_role_id:
needs_role = False
- LOG.debug("User already had admin privilege for this tenant")
+ LOG.debug("User already had admin privilege for this project")
if needs_role:
- LOG.debug("Adding admin privilege for : %s" % tenant_id)
- id_cl.assign_user_role(tenant_id, self.admin_id,
- self.admin_role_id)
- self.admin_role_added.append(tenant_id)
+ LOG.debug("Adding admin privilege for : %s", project_id)
+ rl_cl.create_user_role_on_project(project_id, self.admin_id,
+ self.admin_role_id)
+ self.admin_role_added.append(project_id)
- def _remove_admin_role(self, tenant_id):
- LOG.debug("Remove admin user role for tenant: %s" % tenant_id)
- # Must initialize AdminManager for each user role
+ def _remove_admin_role(self, project_id):
+ LOG.debug("Remove admin user role for projectt: %s", project_id)
+ # Must initialize Admin Manager for each user role
# Otherwise authentication exception is thrown, weird
- id_cl = credentials.AdminManager().identity_client
- if (self._tenant_exists(tenant_id)):
+ id_cl = clients.Manager(
+ credentials.get_configured_admin_credentials()).identity_client
+ if (self._project_exists(project_id)):
try:
- id_cl.delete_user_role(tenant_id, self.admin_id,
- self.admin_role_id)
+ id_cl.delete_role_from_user_on_project(project_id,
+ self.admin_id,
+ self.admin_role_id)
except Exception as ex:
- LOG.exception("Failed removing role from tenant which still"
- "exists, exception: %s" % ex)
+ LOG.exception("Failed removing role from project which still"
+ "exists, exception: %s", ex)
- def _tenant_exists(self, tenant_id):
- tn_cl = self.admin_mgr.tenants_client
+ def _project_exists(self, project_id):
+ pr_cl = self.admin_mgr.projects_client
try:
- t = tn_cl.show_tenant(tenant_id)
- LOG.debug("Tenant is: %s" % str(t))
+ p = pr_cl.show_project(project_id)
+ LOG.debug("Project is: %s", str(p))
return True
except Exception as ex:
- LOG.debug("Tenant no longer exists? %s" % ex)
+ LOG.debug("Project no longer exists? %s", ex)
return False
def _init_state(self):
- print ("Initializing saved state.")
+ print("Initializing saved state.")
data = {}
admin_mgr = self.admin_mgr
kwargs = {'data': data,
@@ -281,21 +305,20 @@
svc = service(admin_mgr, **kwargs)
svc.run()
- f = open(SAVED_STATE_JSON, 'w+')
- f.write(json.dumps(data,
- sort_keys=True, indent=2, separators=(',', ': ')))
- f.close()
+ with open(SAVED_STATE_JSON, 'w+') as f:
+ f.write(json.dumps(data,
+ sort_keys=True, indent=2, separators=(',', ': ')))
def _load_json(self):
try:
- json_file = open(SAVED_STATE_JSON)
- self.json_data = json.load(json_file)
- json_file.close()
+ with open(SAVED_STATE_JSON) as json_file:
+ self.json_data = json.load(json_file)
+
except IOError as ex:
LOG.exception("Failed loading saved state, please be sure you"
" have first run cleanup with --init-saved-state "
- "flag prior to running tempest. Exception: %s" % ex)
+ "flag prior to running tempest. Exception: %s", ex)
sys.exit(ex)
except Exception as ex:
- LOG.exception("Exception parsing saved state json : %s" % ex)
+ LOG.exception("Exception parsing saved state json : %s", ex)
sys.exit(ex)
diff --git a/tempest/cmd/cleanup_service.py b/tempest/cmd/cleanup_service.py
index 2aeb5b1..27e1bc1 100644
--- a/tempest/cmd/cleanup_service.py
+++ b/tempest/cmd/cleanup_service.py
@@ -16,10 +16,12 @@
from oslo_log import log as logging
+from tempest import clients
from tempest.common import credentials_factory as credentials
from tempest.common import identity
+from tempest.common import utils
+from tempest.common.utils import net_info
from tempest import config
-from tempest import test
LOG = logging.getLogger(__name__)
CONF = config.CONF
@@ -30,13 +32,11 @@
CONF_PRIV_NETWORK_NAME = None
CONF_PUB_NETWORK = None
CONF_PUB_ROUTER = None
-CONF_TENANTS = None
+CONF_PROJECTS = None
CONF_USERS = None
-IS_CEILOMETER = None
IS_CINDER = None
IS_GLANCE = None
-IS_HEAT = None
IS_NEUTRON = None
IS_NOVA = None
@@ -49,19 +49,16 @@
global CONF_PRIV_NETWORK_NAME
global CONF_PUB_NETWORK
global CONF_PUB_ROUTER
- global CONF_TENANTS
+ global CONF_PROJECTS
global CONF_USERS
- global IS_CEILOMETER
global IS_CINDER
global IS_GLANCE
global IS_HEAT
global IS_NEUTRON
global IS_NOVA
- IS_CEILOMETER = CONF.service_available.ceilometer
IS_CINDER = CONF.service_available.cinder
IS_GLANCE = CONF.service_available.glance
- IS_HEAT = CONF.service_available.heat
IS_NEUTRON = CONF.service_available.neutron
IS_NOVA = CONF.service_available.nova
@@ -70,29 +67,27 @@
CONF_PRIV_NETWORK_NAME = CONF.compute.fixed_network_name
CONF_PUB_NETWORK = CONF.network.public_network_id
CONF_PUB_ROUTER = CONF.network.public_router_id
- CONF_TENANTS = [CONF.auth.admin_tenant_name,
- CONF.identity.tenant_name,
- CONF.identity.alt_tenant_name]
- CONF_USERS = [CONF.auth.admin_username, CONF.identity.username,
- CONF.identity.alt_username]
+ CONF_PROJECTS = [CONF.auth.admin_project_name]
+ CONF_USERS = [CONF.auth.admin_username]
if IS_NEUTRON:
CONF_PRIV_NETWORK = _get_network_id(CONF.compute.fixed_network_name,
- CONF.identity.tenant_name)
+ CONF.auth.admin_project_name)
CONF_NETWORKS = [CONF_PUB_NETWORK, CONF_PRIV_NETWORK]
-def _get_network_id(net_name, tenant_name):
- am = credentials.AdminManager()
+def _get_network_id(net_name, project_name):
+ am = clients.Manager(
+ credentials.get_configured_admin_credentials())
net_cl = am.networks_client
- tn_cl = am.tenants_client
+ pr_cl = am.projects_client
networks = net_cl.list_networks()
- tenant = identity.get_tenant_by_name(tn_cl, tenant_name)
- t_id = tenant['id']
+ project = identity.get_project_by_name(pr_cl, project_name)
+ p_id = project['id']
n_id = None
for net in networks['networks']:
- if (net['tenant_id'] == t_id and net['name'] == net_name):
+ if (net['project_id'] == p_id and net['name'] == net_name):
n_id = net['id']
break
return n_id
@@ -109,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 len(item_list) == 0
- 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
@@ -144,12 +139,12 @@
def __init__(self, manager, **kwargs):
super(SnapshotService, self).__init__(kwargs)
- self.client = manager.snapshots_client
+ self.client = manager.snapshots_client_latest
def list(self):
client = self.client
snaps = client.list_snapshots()['snapshots']
- LOG.debug("List count, %s Snapshots" % len(snaps))
+ LOG.debug("List count, %s Snapshots", len(snaps))
return snaps
def delete(self):
@@ -176,7 +171,7 @@
client = self.client
servers_body = client.list_servers()
servers = servers_body['servers']
- LOG.debug("List count, %s Servers" % len(servers))
+ LOG.debug("List count, %s Servers", len(servers))
return servers
def delete(self):
@@ -198,7 +193,7 @@
def list(self):
client = self.server_groups_client
sgs = client.list_server_groups()['server_groups']
- LOG.debug("List count, %s Server Groups" % len(sgs))
+ LOG.debug("List count, %s Server Groups", len(sgs))
return sgs
def delete(self):
@@ -215,31 +210,6 @@
self.data['server_groups'] = sgs
-class StackService(BaseService):
- def __init__(self, manager, **kwargs):
- super(StackService, self).__init__(kwargs)
- self.client = manager.orchestration_client
-
- def list(self):
- client = self.client
- stacks = client.list_stacks()['stacks']
- LOG.debug("List count, %s Stacks" % len(stacks))
- return stacks
-
- def delete(self):
- client = self.client
- stacks = self.list()
- for stack in stacks:
- try:
- client.delete_stack(stack['id'])
- except Exception:
- LOG.exception("Delete Stack exception.")
-
- def dry_run(self):
- stacks = self.list()
- self.data['stacks'] = stacks
-
-
class KeyPairService(BaseService):
def __init__(self, manager, **kwargs):
super(KeyPairService, self).__init__(kwargs)
@@ -248,7 +218,7 @@
def list(self):
client = self.client
keypairs = client.list_keypairs()['keypairs']
- LOG.debug("List count, %s Keypairs" % len(keypairs))
+ LOG.debug("List count, %s Keypairs", len(keypairs))
return keypairs
def delete(self):
@@ -275,7 +245,7 @@
client = self.client
secgrps = client.list_security_groups()['security_groups']
secgrp_del = [grp for grp in secgrps if grp['name'] != 'default']
- LOG.debug("List count, %s Security Groups" % len(secgrp_del))
+ LOG.debug("List count, %s Security Groups", len(secgrp_del))
return secgrp_del
def delete(self):
@@ -300,7 +270,7 @@
def list(self):
client = self.client
floating_ips = client.list_floating_ips()['floating_ips']
- LOG.debug("List count, %s Floating IPs" % len(floating_ips))
+ LOG.debug("List count, %s Floating IPs", len(floating_ips))
return floating_ips
def delete(self):
@@ -320,12 +290,12 @@
class VolumeService(BaseService):
def __init__(self, manager, **kwargs):
super(VolumeService, self).__init__(kwargs)
- self.client = manager.volumes_client
+ self.client = manager.volumes_client_latest
def list(self):
client = self.client
vols = client.list_volumes()['volumes']
- LOG.debug("List count, %s Volumes" % len(vols))
+ LOG.debug("List count, %s Volumes", len(vols))
return vols
def delete(self):
@@ -345,7 +315,7 @@
class VolumeQuotaService(BaseService):
def __init__(self, manager, **kwargs):
super(VolumeQuotaService, self).__init__(kwargs)
- self.client = manager.volume_quotas_client
+ self.client = manager.volume_quotas_v2_client
def delete(self):
client = self.client
@@ -355,7 +325,8 @@
LOG.exception("Delete Volume Quotas exception.")
def dry_run(self):
- quotas = self.client.show_quota_usage(self.tenant_id)['quota_set']
+ quotas = self.client.show_quota_set(
+ self.tenant_id, params={'usage': True})['quota_set']
self.data['volume_quotas'] = quotas
@@ -382,7 +353,6 @@
class NetworkService(BaseService):
def __init__(self, manager, **kwargs):
super(NetworkService, self).__init__(kwargs)
- self.client = manager.network_client
self.networks_client = manager.networks_client
self.subnets_client = manager.subnets_client
self.ports_client = manager.ports_client
@@ -390,6 +360,7 @@
self.metering_labels_client = manager.metering_labels_client
self.metering_label_rules_client = manager.metering_label_rules_client
self.security_groups_client = manager.security_groups_client
+ self.routers_client = manager.routers_client
def _filter_by_conf_networks(self, item_list):
if not item_list or not all(('network_id' in i for i in item_list)):
@@ -406,7 +377,7 @@
if self.is_preserve:
networks = [network for network in networks
if network['id'] not in CONF_NETWORKS]
- LOG.debug("List count, %s Networks" % networks)
+ LOG.debug("List count, %s Networks", networks)
return networks
def delete(self):
@@ -429,7 +400,7 @@
client = self.floating_ips_client
flips = client.list_floatingips(**self.tenant_filter)
flips = flips['floatingips']
- LOG.debug("List count, %s Network Floating IPs" % len(flips))
+ LOG.debug("List count, %s Network Floating IPs", len(flips))
return flips
def delete(self):
@@ -449,28 +420,28 @@
class NetworkRouterService(NetworkService):
def list(self):
- client = self.client
+ client = self.routers_client
routers = client.list_routers(**self.tenant_filter)
routers = routers['routers']
if self.is_preserve:
routers = [router for router in routers
if router['id'] != CONF_PUB_ROUTER]
- LOG.debug("List count, %s Routers" % len(routers))
+ LOG.debug("List count, %s Routers", len(routers))
return routers
def delete(self):
- client = self.client
+ client = self.routers_client
+ ports_client = self.ports_client
routers = self.list()
for router in routers:
try:
rid = router['id']
ports = [port for port
- in client.list_router_interfaces(rid)['ports']
- if port["device_owner"] == "network:router_interface"]
+ in ports_client.list_ports(device_id=rid)['ports']
+ if net_info.is_router_interface_port(port)]
for port in ports:
- client.remove_router_interface_with_port_id(rid,
- port['id'])
+ client.remove_router_interface(rid, port_id=port['id'])
client.delete_router(rid)
except Exception:
LOG.exception("Delete Router exception.")
@@ -487,7 +458,7 @@
hms = client.list_health_monitors()
hms = hms['health_monitors']
hms = self._filter_by_tenant_id(hms)
- LOG.debug("List count, %s Health Monitors" % len(hms))
+ LOG.debug("List count, %s Health Monitors", len(hms))
return hms
def delete(self):
@@ -511,7 +482,7 @@
members = client.list_members()
members = members['members']
members = self._filter_by_tenant_id(members)
- LOG.debug("List count, %s Members" % len(members))
+ LOG.debug("List count, %s Members", len(members))
return members
def delete(self):
@@ -535,7 +506,7 @@
vips = client.list_vips()
vips = vips['vips']
vips = self._filter_by_tenant_id(vips)
- LOG.debug("List count, %s VIPs" % len(vips))
+ LOG.debug("List count, %s VIPs", len(vips))
return vips
def delete(self):
@@ -559,7 +530,7 @@
pools = client.list_pools()
pools = pools['pools']
pools = self._filter_by_tenant_id(pools)
- LOG.debug("List count, %s Pools" % len(pools))
+ LOG.debug("List count, %s Pools", len(pools))
return pools
def delete(self):
@@ -583,7 +554,7 @@
rules = client.list_metering_label_rules()
rules = rules['metering_label_rules']
rules = self._filter_by_tenant_id(rules)
- LOG.debug("List count, %s Metering Label Rules" % len(rules))
+ LOG.debug("List count, %s Metering Label Rules", len(rules))
return rules
def delete(self):
@@ -607,7 +578,7 @@
labels = client.list_metering_labels()
labels = labels['metering_labels']
labels = self._filter_by_tenant_id(labels)
- LOG.debug("List count, %s Metering Labels" % len(labels))
+ LOG.debug("List count, %s Metering Labels", len(labels))
return labels
def delete(self):
@@ -636,7 +607,7 @@
if self.is_preserve:
ports = self._filter_by_conf_networks(ports)
- LOG.debug("List count, %s Ports" % len(ports))
+ LOG.debug("List count, %s Ports", len(ports))
return ports
def delete(self):
@@ -664,7 +635,7 @@
if self.is_preserve:
secgroups = self._filter_by_conf_networks(secgroups)
- LOG.debug("List count, %s securtiy_groups" % len(secgroups))
+ LOG.debug("List count, %s security_groups", len(secgroups))
return secgroups
def delete(self):
@@ -689,7 +660,7 @@
subnets = subnets['subnets']
if self.is_preserve:
subnets = self._filter_by_conf_networks(subnets)
- LOG.debug("List count, %s Subnets" % len(subnets))
+ LOG.debug("List count, %s Subnets", len(subnets))
return subnets
def delete(self):
@@ -706,32 +677,6 @@
self.data['subnets'] = subnets
-# Telemetry services
-class TelemetryAlarmService(BaseService):
- def __init__(self, manager, **kwargs):
- super(TelemetryAlarmService, self).__init__(kwargs)
- self.client = manager.telemetry_client
-
- def list(self):
- client = self.client
- alarms = client.list_alarms()
- LOG.debug("List count, %s Alarms" % len(alarms))
- return alarms
-
- def delete(self):
- client = self.client
- alarms = self.list()
- for alarm in alarms:
- try:
- client.delete_alarm(alarm['id'])
- except Exception:
- LOG.exception("Delete Alarms exception.")
-
- def dry_run(self):
- alarms = self.list()
- self.data['alarms'] = alarms
-
-
# begin global services
class FlavorService(BaseService):
def __init__(self, manager, **kwargs):
@@ -749,7 +694,7 @@
if self.is_preserve:
flavors = [flavor for flavor in flavors
if flavor['id'] not in CONF_FLAVORS]
- LOG.debug("List count, %s Flavors after reconcile" % len(flavors))
+ LOG.debug("List count, %s Flavors after reconcile", len(flavors))
return flavors
def delete(self):
@@ -775,7 +720,7 @@
class ImageService(BaseService):
def __init__(self, manager, **kwargs):
super(ImageService, self).__init__(kwargs)
- self.client = manager.images_client
+ self.client = manager.compute_images_client
def list(self):
client = self.client
@@ -786,7 +731,7 @@
if self.is_preserve:
images = [image for image in images
if image['id'] not in CONF_IMAGES]
- LOG.debug("List count, %s Images after reconcile" % len(images))
+ LOG.debug("List count, %s Images after reconcile", len(images))
return images
def delete(self):
@@ -812,14 +757,17 @@
class IdentityService(BaseService):
def __init__(self, manager, **kwargs):
super(IdentityService, self).__init__(kwargs)
- self.client = manager.identity_client
+ self.client = manager.identity_v3_client
-class UserService(IdentityService):
+class UserService(BaseService):
+
+ def __init__(self, manager, **kwargs):
+ super(UserService, self).__init__(kwargs)
+ self.client = manager.users_v3_client
def list(self):
- client = self.client
- users = client.list_users()['users']
+ users = self.client.list_users()['users']
if not self.is_save_state:
users = [user for user in users if user['id']
@@ -833,15 +781,14 @@
users = [user for user in users if user['name'] !=
CONF.auth.admin_username]
- LOG.debug("List count, %s Users after reconcile" % len(users))
+ LOG.debug("List count, %s Users after reconcile", len(users))
return users
def delete(self):
- client = self.client
users = self.list()
for user in users:
try:
- client.delete_user(user['id'])
+ self.client.delete_user(user['id'])
except Exception:
LOG.exception("Delete User exception.")
@@ -869,9 +816,9 @@
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)]
- LOG.debug("List count, %s Roles after reconcile" % len(roles))
+ 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:
LOG.exception("Cannot retrieve Roles.")
@@ -896,50 +843,53 @@
self.data['roles'][role['id']] = role['name']
-class TenantService(BaseService):
+class ProjectService(BaseService):
def __init__(self, manager, **kwargs):
- super(TenantService, self).__init__(kwargs)
- self.client = manager.tenants_client
+ super(ProjectService, self).__init__(kwargs)
+ self.client = manager.projects_client
def list(self):
- tenants = self.client.list_tenants()['tenants']
+ projects = self.client.list_projects()['projects']
if not self.is_save_state:
- tenants = [tenant for tenant in tenants if (tenant['id']
- not in self.saved_state_json['tenants'].keys()
- and tenant['name'] != CONF.auth.admin_tenant_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:
- tenants = [tenant for tenant in tenants if tenant['name']
- not in CONF_TENANTS]
+ projects = [project
+ for project in projects
+ if project['name'] not in CONF_PROJECTS]
- LOG.debug("List count, %s Tenants after reconcile" % len(tenants))
- return tenants
+ LOG.debug("List count, %s Projects after reconcile", len(projects))
+ return projects
def delete(self):
- tenants = self.list()
- for tenant in tenants:
+ projects = self.list()
+ for project in projects:
try:
- self.client.delete_tenant(tenant['id'])
+ self.client.delete_project(project['id'])
except Exception:
- LOG.exception("Delete Tenant exception.")
+ LOG.exception("Delete project exception.")
def dry_run(self):
- tenants = self.list()
- self.data['tenants'] = tenants
+ projects = self.list()
+ self.data['projects'] = projects
def save_state(self):
- tenants = self.list()
- self.data['tenants'] = {}
- for tenant in tenants:
- self.data['tenants'][tenant['id']] = tenant['name']
+ projects = self.list()
+ self.data['projects'] = {}
+ for project in projects:
+ self.data['projects'][project['id']] = project['name']
class DomainService(BaseService):
def __init__(self, manager, **kwargs):
super(DomainService, self).__init__(kwargs)
- self.client = manager.identity_v3_client
+ self.client = manager.domains_client
def list(self):
client = self.client
@@ -948,7 +898,7 @@
domains = [domain for domain in domains if domain['id']
not in self.saved_state_json['domains'].keys()]
- LOG.debug("List count, %s Domains after reconcile" % len(domains))
+ LOG.debug("List count, %s Domains after reconcile", len(domains))
return domains
def delete(self):
@@ -972,35 +922,33 @@
self.data['domains'][domain['id']] = domain['name']
-def get_tenant_cleanup_services():
- tenant_services = []
- if IS_CEILOMETER:
- tenant_services.append(TelemetryAlarmService)
+def get_project_cleanup_services():
+ project_services = []
+ # TODO(gmann): Tempest should provide some plugin hook for cleanup
+ # script extension to plugin tests also.
if IS_NOVA:
- tenant_services.append(ServerService)
- tenant_services.append(KeyPairService)
- tenant_services.append(SecurityGroupService)
- tenant_services.append(ServerGroupService)
+ project_services.append(ServerService)
+ project_services.append(KeyPairService)
+ project_services.append(SecurityGroupService)
+ project_services.append(ServerGroupService)
if not IS_NEUTRON:
- tenant_services.append(FloatingIpService)
- tenant_services.append(NovaQuotaService)
- if IS_HEAT:
- tenant_services.append(StackService)
+ project_services.append(FloatingIpService)
+ project_services.append(NovaQuotaService)
if IS_NEUTRON:
- tenant_services.append(NetworkFloatingIpService)
- if test.is_extension_enabled('metering', 'network'):
- tenant_services.append(NetworkMeteringLabelRuleService)
- tenant_services.append(NetworkMeteringLabelService)
- tenant_services.append(NetworkRouterService)
- tenant_services.append(NetworkPortService)
- tenant_services.append(NetworkSubnetService)
- tenant_services.append(NetworkService)
- tenant_services.append(NetworkSecGroupService)
+ project_services.append(NetworkFloatingIpService)
+ if utils.is_extension_enabled('metering', 'network'):
+ project_services.append(NetworkMeteringLabelRuleService)
+ project_services.append(NetworkMeteringLabelService)
+ project_services.append(NetworkRouterService)
+ project_services.append(NetworkPortService)
+ project_services.append(NetworkSubnetService)
+ project_services.append(NetworkService)
+ project_services.append(NetworkSecGroupService)
if IS_CINDER:
- tenant_services.append(SnapshotService)
- tenant_services.append(VolumeService)
- tenant_services.append(VolumeQuotaService)
- return tenant_services
+ project_services.append(SnapshotService)
+ project_services.append(VolumeService)
+ project_services.append(VolumeQuotaService)
+ return project_services
def get_global_cleanup_services():
@@ -1010,7 +958,7 @@
if IS_GLANCE:
global_services.append(ImageService)
global_services.append(UserService)
- global_services.append(TenantService)
+ global_services.append(ProjectService)
global_services.append(DomainService)
global_services.append(RoleService)
return global_services
diff --git a/tempest/cmd/config-generator.tempest.conf b/tempest/cmd/config-generator.tempest.conf
new file mode 100644
index 0000000..b8f16d9
--- /dev/null
+++ b/tempest/cmd/config-generator.tempest.conf
@@ -0,0 +1,5 @@
+[DEFAULT]
+output_file = etc/tempest.conf.sample
+namespace = tempest.config
+namespace = oslo.concurrency
+namespace = oslo.log
diff --git a/tempest/cmd/init.py b/tempest/cmd/init.py
index ac67ce4..84c8631 100644
--- a/tempest/cmd/init.py
+++ b/tempest/cmd/init.py
@@ -14,22 +14,21 @@
import os
import shutil
-import subprocess
import sys
from cliff import command
+from oslo_config import generator
from oslo_log import log as logging
from six import moves
+from stestr import commands
+
+from tempest.cmd import workspace
LOG = logging.getLogger(__name__)
-TESTR_CONF = """[DEFAULT]
-test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \\
- OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \\
- OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-500} \\
- ${PYTHON:-python} -m subunit.run discover -t %s %s $LISTOPT $IDOPTION
-test_id_option=--load-list $IDFILE
-test_list_option=--list
+STESTR_CONF = """[DEFAULT]
+test_path=%s
+top_dir=%s
group_regex=([^\.]*\.)*
"""
@@ -37,38 +36,28 @@
def get_tempest_default_config_dir():
"""Get default config directory of tempest
- Returns the correct default config dir to support both cases of
- tempest being or not installed in a virtualenv.
- Cases considered:
- - no virtual env, python2: real_prefix and base_prefix not set
- - no virtual env, python3: real_prefix not set, base_prefix set and
- identical to prefix
- - virtualenv, python2: real_prefix and prefix are set and different
- - virtualenv, python3: real_prefix not set, base_prefix and prefix are
- set and identical
- - pyvenv, any python version: real_prefix not set, base_prefix and prefix
- are set and different
+ There are 3 dirs that get tried in priority order. First is /etc/tempest,
+ if that doesn't exist it looks for a tempest dir in the XDG_CONFIG_HOME
+ dir (defaulting to ~/.config/tempest) and last it tries for a
+ ~/.tempest/etc directory. If none of these exist a ~/.tempest/etc
+ directory will be created.
:return: default config dir
"""
- real_prefix = getattr(sys, 'real_prefix', None)
- base_prefix = getattr(sys, 'base_prefix', None)
- prefix = sys.prefix
- if (real_prefix is None and
- (base_prefix is None or base_prefix == prefix)):
- # Probably not running in a virtual environment.
- # NOTE(andreaf) we cannot distinguish this case from the case of
- # a virtual environment created with virtualenv, and running python3.
- # Also if it appears we are not in virtual env and fail to find
- # global config: '/etc/tempest', fall back to
- # '[sys.prefix]/etc/tempest'
- global_conf_dir = '/etc/tempest'
- if os.path.isdir(global_conf_dir):
- return global_conf_dir
- else:
- return os.path.join(prefix, 'etc/tempest')
+ global_conf_dir = '/etc/tempest'
+ xdg_config = os.environ.get('XDG_CONFIG_HOME',
+ os.path.expanduser('~/.config'))
+ user_xdg_global_path = os.path.join(xdg_config, 'tempest')
+ user_global_path = os.path.join(os.path.expanduser('~'), '.tempest/etc')
+ if os.path.isdir(global_conf_dir):
+ return global_conf_dir
+ elif os.path.isdir(user_xdg_global_path):
+ return user_xdg_global_path
+ elif os.path.isdir(user_global_path):
+ return user_global_path
else:
- return os.path.join(prefix, 'etc/tempest')
+ os.makedirs(user_global_path)
+ return user_global_path
class TempestInit(command.Command):
@@ -76,49 +65,78 @@
def get_parser(self, prog_name):
parser = super(TempestInit, self).get_parser(prog_name)
- parser.add_argument('dir', nargs='?', default=os.getcwd())
+ parser.add_argument('dir', nargs='?', default=os.getcwd(),
+ help="The path to the workspace directory. If you "
+ "omit this argument, the workspace directory is "
+ "your current directory")
parser.add_argument('--config-dir', '-c', default=None)
+ parser.add_argument('--show-global-config-dir', '-s',
+ action='store_true', dest='show_global_dir',
+ help="Print the global config dir location, "
+ "then exit")
+ parser.add_argument('--name', help="The workspace name", default=None)
+ parser.add_argument('--workspace-path', default=None,
+ help="The path to the workspace file, the default "
+ "is ~/.tempest/workspace.yaml")
return parser
- def generate_testr_conf(self, local_path):
- testr_conf_path = os.path.join(local_path, '.testr.conf')
+ def generate_stestr_conf(self, local_path):
+ stestr_conf_path = os.path.join(local_path, '.stestr.conf')
top_level_path = os.path.dirname(os.path.dirname(__file__))
discover_path = os.path.join(top_level_path, 'test_discover')
- testr_conf = TESTR_CONF % (top_level_path, discover_path)
- with open(testr_conf_path, 'w+') as testr_conf_file:
- testr_conf_file.write(testr_conf)
+ stestr_conf = STESTR_CONF % (discover_path, top_level_path)
+ with open(stestr_conf_path, 'w+') as stestr_conf_file:
+ stestr_conf_file.write(stestr_conf)
+
+ def get_configparser(self, conf_path):
+ config_parse = moves.configparser.ConfigParser()
+ config_parse.optionxform = str
+ # get any existing values if a config file already exists
+ if os.path.isfile(conf_path):
+ # use read() for Python 2 and 3 compatibility
+ config_parse.read(conf_path)
+ return config_parse
def update_local_conf(self, conf_path, lock_dir, log_dir):
- config_parse = moves.configparser.SafeConfigParser()
- config_parse.optionxform = str
+ config_parse = self.get_configparser(conf_path)
+ # Set local lock_dir in tempest conf
+ if not config_parse.has_section('oslo_concurrency'):
+ config_parse.add_section('oslo_concurrency')
+ config_parse.set('oslo_concurrency', 'lock_path', lock_dir)
+ # Set local log_dir in tempest conf
+ config_parse.set('DEFAULT', 'log_dir', log_dir)
+ # Set default log filename to tempest.log
+ config_parse.set('DEFAULT', 'log_file', 'tempest.log')
+
+ # write out a new file with the updated configurations
with open(conf_path, 'w+') as conf_file:
- config_parse.readfp(conf_file)
- # Set local lock_dir in tempest conf
- if not config_parse.has_section('oslo_concurrency'):
- config_parse.add_section('oslo_concurrency')
- config_parse.set('oslo_concurrency', 'lock_path', lock_dir)
- # Set local log_dir in tempest conf
- config_parse.set('DEFAULT', 'log_dir', log_dir)
- # Set default log filename to tempest.log
- config_parse.set('DEFAULT', 'log_file', 'tempest.log')
+ config_parse.write(conf_file)
def copy_config(self, etc_dir, config_dir):
- shutil.copytree(config_dir, etc_dir)
+ if os.path.isdir(config_dir):
+ shutil.copytree(config_dir, etc_dir)
+ else:
+ LOG.warning("Global config dir %s can't be found", config_dir)
- def generate_sample_config(self, local_dir, config_dir):
- conf_generator = os.path.join(config_dir,
+ def generate_sample_config(self, local_dir):
+ conf_generator = os.path.join(os.path.dirname(__file__),
'config-generator.tempest.conf')
-
- subprocess.call(['oslo-config-generator', '--config-file',
- conf_generator],
- cwd=local_dir)
+ output_file = os.path.join(local_dir, 'etc/tempest.conf.sample')
+ if os.path.isfile(conf_generator):
+ generator.main(['--config-file', conf_generator, '--output-file',
+ output_file])
+ else:
+ LOG.warning("Skipping sample config generation because global "
+ "config file %s can't be found", conf_generator)
def create_working_dir(self, local_dir, config_dir):
+ # make sure we are working with abspath however tempest init is called
+ local_dir = os.path.abspath(local_dir)
# Create local dir if missing
if not os.path.isdir(local_dir):
- LOG.debug('Creating local working dir: %s' % 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)
@@ -126,27 +144,35 @@
etc_dir = os.path.join(local_dir, 'etc')
config_path = os.path.join(etc_dir, 'tempest.conf')
log_dir = os.path.join(local_dir, 'logs')
- testr_dir = os.path.join(local_dir, '.testrepository')
+ stestr_dir = os.path.join(local_dir, '.stestr')
# Create lock dir
if not os.path.isdir(lock_dir):
- LOG.debug('Creating lock dir: %s' % lock_dir)
+ LOG.debug('Creating lock dir: %s', lock_dir)
os.mkdir(lock_dir)
# Create log dir
if not os.path.isdir(log_dir):
- LOG.debug('Creating log dir: %s' % log_dir)
+ LOG.debug('Creating log dir: %s', log_dir)
os.mkdir(log_dir)
# Create and copy local etc dir
self.copy_config(etc_dir, config_dir)
# Generate the sample config file
- self.generate_sample_config(local_dir, config_dir)
+ self.generate_sample_config(local_dir)
# Update local confs to reflect local paths
self.update_local_conf(config_path, lock_dir, log_dir)
- # Generate a testr conf file
- self.generate_testr_conf(local_dir)
- # setup local testr working dir
- if not os.path.isdir(testr_dir):
- subprocess.call(['testr', 'init'], cwd=local_dir)
+ # Generate a stestr conf file
+ self.generate_stestr_conf(local_dir)
+ # setup local stestr working dir
+ if not os.path.isdir(stestr_dir):
+ commands.init_command(repo_url=local_dir)
def take_action(self, parsed_args):
+ workspace_manager = workspace.WorkspaceManager(
+ parsed_args.workspace_path)
+ name = parsed_args.name or parsed_args.dir.split(os.path.sep)[-1]
config_dir = parsed_args.config_dir or get_tempest_default_config_dir()
+ if parsed_args.show_global_dir:
+ print("Global config dir is located at: %s" % config_dir)
+ sys.exit(0)
self.create_working_dir(parsed_args.dir, config_dir)
+ workspace_manager.register_new_workspace(
+ name, parsed_args.dir, init=True)
diff --git a/tempest/cmd/javelin.py b/tempest/cmd/javelin.py
deleted file mode 100755
index 6dfa0a7..0000000
--- a/tempest/cmd/javelin.py
+++ /dev/null
@@ -1,1158 +0,0 @@
-#!/usr/bin/env python
-#
-# 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.
-
-"""Javelin is a tool for creating, verifying, and deleting a small set of
-resources in a declarative way.
-
-Javelin is meant to be used as a way to validate quickly that resources can
-survive an upgrade process.
-
-Authentication
---------------
-
-Javelin will be creating (and removing) users and tenants so it needs the admin
-credentials of your cloud to operate properly. The corresponding info can be
-given the usual way, either through CLI options or environment variables.
-
-You're probably familiar with these, but just in case::
-
- +----------+------------------+----------------------+
- | Param | CLI | Environment Variable |
- +----------+------------------+----------------------+
- | Username | --os-username | OS_USERNAME |
- | Password | --os-password | OS_PASSWORD |
- | Tenant | --os-tenant-name | OS_TENANT_NAME |
- +----------+------------------+----------------------+
-
-
-Runtime Arguments
------------------
-
-**-m/--mode**: (Required) Has to be one of 'check', 'create' or 'destroy'. It
-indicates which actions javelin is going to perform.
-
-**-r/--resources**: (Required) The path to a YAML file describing the resources
-used by Javelin.
-
-**-d/--devstack-base**: (Required) The path to the devstack repo used to
-retrieve artefacts (like images) that will be referenced in the resource files.
-
-**-c/--config-file**: (Optional) The path to a valid Tempest config file
-describing your cloud. Javelin may use this to determine if certain services
-are enabled and modify its behavior accordingly.
-
-
-Resource file
--------------
-
-The resource file is a valid YAML file describing the resources that will be
-created, checked and destroyed by javelin. Here's a canonical example of a
-resource file::
-
- tenants:
- - javelin
- - discuss
-
- users:
- - name: javelin
- pass: gungnir
- tenant: javelin
- - name: javelin2
- pass: gungnir2
- tenant: discuss
-
- # resources that we want to create
- images:
- - name: javelin_cirros
- owner: javelin
- file: cirros-0.3.2-x86_64-blank.img
- disk_format: ami
- container_format: ami
- aki: cirros-0.3.2-x86_64-vmlinuz
- ari: cirros-0.3.2-x86_64-initrd
-
- servers:
- - name: peltast
- owner: javelin
- flavor: m1.small
- image: javelin_cirros
- floating_ip_pool: public
- - name: hoplite
- owner: javelin
- flavor: m1.medium
- image: javelin_cirros
-
-
-An important piece of the resource definition is the *owner* field, which is
-the user (that we've created) that is the owner of that resource. All
-operations on that resource will happen as that regular user to ensure that
-admin level access does not mask issues.
-
-The check phase will act like a unit test, using well known assert methods to
-verify that the correct resources exist.
-
-"""
-
-import argparse
-import collections
-import datetime
-import os
-import sys
-import unittest
-
-import netaddr
-from oslo_log import log as logging
-from oslo_utils import timeutils
-import six
-from tempest_lib import auth
-from tempest_lib import exceptions as lib_exc
-from tempest_lib.services.compute import flavors_client
-from tempest_lib.services.compute import floating_ips_client
-from tempest_lib.services.compute import security_group_rules_client
-from tempest_lib.services.compute import security_groups_client
-from tempest_lib.services.compute import servers_client
-import yaml
-
-from tempest.common import identity
-from tempest.common import waiters
-from tempest import config
-from tempest.services.identity.v2.json import identity_client
-from tempest.services.identity.v2.json import roles_client
-from tempest.services.identity.v2.json import tenants_client
-from tempest.services.image.v2.json import images_client
-from tempest.services.network.json import network_client
-from tempest.services.network.json import subnets_client
-from tempest.services.object_storage import container_client
-from tempest.services.object_storage import object_client
-from tempest.services.telemetry.json import alarming_client
-from tempest.services.telemetry.json import telemetry_client
-from tempest.services.volume.v1.json import volumes_client
-
-CONF = config.CONF
-OPTS = {}
-USERS = {}
-RES = collections.defaultdict(list)
-
-LOG = None
-
-JAVELIN_START = datetime.datetime.utcnow()
-
-
-class OSClient(object):
- _creds = None
- identity = None
- servers = None
-
- def __init__(self, user, pw, tenant):
- default_params = {
- 'disable_ssl_certificate_validation':
- CONF.identity.disable_ssl_certificate_validation,
- 'ca_certs': CONF.identity.ca_certificates_file,
- 'trace_requests': CONF.debug.trace_requests
- }
- default_params_with_timeout_values = {
- 'build_interval': CONF.compute.build_interval,
- 'build_timeout': CONF.compute.build_timeout
- }
- default_params_with_timeout_values.update(default_params)
-
- compute_params = {
- 'service': CONF.compute.catalog_type,
- 'region': CONF.compute.region or CONF.identity.region,
- 'endpoint_type': CONF.compute.endpoint_type,
- 'build_interval': CONF.compute.build_interval,
- 'build_timeout': CONF.compute.build_timeout
- }
- compute_params.update(default_params)
-
- object_storage_params = {
- 'service': CONF.object_storage.catalog_type,
- 'region': CONF.object_storage.region or CONF.identity.region,
- 'endpoint_type': CONF.object_storage.endpoint_type
- }
- object_storage_params.update(default_params)
-
- _creds = auth.KeystoneV2Credentials(
- username=user,
- password=pw,
- tenant_name=tenant)
- auth_provider_params = {
- 'disable_ssl_certificate_validation':
- CONF.identity.disable_ssl_certificate_validation,
- 'ca_certs': CONF.identity.ca_certificates_file,
- 'trace_requests': CONF.debug.trace_requests
- }
- _auth = auth.KeystoneV2AuthProvider(
- _creds, CONF.identity.uri, **auth_provider_params)
- self.identity = identity_client.IdentityClient(
- _auth,
- CONF.identity.catalog_type,
- CONF.identity.region,
- endpoint_type='adminURL',
- **default_params_with_timeout_values)
- self.tenants = tenants_client.TenantsClient(
- _auth,
- CONF.identity.catalog_type,
- CONF.identity.region,
- endpoint_type='adminURL',
- **default_params_with_timeout_values)
- self.roles = roles_client.RolesClient(
- _auth,
- CONF.identity.catalog_type,
- CONF.identity.region,
- endpoint_type='adminURL',
- **default_params_with_timeout_values)
- self.servers = servers_client.ServersClient(_auth,
- **compute_params)
- self.flavors = flavors_client.FlavorsClient(_auth,
- **compute_params)
- self.floating_ips = floating_ips_client.FloatingIPsClient(
- _auth, **compute_params)
- self.secgroups = security_groups_client.SecurityGroupsClient(
- _auth, **compute_params)
- self.secrules = security_group_rules_client.SecurityGroupRulesClient(
- _auth, **compute_params)
- self.objects = object_client.ObjectClient(_auth,
- **object_storage_params)
- self.containers = container_client.ContainerClient(
- _auth, **object_storage_params)
- self.images = images_client.ImagesClientV2(
- _auth,
- CONF.image.catalog_type,
- CONF.image.region or CONF.identity.region,
- endpoint_type=CONF.image.endpoint_type,
- build_interval=CONF.image.build_interval,
- build_timeout=CONF.image.build_timeout,
- **default_params)
- self.telemetry = telemetry_client.TelemetryClient(
- _auth,
- CONF.telemetry.catalog_type,
- CONF.identity.region,
- endpoint_type=CONF.telemetry.endpoint_type,
- **default_params_with_timeout_values)
- self.alarming = alarming_client.AlarmingClient(
- _auth,
- CONF.alarm.catalog_type,
- CONF.identity.region,
- endpoint_type=CONF.alarm.endpoint_type,
- **default_params_with_timeout_values)
- self.volumes = volumes_client.VolumesClient(
- _auth,
- CONF.volume.catalog_type,
- CONF.volume.region or CONF.identity.region,
- endpoint_type=CONF.volume.endpoint_type,
- build_interval=CONF.volume.build_interval,
- build_timeout=CONF.volume.build_timeout,
- **default_params)
- self.networks = network_client.NetworkClient(
- _auth,
- CONF.network.catalog_type,
- CONF.network.region or CONF.identity.region,
- endpoint_type=CONF.network.endpoint_type,
- build_interval=CONF.network.build_interval,
- build_timeout=CONF.network.build_timeout,
- **default_params)
- self.subnets = subnets_client.SubnetsClient(
- _auth,
- CONF.network.catalog_type,
- CONF.network.region or CONF.identity.region,
- endpoint_type=CONF.network.endpoint_type,
- build_interval=CONF.network.build_interval,
- build_timeout=CONF.network.build_timeout,
- **default_params)
-
-
-def load_resources(fname):
- """Load the expected resources from a yaml file."""
- return yaml.load(open(fname, 'r'))
-
-
-def keystone_admin():
- return OSClient(OPTS.os_username, OPTS.os_password, OPTS.os_tenant_name)
-
-
-def client_for_user(name):
- LOG.debug("Entering client_for_user")
- if name in USERS:
- user = USERS[name]
- LOG.debug("Created client for user %s" % user)
- return OSClient(user['name'], user['pass'], user['tenant'])
- else:
- LOG.error("%s not found in USERS: %s" % (name, USERS))
-
-
-###################
-#
-# TENANTS
-#
-###################
-
-
-def create_tenants(tenants):
- """Create tenants from resource definition.
-
- Don't create the tenants if they already exist.
- """
- admin = keystone_admin()
- body = admin.tenants.list_tenants()['tenants']
- existing = [x['name'] for x in body]
- for tenant in tenants:
- if tenant not in existing:
- admin.tenants.create_tenant(tenant)['tenant']
- else:
- LOG.warning("Tenant '%s' already exists in this environment"
- % tenant)
-
-
-def destroy_tenants(tenants):
- admin = keystone_admin()
- for tenant in tenants:
- tenant_id = identity.get_tenant_by_name(admin.tenant, tenant)['id']
- admin.tenants.delete_tenant(tenant_id)
-
-##############
-#
-# USERS
-#
-##############
-
-
-def _users_for_tenant(users, tenant):
- u_for_t = []
- for user in users:
- for n in user:
- if user[n]['tenant'] == tenant:
- u_for_t.append(user[n])
- return u_for_t
-
-
-def _tenants_from_users(users):
- tenants = set()
- for user in users:
- for n in user:
- tenants.add(user[n]['tenant'])
- return tenants
-
-
-def _assign_swift_role(user, swift_role):
- admin = keystone_admin()
- roles = admin.roles.list_roles()
- role = next(r for r in roles if r['name'] == swift_role)
- LOG.debug(USERS[user])
- try:
- admin.roles.assign_user_role(
- USERS[user]['tenant_id'],
- USERS[user]['id'],
- role['id'])
- except lib_exc.Conflict:
- # don't care if it's already assigned
- pass
-
-
-def create_users(users):
- """Create tenants from resource definition.
-
- Don't create the tenants if they already exist.
- """
- global USERS
- LOG.info("Creating users")
- admin = keystone_admin()
- for u in users:
- try:
- tenant = identity.get_tenant_by_name(admin.tenants, u['tenant'])
- except lib_exc.NotFound:
- LOG.error("Tenant: %s - not found" % u['tenant'])
- continue
- try:
- identity.get_user_by_username(admin.identity,
- tenant['id'], u['name'])
- LOG.warning("User '%s' already exists in this environment"
- % u['name'])
- except lib_exc.NotFound:
- admin.identity.create_user(
- u['name'], u['pass'], tenant['id'],
- "%s@%s" % (u['name'], tenant['id']),
- enabled=True)
-
-
-def destroy_users(users):
- admin = keystone_admin()
- for user in users:
- tenant_id = identity.get_tenant_by_name(admin.tenants,
- user['tenant'])['id']
- user_id = identity.get_user_by_username(admin.identity,
- tenant_id, user['name'])['id']
- admin.identity.delete_user(user_id)
-
-
-def collect_users(users):
- global USERS
- LOG.info("Collecting users")
- admin = keystone_admin()
- for u in users:
- tenant = identity.get_tenant_by_name(admin.tenants, u['tenant'])
- u['tenant_id'] = tenant['id']
- USERS[u['name']] = u
- body = identity.get_user_by_username(admin.identity,
- tenant['id'], u['name'])
- USERS[u['name']]['id'] = body['id']
-
-
-class JavelinCheck(unittest.TestCase):
- def __init__(self, users, resources):
- super(JavelinCheck, self).__init__()
- self.users = users
- self.res = resources
-
- def runTest(self, *args):
- pass
-
- def _ping_ip(self, ip_addr, count, namespace=None):
- if namespace is None:
- ping_cmd = "ping -c1 " + ip_addr
- else:
- ping_cmd = "sudo ip netns exec %s ping -c1 %s" % (namespace,
- ip_addr)
- for current in range(count):
- return_code = os.system(ping_cmd)
- if return_code is 0:
- break
- self.assertNotEqual(current, count - 1,
- "Server is not pingable at %s" % ip_addr)
-
- def check(self):
- self.check_users()
- self.check_objects()
- self.check_servers()
- self.check_volumes()
- self.check_telemetry()
- self.check_secgroups()
-
- # validate neutron is enabled and ironic disabled:
- # Tenant network isolation is not supported when using ironic.
- # "admin" has set up a neutron flat network environment within a shared
- # fixed network for all tenants to use.
- # In this case, network/subnet/router creation can be skipped and the
- # server booted the same as nova network.
- if (CONF.service_available.neutron and
- not CONF.baremetal.driver_enabled):
- self.check_networking()
-
- def check_users(self):
- """Check that the users we expect to exist, do.
-
- We don't use the resource list for this because we need to validate
- that things like tenantId didn't drift across versions.
- """
- LOG.info("checking users")
- for name, user in six.iteritems(self.users):
- client = keystone_admin()
- found = client.identity.show_user(user['id'])['user']
- self.assertEqual(found['name'], user['name'])
- self.assertEqual(found['tenantId'], user['tenant_id'])
-
- # also ensure we can auth with that user, and do something
- # on the cloud. We don't care about the results except that it
- # remains authorized.
- client = client_for_user(user['name'])
- client.servers.list_servers()
-
- def check_objects(self):
- """Check that the objects created are still there."""
- if not self.res.get('objects'):
- return
- LOG.info("checking objects")
- for obj in self.res['objects']:
- client = client_for_user(obj['owner'])
- r, contents = client.objects.get_object(
- obj['container'], obj['name'])
- source = _file_contents(obj['file'])
- self.assertEqual(contents, source)
-
- def check_servers(self):
- """Check that the servers are still up and running."""
- if not self.res.get('servers'):
- return
- LOG.info("checking servers")
- for server in self.res['servers']:
- client = client_for_user(server['owner'])
- found = _get_server_by_name(client, server['name'])
- self.assertIsNotNone(
- found,
- "Couldn't find expected server %s" % server['name'])
-
- found = client.servers.show_server(found['id'])['server']
- # validate neutron is enabled and ironic disabled:
- if (CONF.service_available.neutron and
- not CONF.baremetal.driver_enabled):
- _floating_is_alive = False
- for network_name, body in found['addresses'].items():
- for addr in body:
- ip = addr['addr']
- # Use floating IP, fixed IP or other type to
- # reach the server.
- # This is useful in multi-node environment.
- if CONF.validation.connect_method == 'floating':
- if addr.get('OS-EXT-IPS:type',
- 'floating') == 'floating':
- self._ping_ip(ip, 60)
- _floating_is_alive = True
- elif CONF.validation.connect_method == 'fixed':
- if addr.get('OS-EXT-IPS:type',
- 'fixed') == 'fixed':
- namespace = _get_router_namespace(client,
- network_name)
- self._ping_ip(ip, 60, namespace)
- else:
- self._ping_ip(ip, 60)
- # If CONF.validation.connect_method is floating, validate
- # that the floating IP is attached to the server and the
- # the server is pingable.
- if CONF.validation.connect_method == 'floating':
- self.assertTrue(_floating_is_alive,
- "Server %s has no floating IP." %
- server['name'])
- else:
- addr = found['addresses']['private'][0]['addr']
- self._ping_ip(addr, 60)
-
- def check_secgroups(self):
- """Check that the security groups still exist."""
- LOG.info("Checking security groups")
- for secgroup in self.res['secgroups']:
- client = client_for_user(secgroup['owner'])
- found = _get_resource_by_name(client.secgroups, 'security_groups',
- secgroup['name'])
- self.assertIsNotNone(
- found,
- "Couldn't find expected secgroup %s" % secgroup['name'])
-
- def check_telemetry(self):
- """Check that ceilometer provides a sane sample.
-
- Confirm that there is more than one sample and that they have the
- expected metadata.
-
- If in check mode confirm that the oldest sample available is from
- before the upgrade.
- """
- if not self.res.get('telemetry'):
- return
- LOG.info("checking telemetry")
- for server in self.res['servers']:
- client = client_for_user(server['owner'])
- body = client.telemetry.list_samples(
- 'instance',
- query=('metadata.display_name', 'eq', server['name'])
- )
- self.assertTrue(len(body) >= 1, 'expecting at least one sample')
- self._confirm_telemetry_sample(server, body[-1])
-
- def check_volumes(self):
- """Check that the volumes are still there and attached."""
- if not self.res.get('volumes'):
- return
- LOG.info("checking volumes")
- for volume in self.res['volumes']:
- client = client_for_user(volume['owner'])
- vol_body = _get_volume_by_name(client, volume['name'])
- self.assertIsNotNone(
- vol_body,
- "Couldn't find expected volume %s" % volume['name'])
-
- # Verify that a volume's attachment retrieved
- server_id = _get_server_by_name(client, volume['server'])['id']
- attachment = client.volumes.get_attachment_from_volume(vol_body)
- self.assertEqual(vol_body['id'], attachment['volume_id'])
- self.assertEqual(server_id, attachment['server_id'])
-
- def _confirm_telemetry_sample(self, server, sample):
- """Check this sample matches the expected resource metadata."""
- # Confirm display_name
- self.assertEqual(server['name'],
- sample['resource_metadata']['display_name'])
- # Confirm instance_type of flavor
- flavor = sample['resource_metadata'].get(
- 'flavor.name',
- sample['resource_metadata'].get('instance_type')
- )
- self.assertEqual(server['flavor'], flavor)
- # Confirm the oldest sample was created before upgrade.
- if OPTS.mode == 'check':
- oldest_timestamp = timeutils.normalize_time(
- timeutils.parse_isotime(sample['timestamp']))
- self.assertTrue(
- oldest_timestamp < JAVELIN_START,
- 'timestamp should come before start of second javelin run'
- )
-
- def check_networking(self):
- """Check that the networks are still there."""
- for res_type in ('networks', 'subnets', 'routers'):
- for res in self.res[res_type]:
- client = client_for_user(res['owner'])
- found = _get_resource_by_name(client.networks, res_type,
- res['name'])
- self.assertIsNotNone(
- found,
- "Couldn't find expected resource %s" % res['name'])
-
-
-#######################
-#
-# OBJECTS
-#
-#######################
-
-
-def _file_contents(fname):
- with open(fname, 'r') as f:
- return f.read()
-
-
-def create_objects(objects):
- if not objects:
- return
- LOG.info("Creating objects")
- for obj in objects:
- LOG.debug("Object %s" % obj)
- swift_role = obj.get('swift_role', 'Member')
- _assign_swift_role(obj['owner'], swift_role)
- client = client_for_user(obj['owner'])
- client.containers.create_container(obj['container'])
- client.objects.create_object(
- obj['container'], obj['name'],
- _file_contents(obj['file']))
-
-
-def destroy_objects(objects):
- for obj in objects:
- client = client_for_user(obj['owner'])
- r, body = client.objects.delete_object(obj['container'], obj['name'])
- if not (200 <= int(r['status']) < 299):
- raise ValueError("unable to destroy object: [%s] %s" % (r, body))
-
-
-#######################
-#
-# IMAGES
-#
-#######################
-
-
-def _resolve_image(image, imgtype):
- name = image[imgtype]
- fname = os.path.join(OPTS.devstack_base, image['imgdir'], name)
- return name, fname
-
-
-def _get_image_by_name(client, name):
- body = client.images.list_images()
- for image in body:
- if name == image['name']:
- return image
- return None
-
-
-def create_images(images):
- if not images:
- return
- LOG.info("Creating images")
- for image in images:
- client = client_for_user(image['owner'])
-
- # DEPRECATED: 'format' was used for ami images
- # Use 'disk_format' and 'container_format' instead
- if 'format' in image:
- LOG.warning("Deprecated: 'format' is deprecated for images "
- "description. Please use 'disk_format' and 'container_"
- "format' instead.")
- image['disk_format'] = image['format']
- image['container_format'] = image['format']
-
- # only upload a new image if the name isn't there
- if _get_image_by_name(client, image['name']):
- LOG.info("Image '%s' already exists" % image['name'])
- continue
-
- # special handling for 3 part image
- extras = {}
- if image['disk_format'] == 'ami':
- name, fname = _resolve_image(image, 'aki')
- aki = client.images.create_image(
- 'javelin_' + name, 'aki', 'aki')
- client.images.store_image_file(aki.get('id'), open(fname, 'r'))
- extras['kernel_id'] = aki.get('id')
-
- name, fname = _resolve_image(image, 'ari')
- ari = client.images.create_image(
- 'javelin_' + name, 'ari', 'ari')
- client.images.store_image_file(ari.get('id'), open(fname, 'r'))
- extras['ramdisk_id'] = ari.get('id')
-
- _, fname = _resolve_image(image, 'file')
- body = client.images.create_image(
- image['name'], image['container_format'],
- image['disk_format'], **extras)
- image_id = body.get('id')
- client.images.store_image_file(image_id, open(fname, 'r'))
-
-
-def destroy_images(images):
- if not images:
- return
- LOG.info("Destroying images")
- for image in images:
- client = client_for_user(image['owner'])
-
- response = _get_image_by_name(client, image['name'])
- if not response:
- LOG.info("Image '%s' does not exist" % image['name'])
- continue
- client.images.delete_image(response['id'])
-
-
-#######################
-#
-# NETWORKS
-#
-#######################
-
-def _get_router_namespace(client, network):
- network_id = _get_resource_by_name(client.networks,
- 'networks', network)['id']
- n_body = client.networks.list_routers()
- for router in n_body['routers']:
- router_id = router['id']
- r_body = client.networks.list_router_interfaces(router_id)
- for port in r_body['ports']:
- if port['network_id'] == network_id:
- return "qrouter-%s" % router_id
-
-
-def _get_resource_by_name(client, resource, name):
- get_resources = getattr(client, 'list_%s' % resource)
- if get_resources is None:
- raise AttributeError("client doesn't have method list_%s" % resource)
- # Until all tempest client methods are changed to return only one value,
- # we cannot assume they all have the same signature so we need to discard
- # the unused response first value it two values are being returned.
- body = get_resources()
- if type(body) == tuple:
- body = body[1]
- if isinstance(body, dict):
- body = body[resource]
- for res in body:
- if name == res['name']:
- return res
- raise ValueError('%s not found in %s resources' % (name, resource))
-
-
-def create_networks(networks):
- LOG.info("Creating networks")
- for network in networks:
- client = client_for_user(network['owner'])
-
- # only create a network if the name isn't here
- body = client.networks.list_networks()
- if any(item['name'] == network['name'] for item in body['networks']):
- LOG.warning("Duplicated network name: %s" % network['name'])
- continue
-
- client.networks.create_network(name=network['name'])
-
-
-def destroy_networks(networks):
- LOG.info("Destroying subnets")
- for network in networks:
- client = client_for_user(network['owner'])
- network_id = _get_resource_by_name(client.networks, 'networks',
- network['name'])['id']
- client.networks.delete_network(network_id)
-
-
-def create_subnets(subnets):
- LOG.info("Creating subnets")
- for subnet in subnets:
- client = client_for_user(subnet['owner'])
-
- network = _get_resource_by_name(client.networks, 'networks',
- subnet['network'])
- ip_version = netaddr.IPNetwork(subnet['range']).version
- # ensure we don't overlap with another subnet in the network
- try:
- client.networks.create_subnet(network_id=network['id'],
- cidr=subnet['range'],
- name=subnet['name'],
- ip_version=ip_version)
- except lib_exc.BadRequest as e:
- is_overlapping_cidr = 'overlaps with another subnet' in str(e)
- if not is_overlapping_cidr:
- raise
-
-
-def destroy_subnets(subnets):
- LOG.info("Destroying subnets")
- for subnet in subnets:
- client = client_for_user(subnet['owner'])
- subnet_id = _get_resource_by_name(client.subnets,
- 'subnets', subnet['name'])['id']
- client.subnets.delete_subnet(subnet_id)
-
-
-def create_routers(routers):
- LOG.info("Creating routers")
- for router in routers:
- client = client_for_user(router['owner'])
-
- # only create a router if the name isn't here
- body = client.networks.list_routers()
- if any(item['name'] == router['name'] for item in body['routers']):
- LOG.warning("Duplicated router name: %s" % router['name'])
- continue
-
- client.networks.create_router(router['name'])
-
-
-def destroy_routers(routers):
- LOG.info("Destroying routers")
- for router in routers:
- client = client_for_user(router['owner'])
- router_id = _get_resource_by_name(client.networks,
- 'routers', router['name'])['id']
- for subnet in router['subnet']:
- subnet_id = _get_resource_by_name(client.networks,
- 'subnets', subnet)['id']
- client.networks.remove_router_interface_with_subnet_id(router_id,
- subnet_id)
- client.networks.delete_router(router_id)
-
-
-def add_router_interface(routers):
- for router in routers:
- client = client_for_user(router['owner'])
- router_id = _get_resource_by_name(client.networks,
- 'routers', router['name'])['id']
-
- for subnet in router['subnet']:
- subnet_id = _get_resource_by_name(client.networks,
- 'subnets', subnet)['id']
- # connect routers to their subnets
- client.networks.add_router_interface_with_subnet_id(router_id,
- subnet_id)
- # connect routers to external network if set to "gateway"
- if router['gateway']:
- if CONF.network.public_network_id:
- ext_net = CONF.network.public_network_id
- client.networks._update_router(
- router_id, set_enable_snat=True,
- external_gateway_info={"network_id": ext_net})
- else:
- raise ValueError('public_network_id is not configured.')
-
-
-#######################
-#
-# SERVERS
-#
-#######################
-
-def _get_server_by_name(client, name):
- body = client.servers.list_servers()
- for server in body['servers']:
- if name == server['name']:
- return server
- return None
-
-
-def _get_flavor_by_name(client, name):
- body = client.flavors.list_flavors()['flavors']
- for flavor in body:
- if name == flavor['name']:
- return flavor
- return None
-
-
-def create_servers(servers):
- if not servers:
- return
- LOG.info("Creating servers")
- for server in servers:
- client = client_for_user(server['owner'])
-
- if _get_server_by_name(client, server['name']):
- LOG.info("Server '%s' already exists" % server['name'])
- continue
-
- image_id = _get_image_by_name(client, server['image'])['id']
- flavor_id = _get_flavor_by_name(client, server['flavor'])['id']
- # validate neutron is enabled and ironic disabled
- kwargs = dict()
- if (CONF.service_available.neutron and
- not CONF.baremetal.driver_enabled and server.get('networks')):
- get_net_id = lambda x: (_get_resource_by_name(
- client.networks, 'networks', x)['id'])
- kwargs['networks'] = [{'uuid': get_net_id(network)}
- for network in server['networks']]
- body = client.servers.create_server(
- name=server['name'], imageRef=image_id, flavorRef=flavor_id,
- **kwargs)['server']
- server_id = body['id']
- client.servers.wait_for_server_status(server_id, 'ACTIVE')
- # create security group(s) after server spawning
- for secgroup in server['secgroups']:
- client.servers.add_security_group(server_id, name=secgroup)
- if CONF.validation.connect_method == 'floating':
- floating_ip_pool = server.get('floating_ip_pool')
- floating_ip = client.floating_ips.create_floating_ip(
- pool_name=floating_ip_pool)['floating_ip']
- client.floating_ips.associate_floating_ip_to_server(
- floating_ip['ip'], server_id)
-
-
-def destroy_servers(servers):
- if not servers:
- return
- LOG.info("Destroying servers")
- for server in servers:
- client = client_for_user(server['owner'])
-
- response = _get_server_by_name(client, server['name'])
- if not response:
- LOG.info("Server '%s' does not exist" % server['name'])
- continue
-
- # TODO(EmilienM): disassociate floating IP from server and release it.
- client.servers.delete_server(response['id'])
- waiters.wait_for_server_termination(client.servers, response['id'],
- ignore_error=True)
-
-
-def create_secgroups(secgroups):
- LOG.info("Creating security groups")
- for secgroup in secgroups:
- client = client_for_user(secgroup['owner'])
-
- # only create a security group if the name isn't here
- # i.e. a security group may be used by another server
- # only create a router if the name isn't here
- body = client.secgroups.list_security_groups()['security_groups']
- if any(item['name'] == secgroup['name'] for item in body):
- LOG.warning("Security group '%s' already exists" %
- secgroup['name'])
- continue
-
- body = client.secgroups.create_security_group(
- name=secgroup['name'],
- description=secgroup['description'])['security_group']
- secgroup_id = body['id']
- # for each security group, create the rules
- for rule in secgroup['rules']:
- ip_proto, from_port, to_port, cidr = rule.split()
- client.secrules.create_security_group_rule(
- parent_group_id=secgroup_id, ip_protocol=ip_proto,
- from_port=from_port, to_port=to_port, cidr=cidr)
-
-
-def destroy_secgroups(secgroups):
- LOG.info("Destroying security groups")
- for secgroup in secgroups:
- client = client_for_user(secgroup['owner'])
- sg_id = _get_resource_by_name(client.secgroups,
- 'security_groups',
- secgroup['name'])
- # sg rules are deleted automatically
- client.secgroups.delete_security_group(sg_id['id'])
-
-
-#######################
-#
-# VOLUMES
-#
-#######################
-
-def _get_volume_by_name(client, name):
- body = client.volumes.list_volumes()['volumes']
- for volume in body:
- if name == volume['display_name']:
- return volume
- return None
-
-
-def create_volumes(volumes):
- if not volumes:
- return
- LOG.info("Creating volumes")
- for volume in volumes:
- client = client_for_user(volume['owner'])
-
- # only create a volume if the name isn't here
- if _get_volume_by_name(client, volume['name']):
- LOG.info("volume '%s' already exists" % volume['name'])
- continue
-
- size = volume['gb']
- v_name = volume['name']
- body = client.volumes.create_volume(size=size,
- display_name=v_name)['volume']
- client.volumes.wait_for_volume_status(body['id'], 'available')
-
-
-def destroy_volumes(volumes):
- for volume in volumes:
- client = client_for_user(volume['owner'])
- volume_id = _get_volume_by_name(client, volume['name'])['id']
- client.volumes.detach_volume(volume_id)
- client.volumes.delete_volume(volume_id)
-
-
-def attach_volumes(volumes):
- for volume in volumes:
- client = client_for_user(volume['owner'])
- server_id = _get_server_by_name(client, volume['server'])['id']
- volume_id = _get_volume_by_name(client, volume['name'])['id']
- device = volume['device']
- client.volumes.attach_volume(volume_id, server_id, device)
-
-
-#######################
-#
-# MAIN LOGIC
-#
-#######################
-
-def create_resources():
- LOG.info("Creating Resources")
- # first create keystone level resources, and we need to be admin
- # for this.
- create_tenants(RES['tenants'])
- create_users(RES['users'])
- collect_users(RES['users'])
-
- # next create resources in a well known order
- create_objects(RES['objects'])
- create_images(RES['images'])
-
- # validate neutron is enabled and ironic is disabled
- if CONF.service_available.neutron and not CONF.baremetal.driver_enabled:
- create_networks(RES['networks'])
- create_subnets(RES['subnets'])
- create_routers(RES['routers'])
- add_router_interface(RES['routers'])
-
- create_secgroups(RES['secgroups'])
- create_volumes(RES['volumes'])
-
- # Only attempt attaching the volumes if servers are defined in the
- # resource file
- if 'servers' in RES:
- create_servers(RES['servers'])
- attach_volumes(RES['volumes'])
-
-
-def destroy_resources():
- LOG.info("Destroying Resources")
- # Destroy in inverse order of create
- destroy_servers(RES['servers'])
- destroy_images(RES['images'])
- destroy_objects(RES['objects'])
- destroy_volumes(RES['volumes'])
- if CONF.service_available.neutron and not CONF.baremetal.driver_enabled:
- destroy_routers(RES['routers'])
- destroy_subnets(RES['subnets'])
- destroy_networks(RES['networks'])
- destroy_secgroups(RES['secgroups'])
- destroy_users(RES['users'])
- destroy_tenants(RES['tenants'])
- LOG.warning("Destroy mode incomplete")
-
-
-def get_options():
- global OPTS
- parser = argparse.ArgumentParser(
- description='Create and validate a fixed set of OpenStack resources')
- parser.add_argument('-m', '--mode',
- metavar='<create|check|destroy>',
- required=True,
- help=('One of (create, check, destroy)'))
- parser.add_argument('-r', '--resources',
- required=True,
- metavar='resourcefile.yaml',
- help='Resources definition yaml file')
-
- parser.add_argument(
- '-d', '--devstack-base',
- required=True,
- metavar='/opt/stack/old',
- help='Devstack base directory for retrieving artifacts')
- parser.add_argument(
- '-c', '--config-file',
- metavar='/etc/tempest.conf',
- help='path to javelin2(tempest) config file')
-
- # auth bits, letting us also just source the devstack openrc
- parser.add_argument('--os-username',
- metavar='<auth-user-name>',
- default=os.environ.get('OS_USERNAME'),
- help=('Defaults to env[OS_USERNAME].'))
- parser.add_argument('--os-password',
- metavar='<auth-password>',
- default=os.environ.get('OS_PASSWORD'),
- help=('Defaults to env[OS_PASSWORD].'))
- parser.add_argument('--os-tenant-name',
- metavar='<auth-tenant-name>',
- default=os.environ.get('OS_TENANT_NAME'),
- help=('Defaults to env[OS_TENANT_NAME].'))
-
- OPTS = parser.parse_args()
- if OPTS.mode not in ('create', 'check', 'destroy'):
- print("ERROR: Unknown mode -m %s\n" % OPTS.mode)
- parser.print_help()
- sys.exit(1)
- if OPTS.config_file:
- config.CONF.set_config_path(OPTS.config_file)
-
-
-def setup_logging():
- global LOG
- logging.setup(CONF, __name__)
- LOG = logging.getLogger(__name__)
-
-
-def main():
- global RES
- get_options()
- setup_logging()
- RES.update(load_resources(OPTS.resources))
-
- if OPTS.mode == 'create':
- create_resources()
- # Make sure the resources we just created actually work
- checker = JavelinCheck(USERS, RES)
- checker.check()
- elif OPTS.mode == 'check':
- collect_users(RES['users'])
- checker = JavelinCheck(USERS, RES)
- checker.check()
- elif OPTS.mode == 'destroy':
- collect_users(RES['users'])
- destroy_resources()
- else:
- LOG.error('Unknown mode %s' % OPTS.mode)
- return 1
- LOG.info('javelin2 successfully finished')
- return 0
-
-if __name__ == "__main__":
- sys.exit(main())
diff --git a/tempest/cmd/list_plugins.py b/tempest/cmd/list_plugins.py
new file mode 100644
index 0000000..86732da
--- /dev/null
+++ b/tempest/cmd/list_plugins.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python
+
+# 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.
+
+"""
+Utility for listing all currently installed Tempest plugins.
+
+**Usage:** ``tempest list-plugins``.
+"""
+
+from cliff import command
+import prettytable
+
+from tempest.test_discover import plugins as plg
+
+
+class TempestListPlugins(command.Command):
+ def take_action(self, parsed_args):
+ self._list_plugins()
+
+ def get_description(self):
+ return 'List all tempest plugins'
+
+ def _list_plugins(self):
+ plugins = plg.TempestTestPluginManager()
+
+ output = prettytable.PrettyTable(["Name", "EntryPoint"])
+ for plugin in plugins.ext_plugins.extensions:
+ output.add_row([
+ plugin.name, plugin.entry_point_target])
+
+ print(output)
diff --git a/tempest/cmd/main.py b/tempest/cmd/main.py
index 577df9b..1090c41 100644
--- a/tempest/cmd/main.py
+++ b/tempest/cmd/main.py
@@ -11,11 +11,11 @@
# License for the specific language governing permissions and limitations
# under the License.
-import logging
import sys
from cliff import app
from cliff import commandmanager
+from oslo_log import log as logging
from pbr import version
@@ -26,8 +26,9 @@
def __init__(self):
super(Main, self).__init__(
description='Tempest cli application',
- version=version.VersionInfo('tempest').version_string(),
+ version=version.VersionInfo('tempest').version_string_with_vcs(),
command_manager=commandmanager.CommandManager('tempest.cm'),
+ deferred_help=True,
)
def initialize_app(self, argv):
diff --git a/tempest/cmd/resources.yaml b/tempest/cmd/resources.yaml
deleted file mode 100644
index 2d6664c..0000000
--- a/tempest/cmd/resources.yaml
+++ /dev/null
@@ -1,96 +0,0 @@
-# This is a yaml description for the most basic definitions
-# of what should exist across the resource boundary. Perhaps
-# one day this will grow into a Heat resource template, but as
-# Heat isn't a known working element in the upgrades, we do
-# this much simpler thing for now.
-
-tenants:
- - javelin
- - discuss
-
-users:
- - name: javelin
- pass: gungnir
- tenant: javelin
- - name: javelin2
- pass: gungnir2
- tenant: discuss
-
-secgroups:
- - name: angon
- owner: javelin
- description: angon
- rules:
- - 'icmp -1 -1 0.0.0.0/0'
- - 'tcp 22 22 0.0.0.0/0'
- - name: baobab
- owner: javelin
- description: baobab
- rules:
- - 'tcp 80 80 0.0.0.0/0'
-
-# resources that we want to create
-images:
- - name: javelin_cirros
- owner: javelin
- imgdir: files/images/cirros-0.3.2-x86_64-uec
- file: cirros-0.3.2-x86_64-blank.img
- format: ami
- aki: cirros-0.3.2-x86_64-vmlinuz
- ari: cirros-0.3.2-x86_64-initrd
-volumes:
- - name: assegai
- server: peltast
- owner: javelin
- gb: 1
- device: /dev/vdb
- - name: pifpouf
- server: hoplite
- owner: javelin
- gb: 2
- device: /dev/vdb
-networks:
- - name: world1
- owner: javelin
- - name: world2
- owner: javelin
-subnets:
- - name: subnet1
- range: 10.1.0.0/24
- network: world1
- owner: javelin
- - name: subnet2
- range: 192.168.1.0/24
- network: world2
- owner: javelin
-routers:
- - name: connector
- owner: javelin
- gateway: true
- subnet:
- - subnet1
- - subnet2
-servers:
- - name: peltast
- owner: javelin
- flavor: m1.small
- image: javelin_cirros
- networks:
- - world1
- secgroups:
- - angon
- - baobab
- - name: hoplite
- owner: javelin
- flavor: m1.medium
- image: javelin_cirros
- networks:
- - world2
- secgroups:
- - angon
-objects:
- - container: jc1
- name: javelin1
- owner: javelin
- file: /etc/hosts
-telemetry: true
diff --git a/tempest/cmd/run.py b/tempest/cmd/run.py
new file mode 100644
index 0000000..a27425c
--- /dev/null
+++ b/tempest/cmd/run.py
@@ -0,0 +1,288 @@
+# 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.
+
+"""
+Runs tempest tests
+
+This command is used for running the tempest tests
+
+Test Selection
+==============
+Tempest run has several options:
+
+ * **--regex/-r**: This is a selection regex like what stestr uses. It will run
+ any tests that match on re.match() with the regex
+ * **--smoke/-s**: Run all the tests tagged as smoke
+ * **--black-regex**: It allows to do simple test exclusion via passing a
+ rejection/black regexp
+
+There are also the ``--blacklist-file`` and ``--whitelist-file`` options that
+let you pass a filepath to tempest run with the file format being a line
+separated regex, with '#' used to signify the start of a comment on a line.
+For example::
+
+ # Regex file
+ ^regex1 # Match these tests
+ .*regex2 # Match those tests
+
+These arguments are just passed into stestr, you can refer to the stestr
+selection docs for more details on how these operate:
+http://stestr.readthedocs.io/en/latest/MANUAL.html#test-selection
+
+You can also use the ``--list-tests`` option in conjunction with selection
+arguments to list which tests will be run.
+
+You can also use the ``--load-list`` option that lets you pass a filepath to
+tempest run with the file format being in a non-regex format, similar to the
+tests generated by the ``--list-tests`` option. You can specify target tests
+by removing unnecessary tests from a list file which is generated from
+``--list-tests`` option.
+
+Test Execution
+==============
+There are several options to control how the tests are executed. By default
+tempest will run in parallel with a worker for each CPU present on the machine.
+If you want to adjust the number of workers use the ``--concurrency`` option
+and if you want to run tests serially use ``--serial/-t``
+
+Running with Workspaces
+-----------------------
+Tempest run enables you to run your tempest tests from any setup tempest
+workspace it relies on you having setup a tempest workspace with either the
+``tempest init`` or ``tempest workspace`` commands. Then using the
+``--workspace`` CLI option you can specify which one of your workspaces you
+want to run tempest from. Using this option you don't have to run Tempest
+directly with you current working directory being the workspace, Tempest will
+take care of managing everything to be executed from there.
+
+Running from Anywhere
+---------------------
+Tempest run provides you with an option to execute tempest from anywhere on
+your system. You are required to provide a config file in this case with the
+``--config-file`` option. When run tempest will create a .stestr
+directory and a .stestr.conf file in your current working directory. This way
+you can use stestr commands directly to inspect the state of the previous run.
+
+Test Output
+===========
+By default tempest run's output to STDOUT will be generated using the
+subunit-trace output filter. But, if you would prefer a subunit v2 stream be
+output to STDOUT use the ``--subunit`` flag
+
+Combining Runs
+==============
+
+There are certain situations in which you want to split a single run of tempest
+across 2 executions of tempest run. (for example to run part of the tests
+serially and others in parallel) To accomplish this but still treat the results
+as a single run you can leverage the ``--combine`` option which will append
+the current run's results with the previous runs.
+"""
+
+import os
+import sys
+
+from cliff import command
+from oslo_serialization import jsonutils as json
+import six
+from stestr import commands
+
+from tempest import clients
+from tempest.cmd import cleanup_service
+from tempest.cmd import init
+from tempest.cmd import workspace
+from tempest.common import credentials_factory as credentials
+from tempest import config
+
+
+CONF = config.CONF
+SAVED_STATE_JSON = "saved_state.json"
+
+
+class TempestRun(command.Command):
+
+ def _set_env(self, config_file=None):
+ if config_file:
+ CONF.set_config_path(os.path.abspath(config_file))
+ # NOTE(mtreinish): This is needed so that stestr doesn't gobble up any
+ # stacktraces on failure.
+ if 'TESTR_PDB' in os.environ:
+ return
+ else:
+ os.environ["TESTR_PDB"] = ""
+ # NOTE(dims): most of our .stestr.conf try to test for PYTHON
+ # environment variable and fall back to "python", under python3
+ # if it does not exist. we should set it to the python3 executable
+ # to deal with this situation better for now.
+ if six.PY3 and 'PYTHON' not in os.environ:
+ os.environ['PYTHON'] = sys.executable
+
+ def _create_stestr_conf(self):
+ top_level_path = os.path.dirname(os.path.dirname(__file__))
+ discover_path = os.path.join(top_level_path, 'test_discover')
+ file_contents = init.STESTR_CONF % (discover_path, top_level_path)
+ with open('.stestr.conf', 'w+') as stestr_conf_file:
+ stestr_conf_file.write(file_contents)
+
+ def take_action(self, parsed_args):
+ if parsed_args.config_file:
+ self._set_env(parsed_args.config_file)
+ else:
+ self._set_env()
+ # Workspace execution mode
+ if parsed_args.workspace:
+ workspace_mgr = workspace.WorkspaceManager(
+ parsed_args.workspace_path)
+ path = workspace_mgr.get_workspace(parsed_args.workspace)
+ if not path:
+ sys.exit(
+ "The %r workspace isn't registered in "
+ "%r. Use 'tempest init' to "
+ "register the workspace." %
+ (parsed_args.workspace, workspace_mgr.path))
+ os.chdir(path)
+ if not os.path.isfile('.stestr.conf'):
+ self._create_stestr_conf()
+ # local execution with config file mode
+ elif parsed_args.config_file and not os.path.isfile('.stestr.conf'):
+ self._create_stestr_conf()
+ elif not os.path.isfile('.stestr.conf'):
+ print("No .stestr.conf file was found for local execution")
+ sys.exit(2)
+ if parsed_args.state:
+ self._init_state()
+ else:
+ pass
+
+ regex = self._build_regex(parsed_args)
+ return_code = 0
+ if parsed_args.list_tests:
+ return_code = commands.list_command(
+ filters=regex, whitelist_file=parsed_args.whitelist_file,
+ blacklist_file=parsed_args.blacklist_file,
+ black_regex=parsed_args.black_regex)
+
+ else:
+ serial = not parsed_args.parallel
+ return_code = commands.run_command(
+ filters=regex, subunit_out=parsed_args.subunit,
+ serial=serial, concurrency=parsed_args.concurrency,
+ blacklist_file=parsed_args.blacklist_file,
+ whitelist_file=parsed_args.whitelist_file,
+ black_regex=parsed_args.black_regex,
+ load_list=parsed_args.load_list, combine=parsed_args.combine)
+ if return_code > 0:
+ sys.exit(return_code)
+ return return_code
+
+ def get_description(self):
+ return 'Run tempest'
+
+ def _init_state(self):
+ print("Initializing saved state.")
+ data = {}
+ self.global_services = cleanup_service.get_global_cleanup_services()
+ self.admin_mgr = clients.Manager(
+ credentials.get_configured_admin_credentials())
+ admin_mgr = self.admin_mgr
+ kwargs = {'data': data,
+ 'is_dry_run': False,
+ 'saved_state_json': data,
+ 'is_preserve': False,
+ 'is_save_state': True}
+ for service in self.global_services:
+ svc = service(admin_mgr, **kwargs)
+ svc.run()
+
+ with open(SAVED_STATE_JSON, 'w+') as f:
+ f.write(json.dumps(data,
+ sort_keys=True, indent=2, separators=(',', ': ')))
+
+ def get_parser(self, prog_name):
+ parser = super(TempestRun, self).get_parser(prog_name)
+ parser = self._add_args(parser)
+ return parser
+
+ def _add_args(self, parser):
+ # workspace args
+ parser.add_argument('--workspace', default=None,
+ help='Name of tempest workspace to use for running'
+ ' tests. You can see a list of workspaces '
+ 'with tempest workspace list')
+ parser.add_argument('--workspace-path', default=None,
+ dest='workspace_path',
+ help="The path to the workspace file, the default "
+ "is ~/.tempest/workspace.yaml")
+ # Configuration flags
+ parser.add_argument('--config-file', default=None, dest='config_file',
+ help='Configuration file to run tempest with')
+ # test selection args
+ regex = parser.add_mutually_exclusive_group()
+ regex.add_argument('--smoke', '-s', action='store_true',
+ help="Run the smoke tests only")
+ regex.add_argument('--regex', '-r', default='',
+ help='A normal stestr selection regex used to '
+ 'specify a subset of tests to run')
+ parser.add_argument('--black-regex', dest='black_regex',
+ help='A regex to exclude tests that match it')
+ parser.add_argument('--whitelist-file', '--whitelist_file',
+ help="Path to a whitelist file, this file "
+ "contains a separate regex on each "
+ "newline.")
+ parser.add_argument('--blacklist-file', '--blacklist_file',
+ help='Path to a blacklist file, this file '
+ 'contains a separate regex exclude on '
+ 'each newline')
+ parser.add_argument('--load-list', '--load_list',
+ help='Path to a non-regex whitelist file, '
+ 'this file contains a separate test '
+ 'on each newline. This command'
+ 'supports files created by the tempest'
+ 'run ``--list-tests`` command')
+ # list only args
+ parser.add_argument('--list-tests', '-l', action='store_true',
+ help='List tests',
+ default=False)
+ # execution args
+ parser.add_argument('--concurrency', '-w',
+ help="The number of workers to use, defaults to "
+ "the number of cpus")
+ parallel = parser.add_mutually_exclusive_group()
+ parallel.add_argument('--parallel', dest='parallel',
+ action='store_true',
+ help='Run tests in parallel (this is the'
+ ' default)')
+ parallel.add_argument('--serial', '-t', dest='parallel',
+ action='store_false',
+ help='Run tests serially')
+ parser.add_argument('--save-state', dest='state',
+ action='store_true',
+ help="To save the state of the cloud before "
+ "running tempest.")
+ # output args
+ parser.add_argument("--subunit", action='store_true',
+ help='Enable subunit v2 output')
+ parser.add_argument("--combine", action='store_true',
+ help='Combine the output of this run with the '
+ "previous run's as a combined stream in the "
+ "stestr repository after it finish")
+
+ parser.set_defaults(parallel=True)
+ return parser
+
+ def _build_regex(self, parsed_args):
+ regex = None
+ if parsed_args.smoke:
+ regex = ['smoke']
+ elif parsed_args.regex:
+ regex = parsed_args.regex.split()
+ return regex
diff --git a/tempest/cmd/run_stress.py b/tempest/cmd/run_stress.py
deleted file mode 100644
index 943fe5b..0000000
--- a/tempest/cmd/run_stress.py
+++ /dev/null
@@ -1,163 +0,0 @@
-#!/usr/bin/env python
-
-# Copyright 2013 Quanta Research Cambridge, Inc.
-#
-# 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 argparse
-import inspect
-import sys
-try:
- from unittest import loader
-except ImportError:
- # unittest in python 2.6 does not contain loader, so uses unittest2
- from unittest2 import loader
-import traceback
-
-from cliff import command
-from oslo_log import log as logging
-from oslo_serialization import jsonutils as json
-from testtools import testsuite
-
-from tempest.stress import driver
-
-LOG = logging.getLogger(__name__)
-
-
-def discover_stress_tests(path="./", filter_attr=None, call_inherited=False):
- """Discovers all tempest tests and create action out of them"""
- LOG.info("Start test discovery")
- tests = []
- testloader = loader.TestLoader()
- list = testloader.discover(path)
- for func in (testsuite.iterate_tests(list)):
- attrs = []
- try:
- method_name = getattr(func, '_testMethodName')
- full_name = "%s.%s.%s" % (func.__module__,
- func.__class__.__name__,
- method_name)
- test_func = getattr(func, method_name)
- # NOTE(mkoderer): this contains a list of all type attributes
- attrs = getattr(test_func, "__testtools_attrs")
- except Exception:
- next
- if 'stress' in attrs:
- if filter_attr is not None and filter_attr not in attrs:
- continue
- class_setup_per = getattr(test_func, "st_class_setup_per")
-
- action = {'action':
- "tempest.stress.actions.unit_test.UnitTest",
- 'kwargs': {"test_method": full_name,
- "class_setup_per": class_setup_per
- }
- }
- if (not call_inherited and
- getattr(test_func, "st_allow_inheritance") is not True):
- class_structure = inspect.getmro(test_func.im_class)
- if test_func.__name__ not in class_structure[0].__dict__:
- continue
- tests.append(action)
- return tests
-
-
-class TempestRunStress(command.Command):
-
- def get_parser(self, prog_name):
- pa = super(TempestRunStress, self).get_parser(prog_name)
- pa = add_arguments(pa)
- return pa
-
- def take_action(self, pa):
- try:
- action(pa)
- except Exception:
- LOG.exception("Failure in the stress test framework")
- traceback.print_exc()
- raise
- return 0
-
- def get_description(self):
- return 'Run tempest stress tests'
-
-
-def add_arguments(parser):
- parser.add_argument('-d', '--duration', default=300, type=int,
- help="Duration of test in secs")
- parser.add_argument('-s', '--serial', action='store_true',
- help="Trigger running tests serially")
- parser.add_argument('-S', '--stop', action='store_true',
- default=False, help="Stop on first error")
- parser.add_argument('-n', '--number', type=int,
- help="How often an action is executed for each "
- "process")
- group = parser.add_mutually_exclusive_group(required=True)
- group.add_argument('-a', '--all', action='store_true',
- help="Execute all stress tests")
- parser.add_argument('-T', '--type',
- help="Filters tests of a certain type (e.g. gate)")
- parser.add_argument('-i', '--call-inherited', action='store_true',
- default=False,
- help="Call also inherited function with stress "
- "attribute")
- group.add_argument('-t', "--tests", nargs='?',
- help="Name of the file with test description")
- return parser
-
-
-def action(ns):
- result = 0
- if not ns.all:
- tests = json.load(open(ns.tests, 'r'))
- else:
- tests = discover_stress_tests(filter_attr=ns.type,
- call_inherited=ns.call_inherited)
-
- if ns.serial:
- # Duration is total time
- duration = ns.duration / len(tests)
- for test in tests:
- step_result = driver.stress_openstack([test],
- duration,
- ns.number,
- ns.stop)
- # NOTE(mkoderer): we just save the last result code
- if (step_result != 0):
- result = step_result
- if ns.stop:
- return result
- else:
- result = driver.stress_openstack(tests,
- ns.duration,
- ns.number,
- ns.stop)
- return result
-
-
-def main():
- LOG.warning("Deprecated: Use 'tempest run-stress' instead. "
- "The old entrypoint will be removed in a future release.")
- parser = argparse.ArgumentParser(description='Run stress tests')
- pa = add_arguments(parser)
- ns = pa.parse_args()
- return action(ns)
-
-
-if __name__ == "__main__":
- try:
- sys.exit(main())
- except Exception:
- LOG.exception("Failure in the stress test framework")
- traceback.print_exc()
- sys.exit(1)
diff --git a/tempest/cmd/subunit_describe_calls.py b/tempest/cmd/subunit_describe_calls.py
new file mode 100644
index 0000000..a4402fe
--- /dev/null
+++ b/tempest/cmd/subunit_describe_calls.py
@@ -0,0 +1,331 @@
+# Copyright 2016 Rackspace
+#
+# 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.
+
+"""
+subunit-describe-calls is a parser for subunit streams to determine what REST
+API calls are made inside of a test and in what order they are called.
+
+Runtime Arguments
+-----------------
+
+* ``--subunit, -s``: (Optional) The path to the subunit file being parsed,
+ defaults to stdin
+* ``--non-subunit-name, -n``: (Optional) The file_name that the logs are being
+ stored in
+* ``--output-file, -o``: (Optional) The path where the JSON output will be
+ written to. This contains more information than is present in stdout.
+* ``--ports, -p``: (Optional) The path to a JSON file describing the ports
+ being used by different services
+* ``--verbose, -v``: (Optional) Print Request and Response Headers and Body
+ data to stdout
+
+
+Usage
+-----
+
+subunit-describe-calls will take in either stdin subunit v1 or v2 stream or a
+file path which contains either a subunit v1 or v2 stream passed via the
+--subunit parameter. This is then parsed checking for details contained in the
+file_bytes of the --non-subunit-name parameter (the default is pythonlogging
+which is what Tempest uses to store logs). By default the OpenStack Kilo
+release port defaults (http://bit.ly/22jpF5P) are used unless a file is
+provided via the --ports option. The resulting output is dumped in JSON output
+to the path provided in the --output-file option.
+
+Ports file JSON structure
+^^^^^^^^^^^^^^^^^^^^^^^^^
+::
+
+ {
+ "<port number>": "<name of service>",
+ ...
+ }
+
+
+Output file JSON structure
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+::
+
+ {
+ "full_test_name[with_id_and_tags]": [
+ {
+ "name": "The ClassName.MethodName that made the call",
+ "verb": "HTTP Verb",
+ "service": "Name of the service",
+ "url": "A shortened version of the URL called",
+ "status_code": "The status code of the response",
+ "request_headers": "The headers of the request",
+ "request_body": "The body of the request",
+ "response_headers": "The headers of the response",
+ "response_body": "The body of the response"
+ }
+ ]
+ }
+"""
+import argparse
+import collections
+import io
+import json
+import os
+import re
+import sys
+
+import subunit
+import testtools
+
+
+class UrlParser(testtools.TestResult):
+ uuid_re = re.compile(r'(^|[^0-9a-f])[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-'
+ '[0-9a-f]{4}-[0-9a-f]{12}([^0-9a-f]|$)')
+ id_re = re.compile(r'(^|[^0-9a-z])[0-9a-z]{8}[0-9a-z]{4}[0-9a-z]{4}'
+ '[0-9a-z]{4}[0-9a-z]{12}([^0-9a-z]|$)')
+ ip_re = re.compile(r'(^|[^0-9])[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]'
+ '{1,3}([^0-9]|$)')
+ url_re = re.compile(r'.*INFO.*Request \((?P<name>.*)\): (?P<code>[\d]{3}) '
+ '(?P<verb>\w*) (?P<url>.*) .*')
+ port_re = re.compile(r'.*:(?P<port>\d+).*')
+ path_re = re.compile(r'http[s]?://[^/]*/(?P<path>.*)')
+ request_re = re.compile(r'.* Request - Headers: (?P<headers>.*)')
+ response_re = re.compile(r'.* Response - Headers: (?P<headers>.*)')
+ body_re = re.compile(r'.*Body: (?P<body>.*)')
+
+ # Based on newton defaults:
+ # http://docs.openstack.org/newton/config-reference/
+ # firewalls-default-ports.html
+ services = {
+ "8776": "Block Storage",
+ "8774": "Nova",
+ "8773": "Nova-API", "8775": "Nova-API",
+ "8386": "Sahara",
+ "35357": "Keystone", "5000": "Keystone",
+ "9292": "Glance", "9191": "Glance",
+ "9696": "Neutron",
+ "6000": "Swift", "6001": "Swift", "6002": "Swift",
+ "8004": "Heat", "8000": "Heat", "8003": "Heat",
+ "8777": "Ceilometer",
+ "80": "Horizon",
+ "8080": "Swift",
+ "443": "SSL",
+ "873": "rsync",
+ "3260": "iSCSI",
+ "3306": "MySQL",
+ "5672": "AMQP",
+ "8082": "murano"}
+
+ def __init__(self, services=None):
+ super(UrlParser, self).__init__()
+ self.test_logs = {}
+ self.services = services or self.services
+
+ def addSuccess(self, test, details=None):
+ output = test.shortDescription() or test.id()
+ calls = self.parse_details(details)
+ self.test_logs.update({output: calls})
+
+ def addSkip(self, test, err, details=None):
+ output = test.shortDescription() or test.id()
+ calls = self.parse_details(details)
+ self.test_logs.update({output: calls})
+
+ def addError(self, test, err, details=None):
+ output = test.shortDescription() or test.id()
+ calls = self.parse_details(details)
+ self.test_logs.update({output: calls})
+
+ def addFailure(self, test, err, details=None):
+ output = test.shortDescription() or test.id()
+ calls = self.parse_details(details)
+ self.test_logs.update({output: calls})
+
+ def stopTestRun(self):
+ super(UrlParser, self).stopTestRun()
+
+ def startTestRun(self):
+ super(UrlParser, self).startTestRun()
+
+ def parse_details(self, details):
+ if details is None:
+ return
+
+ calls = []
+ for _, detail in details.items():
+ in_request = False
+ in_response = False
+ current_call = {}
+ for line in detail.as_text().split("\n"):
+ url_match = self.url_re.match(line)
+ request_match = self.request_re.match(line)
+ response_match = self.response_re.match(line)
+ body_match = self.body_re.match(line)
+
+ if url_match is not None:
+ if current_call != {}:
+ calls.append(current_call.copy())
+ current_call = {}
+ in_request, in_response = False, False
+ current_call.update({
+ "name": url_match.group("name"),
+ "verb": url_match.group("verb"),
+ "status_code": url_match.group("code"),
+ "service": self.get_service(url_match.group("url")),
+ "url": self.url_path(url_match.group("url"))})
+ elif request_match is not None:
+ in_request, in_response = True, False
+ current_call.update(
+ {"request_headers": request_match.group("headers")})
+ elif in_request and body_match is not None:
+ in_request = False
+ current_call.update(
+ {"request_body": body_match.group(
+ "body")})
+ elif response_match is not None:
+ in_request, in_response = False, True
+ current_call.update(
+ {"response_headers": response_match.group(
+ "headers")})
+ elif in_response and body_match is not None:
+ in_response = False
+ current_call.update(
+ {"response_body": body_match.group("body")})
+ if current_call != {}:
+ calls.append(current_call.copy())
+
+ return calls
+
+ def get_service(self, url):
+ match = self.port_re.match(url)
+ if match is not None:
+ return self.services.get(match.group("port"), "Unknown")
+ return "Unknown"
+
+ def url_path(self, url):
+ match = self.path_re.match(url)
+ if match is not None:
+ path = match.group("path")
+ path = self.uuid_re.sub(r'\1<uuid>\2', path)
+ path = self.ip_re.sub(r'\1<ip>\2', path)
+ path = self.id_re.sub(r'\1<id>\2', path)
+ return path
+ return url
+
+
+class FileAccumulator(testtools.StreamResult):
+
+ def __init__(self, non_subunit_name='pythonlogging'):
+ super(FileAccumulator, self).__init__()
+ self.route_codes = collections.defaultdict(io.BytesIO)
+ self.non_subunit_name = non_subunit_name
+
+ def status(self, **kwargs):
+ if kwargs.get('file_name') != self.non_subunit_name:
+ return
+ file_bytes = kwargs.get('file_bytes')
+ if not file_bytes:
+ return
+ route_code = kwargs.get('route_code')
+ stream = self.route_codes[route_code]
+ stream.write(file_bytes)
+
+
+class ArgumentParser(argparse.ArgumentParser):
+ def __init__(self):
+ desc = "Outputs all HTTP calls a given test made that were logged."
+ super(ArgumentParser, self).__init__(description=desc)
+
+ self.prog = "subunit-describe-calls"
+
+ self.add_argument(
+ "-s", "--subunit", metavar="<subunit file>",
+ nargs="?", type=argparse.FileType('rb'), default=sys.stdin,
+ help="The path to the subunit output file.")
+
+ self.add_argument(
+ "-n", "--non-subunit-name", metavar="<non subunit name>",
+ default="pythonlogging",
+ help="The name used in subunit to describe the file contents.")
+
+ self.add_argument(
+ "-o", "--output-file", metavar="<output file>", default=None,
+ help="The output file name for the json.")
+
+ self.add_argument(
+ "-p", "--ports", metavar="<ports file>", default=None,
+ help="A JSON file describing the ports for each service.")
+
+ self.add_argument(
+ "-v", "--verbose", action='store_true', default=False,
+ help="Add Request and Response header and body data to stdout.")
+
+
+def parse(stream, non_subunit_name, ports):
+ if ports is not None and os.path.exists(ports):
+ ports = json.loads(open(ports).read())
+
+ url_parser = UrlParser(ports)
+ suite = subunit.ByteStreamToStreamResult(
+ stream, non_subunit_name=non_subunit_name)
+ result = testtools.StreamToExtendedDecorator(url_parser)
+ accumulator = FileAccumulator(non_subunit_name)
+ result = testtools.StreamResultRouter(result)
+ result.add_rule(accumulator, 'test_id', test_id=None)
+ result.startTestRun()
+ suite.run(result)
+
+ for bytes_io in accumulator.route_codes.values(): # v1 processing
+ bytes_io.seek(0)
+ suite = subunit.ProtocolTestCase(bytes_io)
+ suite.run(url_parser)
+ result.stopTestRun()
+
+ return url_parser
+
+
+def output(url_parser, output_file, verbose):
+ if output_file is not None:
+ with open(output_file, "w") as outfile:
+ outfile.write(json.dumps(url_parser.test_logs))
+ return
+
+ for test_name in url_parser.test_logs:
+ items = url_parser.test_logs[test_name]
+ sys.stdout.write('{0}\n'.format(test_name))
+ if not items:
+ sys.stdout.write('\n')
+ continue
+ for item in items:
+ sys.stdout.write('\t- {0} {1} request for {2} to {3}\n'.format(
+ item.get('status_code'), item.get('verb'),
+ item.get('service'), item.get('url')))
+ if verbose:
+ sys.stdout.write('\t\t- request headers: {0}\n'.format(
+ item.get('request_headers')))
+ sys.stdout.write('\t\t- request body: {0}\n'.format(
+ item.get('request_body')))
+ sys.stdout.write('\t\t- response headers: {0}\n'.format(
+ item.get('response_headers')))
+ sys.stdout.write('\t\t- response body: {0}\n'.format(
+ item.get('response_body')))
+ sys.stdout.write('\n')
+
+
+def entry_point():
+ cl_args = ArgumentParser().parse_args()
+ parser = parse(cl_args.subunit, cl_args.non_subunit_name, cl_args.ports)
+ output(parser, cl_args.output_file, cl_args.verbose)
+
+
+if __name__ == "__main__":
+ entry_point()
diff --git a/tempest/cmd/verify_tempest_config.py b/tempest/cmd/verify_tempest_config.py
old mode 100755
new mode 100644
index 9c8e2a0..15af271
--- a/tempest/cmd/verify_tempest_config.py
+++ b/tempest/cmd/verify_tempest_config.py
@@ -14,11 +14,59 @@
# License for the specific language governing permissions and limitations
# under the License.
+"""
+Verifies user's current tempest configuration.
+
+This command is used for updating or user's tempest configuration file based on
+api queries or replacing all option in a tempest configuration file for a full
+list of extensions.
+
+General Options
+===============
+
+-u, --update
+------------
+Update the config file with results from api queries. This assumes whatever is
+set in the config file is incorrect.
+
+-o FILE, --output=FILE
+----------------------
+Output file to write an updated config file to. This has to be a separate file
+from the original one. If one isn't specified with -u the values which should
+be changed will be printed to STDOUT.
+
+-r, --replace-ext
+-----------------
+If specified the all option will be replaced with a full list of extensions.
+
+Environment Variables
+=====================
+
+The command is workspace aware - it uses tempest config file tempest.conf
+located in ./etc/ directory.
+The path to the config file and it's name can be changed through environment
+variables.
+
+TEMPEST_CONFIG_DIR
+------------------
+Path to a directory where tempest configuration file is stored. If the variable
+is set, the default path (./etc/) is overridden.
+
+TEMPEST_CONFIG
+--------------
+Name of a tempest configuration file. If the variable is specified, the default
+name (tempest.conf) is overridden.
+
+"""
+
import argparse
import os
+import re
import sys
+import traceback
-import httplib2
+from cliff import command
+from oslo_log import log as logging
from oslo_serialization import jsonutils as json
from six import moves
from six.moves.urllib import parse as urlparse
@@ -26,21 +74,25 @@
from tempest import clients
from tempest.common import credentials_factory as credentials
from tempest import config
+import tempest.lib.common.http
+from tempest.lib import exceptions as lib_exc
CONF = config.CONF
CONF_PARSER = None
+LOG = logging.getLogger(__name__)
+
def _get_config_file():
- default_config_dir = os.path.join(os.path.abspath(
- os.path.dirname(os.path.dirname(os.path.dirname(__file__)))), "etc")
+ config_dir = os.getcwd()
+ default_config_dir = os.path.join(config_dir, "etc")
default_config_file = "tempest.conf"
conf_dir = os.environ.get('TEMPEST_CONFIG_DIR', default_config_dir)
conf_file = os.environ.get('TEMPEST_CONFIG', default_config_file)
path = os.path.join(conf_dir, conf_file)
- fd = open(path, 'rw')
+ fd = open(path, 'r+')
return fd
@@ -63,7 +115,25 @@
def verify_glance_api_versions(os, update):
# Check glance api versions
- _, versions = os.image_client.get_versions()
+ # Since we want to verify that the configuration is correct, we cannot
+ # rely on a specific version of the API being available.
+ try:
+ _, versions = os.image_v1.ImagesClient().get_versions()
+ except lib_exc.NotFound:
+ # If not found, we use v2. The assumption is that either v1 or v2
+ # are available, since glance is marked as available in the catalog.
+ # If not, glance should be disabled in Tempest conf.
+ try:
+ versions = os.image_v2.VersionsClient().list_versions()['versions']
+ versions = [x['id'] for x in versions]
+ except lib_exc.NotFound:
+ msg = ('Glance is available in the catalog, but no known version, '
+ '(v1.x or v2.x) of Glance could be found, so Glance should '
+ 'be configured as not available')
+ LOG.warn(msg)
+ print_and_or_update('glance', 'service-available', False, update)
+ return
+
if CONF.image_feature_enabled.api_v1 != contains_version('v1.', versions):
print_and_or_update('api_v1', 'image-feature-enabled',
not CONF.image_feature_enabled.api_v1, update)
@@ -72,27 +142,50 @@
not CONF.image_feature_enabled.api_v2, update)
+def _remove_version_project(url_path):
+ # The regex matches strings like /v2.0, /v3/, /v2.1/project-id/
+ return re.sub(r'/v\d+(\.\d+)?(/[^/]+)?', '', url_path)
+
+
def _get_unversioned_endpoint(base_url):
endpoint_parts = urlparse.urlparse(base_url)
- endpoint = endpoint_parts.scheme + '://' + endpoint_parts.netloc
+ new_path = _remove_version_project(endpoint_parts.path)
+ endpoint_parts = endpoint_parts._replace(path=new_path)
+ endpoint = urlparse.urlunparse(endpoint_parts)
return endpoint
def _get_api_versions(os, service):
+ # Clients are used to obtain the base_url. Each client applies the
+ # appropriate filters to the catalog to extract a base_url which
+ # matches the configured region and endpoint_type.
+ # The base URL is used to obtain the list of versions available.
client_dict = {
- 'nova': os.servers_client,
- 'keystone': os.identity_client,
- 'cinder': os.volumes_client,
+ 'nova': os.compute.ServersClient(),
+ 'keystone': os.identity_v3.IdentityClient(
+ endpoint_type=CONF.identity.v3_endpoint_type),
+ 'cinder': os.volume_v3.VolumesClient(),
}
- client_dict[service].skip_path()
+ if service != 'keystone' and service != 'cinder':
+ # Since keystone and cinder may be listening on a path,
+ # do not remove the path.
+ client_dict[service].skip_path()
endpoint = _get_unversioned_endpoint(client_dict[service].base_url)
- dscv = CONF.identity.disable_ssl_certificate_validation
- ca_certs = CONF.identity.ca_certificates_file
- raw_http = httplib2.Http(disable_ssl_certificate_validation=dscv,
- ca_certs=ca_certs)
- __, body = raw_http.request(endpoint, 'GET')
+
+ http = tempest.lib.common.http.ClosingHttp(
+ CONF.identity.disable_ssl_certificate_validation,
+ CONF.identity.ca_certificates_file)
+
+ __, body = http.request(endpoint, 'GET')
client_dict[service].reset_path()
- body = json.loads(body)
+ try:
+ body = json.loads(body)
+ except ValueError:
+ LOG.error(
+ 'Failed to get a JSON response from unversioned endpoint %s '
+ '(versioned endpoint was %s). Response is:\n%s',
+ endpoint, client_dict[service].base_url, body[:100])
+ raise
if service == 'keystone':
versions = map(lambda x: x['id'], body['versions']['values'])
else:
@@ -103,10 +196,6 @@
def verify_keystone_api_versions(os, update):
# Check keystone api versions
versions = _get_api_versions(os, 'keystone')
- if (CONF.identity_feature_enabled.api_v2 !=
- contains_version('v2.', versions)):
- print_and_or_update('api_v2', 'identity-feature-enabled',
- not CONF.identity_feature_enabled.api_v2, update)
if (CONF.identity_feature_enabled.api_v3 !=
contains_version('v3.', versions)):
print_and_or_update('api_v3', 'identity-feature-enabled',
@@ -124,6 +213,10 @@
contains_version('v2.', versions)):
print_and_or_update('api_v2', 'volume-feature-enabled',
not CONF.volume_feature_enabled.api_v2, update)
+ if (CONF.volume_feature_enabled.api_v3 !=
+ contains_version('v3.', versions)):
+ print_and_or_update('api_v3', 'volume-feature-enabled',
+ not CONF.volume_feature_enabled.api_v3, update)
def verify_api_versions(os, service, update):
@@ -139,20 +232,18 @@
def get_extension_client(os, service):
extensions_client = {
- 'nova': os.extensions_client,
- 'cinder': os.volumes_extension_client,
- 'neutron': os.network_client,
- 'swift': os.account_client,
+ 'nova': os.compute.ExtensionsClient(),
+ 'neutron': os.network.ExtensionsClient(),
+ 'swift': os.object_storage.CapabilitiesClient(),
+ # NOTE: Cinder v3 API is current and v2 and v1 are deprecated.
+ # V3 extension API is the same as v2, so we reuse the v2 client
+ # for v3 API also.
+ 'cinder': os.volume_v2.ExtensionsClient(),
}
- # NOTE (e0ne): Use Cinder API v2 by default because v1 is deprecated
- if CONF.volume_feature_enabled.api_v2:
- extensions_client['cinder'] = os.volumes_v2_extension_client
- else:
- extensions_client['cinder'] = os.volumes_extension_client
if service not in extensions_client:
print('No tempest extensions client for %s' % service)
- exit(1)
+ sys.exit(1)
return extensions_client[service]
@@ -165,7 +256,7 @@
}
if service not in extensions_options:
print('No supported extensions list option for %s' % service)
- exit(1)
+ sys.exit(1)
return extensions_options[service]
@@ -174,7 +265,7 @@
if service != 'swift':
resp = extensions_client.list_extensions()
else:
- __, resp = extensions_client.list_extensions()
+ resp = extensions_client.list_capabilities()
# For Nova, Cinder and Neutron we use the alias name rather than the
# 'name' field because the alias is considered to be the canonical
# name.
@@ -258,14 +349,8 @@
'image': 'glance',
'object_storage': 'swift',
'compute': 'nova',
- 'orchestration': 'heat',
- 'metering': 'ceilometer',
- 'telemetry': 'ceilometer',
- 'data_processing': 'sahara',
'baremetal': 'ironic',
'identity': 'keystone',
- 'messaging': 'zaqar',
- 'database': 'trove'
}
# Get catalog list for endpoints to use for validation
_token, auth_data = os.auth_provider.get_auth()
@@ -310,8 +395,7 @@
return avail_services
-def parse_args():
- parser = argparse.ArgumentParser()
+def _parser_add_args(parser):
parser.add_argument('-u', '--update', action='store_true',
help='Update the config file with results from api '
'queries. This assumes whatever is set in the '
@@ -324,33 +408,47 @@
help="Output file to write an updated config file to. "
"This has to be a separate file from the "
"original config file. If one isn't specified "
- "with -u the new config file will be printed to "
- "STDOUT")
+ "with -u the values which should be changed "
+ "will be printed to STDOUT")
parser.add_argument('-r', '--replace-ext', action='store_true',
help="If specified the all option will be replaced "
"with a full list of extensions")
- args = parser.parse_args()
- return args
-def main():
+def parse_args():
+ parser = argparse.ArgumentParser()
+ _parser_add_args(parser)
+ opts = parser.parse_args()
+ return opts
+
+
+def main(opts=None):
print('Running config verification...')
- opts = parse_args()
+ if opts is None:
+ print("Use of: 'verify-tempest-config' is deprecated, "
+ "please use: 'tempest verify-config'")
+ opts = parse_args()
update = opts.update
replace = opts.replace_ext
global CONF_PARSER
- outfile = sys.stdout
if update:
conf_file = _get_config_file()
- if opts.output:
- outfile = open(opts.output, 'w+')
- CONF_PARSER = moves.configparser.SafeConfigParser()
+ CONF_PARSER = moves.configparser.ConfigParser()
CONF_PARSER.optionxform = str
CONF_PARSER.readfp(conf_file)
- icreds = credentials.get_credentials_provider('verify_tempest_config')
+
+ # Indicate not to create network resources as part of getting credentials
+ net_resources = {
+ 'network': False,
+ 'router': False,
+ 'subnet': False,
+ 'dhcp': False
+ }
+ icreds = credentials.get_credentials_provider(
+ 'verify_tempest_config', network_resources=net_resources)
try:
- os = clients.Manager(icreds.get_primary_creds())
+ os = clients.Manager(icreds.get_primary_creds().credentials)
services = check_service_availability(os, update)
results = {}
for service in ['nova', 'cinder', 'neutron', 'swift']:
@@ -367,11 +465,28 @@
display_results(results, update, replace)
if update:
conf_file.close()
- CONF_PARSER.write(outfile)
- outfile.close()
+ if opts.output:
+ with open(opts.output, 'w+') as outfile:
+ CONF_PARSER.write(outfile)
finally:
icreds.clear_creds()
+class TempestVerifyConfig(command.Command):
+ """Verify your current tempest configuration"""
+
+ def get_parser(self, prog_name):
+ parser = super(TempestVerifyConfig, self).get_parser(prog_name)
+ _parser_add_args(parser)
+ return parser
+
+ def take_action(self, parsed_args):
+ try:
+ main(parsed_args)
+ except Exception:
+ LOG.exception("Failure verifying configuration.")
+ traceback.print_exc()
+ raise
+
if __name__ == "__main__":
main()
diff --git a/tempest/cmd/workspace.py b/tempest/cmd/workspace.py
new file mode 100644
index 0000000..929a584
--- /dev/null
+++ b/tempest/cmd/workspace.py
@@ -0,0 +1,262 @@
+# Copyright 2016 Rackspace
+#
+# 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.
+
+"""
+Manages Tempest workspaces
+
+This command is used for managing tempest workspaces
+
+Commands
+========
+
+list
+----
+Outputs the name and path of all known tempest workspaces
+
+register
+--------
+Registers a new tempest workspace via a given ``--name`` and ``--path``
+
+rename
+------
+Renames a tempest workspace from ``--old-name`` to ``--new-name``
+
+move
+----
+Changes the path of a given tempest workspace ``--name`` to ``--path``
+
+remove
+------
+Deletes the entry for a given tempest workspace ``--name``
+
+``--rmdir`` Deletes the given tempest workspace directory
+
+General Options
+===============
+
+* ``--workspace_path``: Allows the user to specify a different location for the
+ workspace.yaml file containing the workspace definitions instead of
+ ``~/.tempest/workspace.yaml``
+"""
+
+import os
+import shutil
+import sys
+
+from cliff import command
+from cliff import lister
+from oslo_concurrency import lockutils
+import yaml
+
+from tempest import config
+
+CONF = config.CONF
+
+
+class WorkspaceManager(object):
+ def __init__(self, path=None):
+ lockutils.get_lock_path(CONF)
+ self.path = path or os.path.join(
+ os.path.expanduser("~"), ".tempest", "workspace.yaml")
+ if not os.path.isdir(os.path.dirname(self.path)):
+ os.makedirs(self.path.rsplit(os.path.sep, 1)[0])
+ self.workspaces = {}
+
+ @lockutils.synchronized('workspaces', external=True)
+ def get_workspace(self, name):
+ """Returns the workspace that has the given name
+
+ If the workspace isn't registered then `None` is returned.
+ """
+ self._populate()
+ return self.workspaces.get(name)
+
+ @lockutils.synchronized('workspaces', external=True)
+ def rename_workspace(self, old_name, new_name):
+ self._populate()
+ self._name_exists(old_name)
+ self._workspace_name_exists(new_name)
+ self.workspaces[new_name] = self.workspaces.pop(old_name)
+ self._write_file()
+
+ @lockutils.synchronized('workspaces', external=True)
+ def move_workspace(self, name, path):
+ self._populate()
+ path = os.path.abspath(os.path.expanduser(path))
+ self._name_exists(name)
+ self._validate_path(path)
+ self.workspaces[name] = path
+ self._write_file()
+
+ def _name_exists(self, name):
+ if name not in self.workspaces:
+ print("A workspace was not found with name: {0}".format(name))
+ sys.exit(1)
+
+ @lockutils.synchronized('workspaces', external=True)
+ def remove_workspace_entry(self, name):
+ self._populate()
+ self._name_exists(name)
+ workspace_path = self.workspaces.pop(name)
+ self._write_file()
+ return workspace_path
+
+ @lockutils.synchronized('workspaces', external=True)
+ def remove_workspace_directory(self, workspace_path):
+ shutil.rmtree(workspace_path)
+
+ @lockutils.synchronized('workspaces', external=True)
+ def list_workspaces(self):
+ self._populate()
+ self._validate_workspaces()
+ return self.workspaces
+
+ def _workspace_name_exists(self, name):
+ if name in self.workspaces:
+ print("A workspace already exists with name: {0}.".format(
+ name))
+ sys.exit(1)
+
+ def _validate_path(self, path):
+ if not os.path.exists(path):
+ print("Path does not exist.")
+ sys.exit(1)
+
+ @lockutils.synchronized('workspaces', external=True)
+ def register_new_workspace(self, name, path, init=False):
+ """Adds the new workspace and writes out the new workspace config"""
+ self._populate()
+ path = os.path.abspath(os.path.expanduser(path))
+ # This only happens when register is called from outside of init
+ if not init:
+ self._validate_path(path)
+ self._workspace_name_exists(name)
+ self.workspaces[name] = path
+ self._write_file()
+
+ def _validate_workspaces(self):
+ if self.workspaces is not None:
+ self.workspaces = {n: p for n, p in self.workspaces.items()
+ if os.path.exists(p)}
+ self._write_file()
+
+ def _write_file(self):
+ with open(self.path, 'w') as f:
+ f.write(yaml.dump(self.workspaces))
+
+ def _populate(self):
+ if not os.path.isfile(self.path):
+ return
+ with open(self.path, 'r') as f:
+ self.workspaces = yaml.safe_load(f) or {}
+
+
+def add_global_arguments(parser):
+ parser.add_argument(
+ '--workspace-path', required=False, default=None,
+ help="The path to the workspace file, the default is "
+ "~/.tempest/workspace.yaml")
+ return parser
+
+
+class TempestWorkspaceRegister(command.Command):
+ def get_description(self):
+ return ('Registers a new tempest workspace via a given '
+ '--name and --path')
+
+ def get_parser(self, prog_name):
+ parser = super(TempestWorkspaceRegister, self).get_parser(prog_name)
+ add_global_arguments(parser)
+ parser.add_argument('--name', required=True)
+ parser.add_argument('--path', required=True)
+
+ return parser
+
+ def take_action(self, parsed_args):
+ self.manager = WorkspaceManager(parsed_args.workspace_path)
+ self.manager.register_new_workspace(parsed_args.name, parsed_args.path)
+ sys.exit(0)
+
+
+class TempestWorkspaceRename(command.Command):
+ def get_description(self):
+ return 'Renames a tempest workspace from --old-name to --new-name'
+
+ def get_parser(self, prog_name):
+ parser = super(TempestWorkspaceRename, self).get_parser(prog_name)
+ add_global_arguments(parser)
+ parser.add_argument('--old-name', required=True)
+ parser.add_argument('--new-name', required=True)
+
+ return parser
+
+ def take_action(self, parsed_args):
+ self.manager = WorkspaceManager(parsed_args.workspace_path)
+ self.manager.rename_workspace(
+ parsed_args.old_name, parsed_args.new_name)
+ sys.exit(0)
+
+
+class TempestWorkspaceMove(command.Command):
+ def get_description(self):
+ return 'Changes the path of a given tempest workspace --name to --path'
+
+ def get_parser(self, prog_name):
+ parser = super(TempestWorkspaceMove, self).get_parser(prog_name)
+ add_global_arguments(parser)
+ parser.add_argument('--name', required=True)
+ parser.add_argument('--path', required=True)
+
+ return parser
+
+ def take_action(self, parsed_args):
+ self.manager = WorkspaceManager(parsed_args.workspace_path)
+ self.manager.move_workspace(parsed_args.name, parsed_args.path)
+ sys.exit(0)
+
+
+class TempestWorkspaceRemove(command.Command):
+ def get_description(self):
+ return 'Deletes the entry for a given tempest workspace --name'
+
+ def get_parser(self, prog_name):
+ parser = super(TempestWorkspaceRemove, self).get_parser(prog_name)
+ add_global_arguments(parser)
+ parser.add_argument('--name', required=True)
+ parser.add_argument('--rmdir', action='store_true',
+ help='Deletes the given workspace directory')
+
+ return parser
+
+ def take_action(self, parsed_args):
+ self.manager = WorkspaceManager(parsed_args.workspace_path)
+ workspace_path = self.manager.remove_workspace_entry(parsed_args.name)
+ if parsed_args.rmdir:
+ self.manager.remove_workspace_directory(workspace_path)
+ sys.exit(0)
+
+
+class TempestWorkspaceList(lister.Lister):
+ def get_description(self):
+ return 'Outputs the name and path of all known tempest workspaces'
+
+ def get_parser(self, prog_name):
+ parser = super(TempestWorkspaceList, self).get_parser(prog_name)
+ add_global_arguments(parser)
+ return parser
+
+ def take_action(self, parsed_args):
+ self.manager = WorkspaceManager(parsed_args.workspace_path)
+ return (("Name", "Path"),
+ ((n, p) for n, p in self.manager.list_workspaces().items()))
diff --git a/tempest/common/api_version_request.py b/tempest/common/api_version_request.py
deleted file mode 100644
index d8a5b56..0000000
--- a/tempest/common/api_version_request.py
+++ /dev/null
@@ -1,154 +0,0 @@
-# Copyright 2014 IBM Corp.
-#
-# 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 re
-
-from tempest import exceptions
-
-
-# Define the minimum and maximum version of the API across all of the
-# REST API. The format of the version is:
-# X.Y where:
-#
-# - X will only be changed if a significant backwards incompatible API
-# change is made which affects the API as whole. That is, something
-# that is only very very rarely incremented.
-#
-# - Y when you make any change to the API. Note that this includes
-# semantic changes which may not affect the input or output formats or
-# even originate in the API code layer. We are not distinguishing
-# between backwards compatible and backwards incompatible changes in
-# the versioning system. It must be made clear in the documentation as
-# to what is a backwards compatible change and what is a backwards
-# incompatible one.
-
-class APIVersionRequest(object):
- """This class represents an API Version Request.
-
- This class provides convenience methods for manipulation
- and comparison of version numbers that we need to do to
- implement microversions.
- """
-
- # NOTE: This 'latest' version is a magic number, we assume any
- # projects(Nova, etc.) never achieve this number.
- latest_ver_major = 99999
- latest_ver_minor = 99999
-
- def __init__(self, version_string=None):
- """Create an API version request object.
-
- :param version_string: String representation of APIVersionRequest.
- Correct format is 'X.Y', where 'X' and 'Y' are int values.
- None value should be used to create Null APIVersionRequest,
- which is equal to 0.0
- """
- # NOTE(gmann): 'version_string' as String "None" will be considered as
- # invalid version string.
- self.ver_major = 0
- self.ver_minor = 0
-
- if version_string is not None:
- match = re.match(r"^([1-9]\d*)\.([1-9]\d*|0)$",
- version_string)
- if match:
- self.ver_major = int(match.group(1))
- self.ver_minor = int(match.group(2))
- elif version_string == 'latest':
- self.ver_major = self.latest_ver_major
- self.ver_minor = self.latest_ver_minor
- else:
- raise exceptions.InvalidAPIVersionString(
- version=version_string)
-
- def __str__(self):
- """Debug/Logging representation of object."""
- return ("API Version Request: %s" % self.get_string())
-
- def is_null(self):
- return self.ver_major == 0 and self.ver_minor == 0
-
- def _format_type_error(self, other):
- return TypeError("'%(other)s' should be an instance of '%(cls)s'" %
- {"other": other, "cls": self.__class__})
-
- def __lt__(self, other):
- if not isinstance(other, APIVersionRequest):
- raise self._format_type_error(other)
-
- return ((self.ver_major, self.ver_minor) <
- (other.ver_major, other.ver_minor))
-
- def __eq__(self, other):
- if not isinstance(other, APIVersionRequest):
- raise self._format_type_error(other)
-
- return ((self.ver_major, self.ver_minor) ==
- (other.ver_major, other.ver_minor))
-
- def __gt__(self, other):
- if not isinstance(other, APIVersionRequest):
- raise self._format_type_error(other)
-
- return ((self.ver_major, self.ver_minor) >
- (other.ver_major, other.ver_minor))
-
- def __le__(self, other):
- return self < other or self == other
-
- def __ne__(self, other):
- return not self.__eq__(other)
-
- def __ge__(self, other):
- return self > other or self == other
-
- def matches(self, min_version, max_version):
- """Matches the version object.
-
- Returns whether the version object represents a version
- greater than or equal to the minimum version and less than
- or equal to the maximum version.
-
- @param min_version: Minimum acceptable version.
- @param max_version: Maximum acceptable version.
- @returns: boolean
-
- If min_version is null then there is no minimum limit.
- If max_version is null then there is no maximum limit.
- If self is null then raise ValueError
- """
-
- if self.is_null():
- raise ValueError
- if max_version.is_null() and min_version.is_null():
- return True
- elif max_version.is_null():
- return min_version <= self
- elif min_version.is_null():
- return self <= max_version
- else:
- return min_version <= self <= max_version
-
- def get_string(self):
- """Version string representation.
-
- Converts object to string representation which if used to create
- an APIVersionRequest object results in the same version request.
- """
- if self.is_null():
- return None
- if (self.ver_major == self.latest_ver_major and
- self.ver_minor == self.latest_ver_minor):
- return 'latest'
- return "%s.%s" % (self.ver_major, self.ver_minor)
diff --git a/tempest/common/api_version_utils.py b/tempest/common/api_version_utils.py
deleted file mode 100644
index 6b6364e..0000000
--- a/tempest/common/api_version_utils.py
+++ /dev/null
@@ -1,71 +0,0 @@
-# Copyright 2015 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.
-
-import testtools
-
-from tempest.common import api_version_request
-from tempest import exceptions
-
-
-class BaseMicroversionTest(object):
- """Mixin class for API microversion test class."""
-
- # NOTE: Basically, each microversion is small API change and we
- # can use the same tests for most microversions in most cases.
- # So it is nice to define the test class as possible to run
- # for all microversions. We need to define microversion range
- # (min_microversion, max_microversion) on each test class if necessary.
- min_microversion = None
- max_microversion = 'latest'
-
-
-def check_skip_with_microversion(test_min_version, test_max_version,
- cfg_min_version, cfg_max_version):
- min_version = api_version_request.APIVersionRequest(test_min_version)
- max_version = api_version_request.APIVersionRequest(test_max_version)
- config_min_version = api_version_request.APIVersionRequest(cfg_min_version)
- config_max_version = api_version_request.APIVersionRequest(cfg_max_version)
- if ((min_version > max_version) or
- (config_min_version > config_max_version)):
- msg = ("Min version is greater than Max version. Test Class versions "
- "[%s - %s]. configration versions [%s - %s]."
- % (min_version.get_string(),
- max_version.get_string(),
- config_min_version.get_string(),
- config_max_version.get_string()))
- raise exceptions.InvalidConfiguration(msg)
-
- # NOTE: Select tests which are in range of configuration like
- # config min config max
- # ----------------+--------------------------+----------------
- # ...don't-select|
- # ...select... ...select... ...select...
- # |don't-select...
- # ......................select............................
- if (max_version < config_min_version or
- config_max_version < min_version):
- msg = ("The microversion range[%s - %s] of this test is out of the "
- "configration range[%s - %s]."
- % (min_version.get_string(),
- max_version.get_string(),
- config_min_version.get_string(),
- config_max_version.get_string()))
- raise testtools.TestCase.skipException(msg)
-
-
-def select_request_microversion(test_min_version, cfg_min_version):
- test_version = api_version_request.APIVersionRequest(test_min_version)
- cfg_version = api_version_request.APIVersionRequest(cfg_min_version)
- max_version = cfg_version if cfg_version >= test_version else test_version
- return max_version.get_string()
diff --git a/tempest/common/commands.py b/tempest/common/commands.py
deleted file mode 100644
index 392c9d0..0000000
--- a/tempest/common/commands.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# 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 shlex
-import subprocess
-
-from oslo_log import log as logging
-
-LOG = logging.getLogger(__name__)
-
-
-def copy_file_to_host(file_from, dest, host, username, pkey):
- dest = "%s@%s:%s" % (username, host, dest)
- cmd = "scp -v -o UserKnownHostsFile=/dev/null " \
- "-o StrictHostKeyChecking=no " \
- "-i %(pkey)s %(file1)s %(dest)s" % {'pkey': pkey,
- 'file1': file_from,
- 'dest': dest}
- args = shlex.split(cmd.encode('utf-8'))
- subprocess_args = {'stdout': subprocess.PIPE,
- 'stderr': subprocess.STDOUT}
- proc = subprocess.Popen(args, **subprocess_args)
- stdout, stderr = proc.communicate()
- if proc.returncode != 0:
- LOG.error(("Command {0} returned with exit status {1},"
- "output {2}, error {3}").format(cmd, proc.returncode,
- stdout, stderr))
- return stdout
diff --git a/tempest/common/compute.py b/tempest/common/compute.py
index 5a14fbe..68c4a10 100644
--- a/tempest/common/compute.py
+++ b/tempest/common/compute.py
@@ -13,23 +13,59 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import base64
+import socket
+import ssl
+import struct
+import textwrap
+
+import six
+from six.moves.urllib import parse as urlparse
+
from oslo_log import log as logging
from oslo_utils import excutils
-from tempest_lib.common.utils import data_utils
-from tempest.common import fixed_network
-from tempest.common import service_client
from tempest.common import waiters
from tempest import config
+from tempest.lib.common import fixed_network
+from tempest.lib.common import rest_client
+from tempest.lib.common.utils import data_utils
+
+if six.PY2:
+ ord_func = ord
+else:
+ ord_func = int
CONF = config.CONF
LOG = logging.getLogger(__name__)
+def is_scheduler_filter_enabled(filter_name):
+ """Check the list of enabled compute scheduler filters from config.
+
+ This function checks whether the given compute scheduler filter is
+ available and configured in the config file. If the
+ scheduler_available_filters option is set to 'all' (Default value. which
+ means default filters are configured in nova) in tempest.conf then, this
+ function returns True with assumption that requested filter 'filter_name'
+ is one of available filter in nova ("nova.scheduler.filters.all_filters").
+ """
+
+ filters = CONF.compute_feature_enabled.scheduler_available_filters
+ if not filters:
+ return False
+ if 'all' in filters:
+ return True
+ if filter_name in filters:
+ return True
+ return False
+
+
def create_test_server(clients, validatable=False, validation_resources=None,
tenant_network=None, wait_until=None,
- volume_backed=False, **kwargs):
+ volume_backed=False, name=None, flavor=None,
+ image_id=None, **kwargs):
"""Common wrapper utility returning a test server.
This method is a common wrapper returning a test server that can be
@@ -38,35 +74,62 @@
:param clients: Client manager which provides OpenStack Tempest clients.
:param validatable: Whether the server will be pingable or sshable.
:param validation_resources: Resources created for the connection to the
- server. Include a keypair, a security group and an IP.
+ server. Include a keypair, a security group and an IP.
:param tenant_network: Tenant network to be used for creating a server.
:param wait_until: Server status to wait for the server to reach after
- its creation.
- :param volume_backed: Whether the instance is volume backed or not.
+ its creation.
+ :param volume_backed: Whether the server is volume backed or not.
+ If this is true, a volume will be created and
+ create server will be requested with
+ 'block_device_mapping_v2' populated with below
+ values:
+ --------------------------------------------
+ bd_map_v2 = [{
+ 'uuid': volume['volume']['id'],
+ 'source_type': 'volume',
+ 'destination_type': 'volume',
+ 'boot_index': 0,
+ 'delete_on_termination': True}]
+ kwargs['block_device_mapping_v2'] = bd_map_v2
+ ---------------------------------------------
+ If server needs to be booted from volume with other
+ combination of bdm inputs than mentioned above, then
+ pass the bdm inputs explicitly as kwargs and image_id
+ as empty string ('').
+ :param name: Name of the server to be provisioned. If not defined a random
+ string ending with '-instance' will be generated.
+ :param flavor: Flavor of the server to be provisioned. If not defined,
+ CONF.compute.flavor_ref will be used instead.
+ :param image_id: ID of the image to be used to provision the server. If not
+ defined, CONF.compute.image_ref will be used instead.
:returns: a tuple
"""
# TODO(jlanoux) add support of wait_until PINGABLE/SSHABLE
- if 'name' in kwargs:
- name = kwargs.pop('name')
- else:
+ if name is None:
name = data_utils.rand_name(__name__ + "-instance")
-
- flavor = kwargs.pop('flavor', CONF.compute.flavor_ref)
- image_id = kwargs.pop('image_id', CONF.compute.image_ref)
+ if flavor is None:
+ flavor = CONF.compute.flavor_ref
+ if image_id is None:
+ image_id = CONF.compute.image_ref
kwargs = fixed_network.set_networks_kwarg(
tenant_network, kwargs) or {}
+ multiple_create_request = (max(kwargs.get('min_count', 0),
+ kwargs.get('max_count', 0)) > 1)
+
if CONF.validation.run_validation and validatable:
# As a first implementation, multiple pingable or sshable servers will
# not be supported
- if 'min_count' in kwargs or 'max_count' in kwargs:
+ if multiple_create_request:
msg = ("Multiple pingable or sshable servers not supported at "
"this stage.")
raise ValueError(msg)
+ LOG.debug("Provisioning test server with validation resources %s",
+ validation_resources)
if 'security_groups' in kwargs:
kwargs['security_groups'].append(
{'name': validation_resources['security_group']['name']})
@@ -87,16 +150,29 @@
if wait_until is None:
wait_until = 'ACTIVE'
+ if 'user_data' not in kwargs:
+ # If nothing overrides the default user data script then run
+ # a simple script on the host to print networking info. This is
+ # to aid in debugging ssh failures.
+ script = '''
+ #!/bin/sh
+ echo "Printing {user} user authorized keys"
+ cat ~{user}/.ssh/authorized_keys || true
+ '''.format(user=CONF.validation.image_ssh_user)
+ script_clean = textwrap.dedent(script).lstrip().encode('utf8')
+ script_b64 = base64.b64encode(script_clean)
+ kwargs['user_data'] = script_b64
+
if volume_backed:
- volume_name = data_utils.rand_name('volume')
+ volume_name = data_utils.rand_name(__name__ + '-volume')
volumes_client = clients.volumes_v2_client
- if CONF.volume_feature_enabled.api_v1:
- volumes_client = clients.volumes_client
- volume = volumes_client.create_volume(
- display_name=volume_name,
- imageRef=image_id)
- volumes_client.wait_for_volume_status(volume['volume']['id'],
- 'available')
+ params = {'name': volume_name,
+ 'imageRef': image_id,
+ 'size': CONF.volume.volume_size}
+ volume = volumes_client.create_volume(**params)
+ waiters.wait_for_volume_resource_status(volumes_client,
+ volume['volume']['id'],
+ 'available')
bd_map_v2 = [{
'uuid': volume['volume']['id'],
@@ -115,19 +191,36 @@
**kwargs)
# handle the case of multiple servers
- servers = []
- if 'min_count' in kwargs or 'max_count' in kwargs:
+ if multiple_create_request:
# Get servers created which name match with name param.
body_servers = clients.servers_client.list_servers()
servers = \
[s for s in body_servers['servers'] if s['name'].startswith(name)]
else:
- body = service_client.ResponseBody(body.response, body['server'])
+ body = rest_client.ResponseBody(body.response, body['server'])
servers = [body]
- # The name of the method to associate a floating IP to as server is too
- # long for PEP8 compliance so:
- assoc = clients.compute_floating_ips_client.associate_floating_ip_to_server
+ def _setup_validation_fip():
+ if CONF.service_available.neutron:
+ ifaces = clients.interfaces_client.list_interfaces(server['id'])
+ validation_port = None
+ for iface in ifaces['interfaceAttachments']:
+ if iface['net_id'] == tenant_network['id']:
+ validation_port = iface['port_id']
+ break
+ if not validation_port:
+ # NOTE(artom) This will get caught by the catch-all clause in
+ # the wait_until loop below
+ raise ValueError('Unable to setup floating IP for validation: '
+ 'port not found on tenant network')
+ clients.floating_ips_client.update_floatingip(
+ validation_resources['floating_ip']['id'],
+ port_id=validation_port)
+ else:
+ fip_client = clients.compute_floating_ips_client
+ fip_client.associate_floating_ip_to_server(
+ floating_ip=validation_resources['floating_ip']['ip'],
+ server_id=servers[0]['id'])
if wait_until:
for server in servers:
@@ -136,23 +229,192 @@
clients.servers_client, server['id'], wait_until)
# Multiple validatable servers are not supported for now. Their
- # creation will fail with the condition above (l.58).
+ # creation will fail with the condition above.
if CONF.validation.run_validation and validatable:
if CONF.validation.connect_method == 'floating':
- assoc(floating_ip=validation_resources[
- 'floating_ip']['ip'],
- server_id=servers[0]['id'])
+ _setup_validation_fip()
except Exception:
with excutils.save_and_reraise_exception():
- if ('preserve_server_on_error' not in kwargs
- or kwargs['preserve_server_on_error'] is False):
- for server in servers:
- try:
- clients.servers_client.delete_server(
- server['id'])
- except Exception:
- LOG.exception('Deleting server %s failed'
- % server['id'])
+ for server in servers:
+ try:
+ clients.servers_client.delete_server(
+ server['id'])
+ except Exception:
+ LOG.exception('Deleting server %s failed',
+ server['id'])
+ for server in servers:
+ # NOTE(artom) If the servers were booted with volumes
+ # and with delete_on_termination=False we need to wait
+ # for the servers to go away before proceeding with
+ # cleanup, otherwise we'll attempt to delete the
+ # volumes while they're still attached to servers that
+ # are in the process of being deleted.
+ try:
+ waiters.wait_for_server_termination(
+ clients.servers_client, server['id'])
+ except Exception:
+ LOG.exception('Server %s failed to delete in time',
+ server['id'])
return body, servers
+
+
+def shelve_server(servers_client, server_id, force_shelve_offload=False):
+ """Common wrapper utility to shelve server.
+
+ This method is a common wrapper to make server in 'SHELVED'
+ or 'SHELVED_OFFLOADED' state.
+
+ :param servers_clients: Compute servers client instance.
+ :param server_id: Server to make in shelve state
+ :param force_shelve_offload: Forcefully offload shelve server if it
+ is configured not to offload server
+ automatically after offload time.
+ """
+ servers_client.shelve_server(server_id)
+
+ offload_time = CONF.compute.shelved_offload_time
+ if offload_time >= 0:
+ waiters.wait_for_server_status(servers_client, server_id,
+ 'SHELVED_OFFLOADED',
+ extra_timeout=offload_time)
+ else:
+ waiters.wait_for_server_status(servers_client, server_id, 'SHELVED')
+ if force_shelve_offload:
+ servers_client.shelve_offload_server(server_id)
+ waiters.wait_for_server_status(servers_client, server_id,
+ 'SHELVED_OFFLOADED')
+
+
+def create_websocket(url):
+ url = urlparse.urlparse(url)
+
+ # NOTE(mnaser): It is possible that there is no port specified, so fall
+ # back to the default port based on the scheme.
+ port = url.port or (443 if url.scheme == 'https' else 80)
+
+ for res in socket.getaddrinfo(url.hostname, port,
+ socket.AF_UNSPEC, socket.SOCK_STREAM):
+ af, socktype, proto, _, sa = res
+ client_socket = socket.socket(af, socktype, proto)
+ if url.scheme == 'https':
+ client_socket = ssl.wrap_socket(client_socket)
+ client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ try:
+ client_socket.connect(sa)
+ except socket.error:
+ client_socket.close()
+ continue
+ break
+ else:
+ raise socket.error('WebSocket creation failed')
+ # Turn the Socket into a WebSocket to do the communication
+ return _WebSocket(client_socket, url)
+
+
+class _WebSocket(object):
+ def __init__(self, client_socket, url):
+ """Contructor for the WebSocket wrapper to the socket."""
+ self._socket = client_socket
+ # cached stream for early frames.
+ self.cached_stream = b''
+ # Upgrade the HTTP connection to a WebSocket
+ self._upgrade(url)
+
+ def _recv(self, recv_size):
+ """Wrapper to receive data from the cached stream or socket."""
+ if recv_size <= 0:
+ return None
+
+ data_from_cached = b''
+ data_from_socket = b''
+ if len(self.cached_stream) > 0:
+ read_from_cached = min(len(self.cached_stream), recv_size)
+ data_from_cached += self.cached_stream[:read_from_cached]
+ self.cached_stream = self.cached_stream[read_from_cached:]
+ recv_size -= read_from_cached
+ if recv_size > 0:
+ data_from_socket = self._socket.recv(recv_size)
+ return data_from_cached + data_from_socket
+
+ def receive_frame(self):
+ """Wrapper for receiving data to parse the WebSocket frame format"""
+ # We need to loop until we either get some bytes back in the frame
+ # or no data was received (meaning the socket was closed). This is
+ # done to handle the case where we get back some empty frames
+ while True:
+ header = self._recv(2)
+ # If we didn't receive any data, just return None
+ if not header:
+ return None
+ # We will make the assumption that we are only dealing with
+ # frames less than 125 bytes here (for the negotiation) and
+ # that only the 2nd byte contains the length, and since the
+ # server doesn't do masking, we can just read the data length
+ if ord_func(header[1]) & 127 > 0:
+ return self._recv(ord_func(header[1]) & 127)
+
+ def send_frame(self, data):
+ """Wrapper for sending data to add in the WebSocket frame format."""
+ frame_bytes = list()
+ # For the first byte, want to say we are sending binary data (130)
+ frame_bytes.append(130)
+ # Only sending negotiation data so don't need to worry about > 125
+ # We do need to add the bit that says we are masking the data
+ frame_bytes.append(len(data) | 128)
+ # We don't really care about providing a random mask for security
+ # So we will just hard-code a value since a test program
+ mask = [7, 2, 1, 9]
+ for i in range(len(mask)):
+ frame_bytes.append(mask[i])
+ # Mask each of the actual data bytes that we are going to send
+ for i in range(len(data)):
+ frame_bytes.append(ord_func(data[i]) ^ mask[i % 4])
+ # Convert our integer list to a binary array of bytes
+ frame_bytes = struct.pack('!%iB' % len(frame_bytes), * frame_bytes)
+ self._socket.sendall(frame_bytes)
+
+ def close(self):
+ """Helper method to close the connection."""
+ # Close down the real socket connection and exit the test program
+ if self._socket is not None:
+ self._socket.shutdown(1)
+ self._socket.close()
+ self._socket = None
+
+ def _upgrade(self, url):
+ """Upgrade the HTTP connection to a WebSocket and verify."""
+ # The real request goes to the /websockify URI always
+ reqdata = 'GET /websockify HTTP/1.1\r\n'
+ reqdata += 'Host: %s' % url.hostname
+ # Add port only if we have one specified
+ if url.port:
+ reqdata += ':%s' % url.port
+ # Line-ending for Host header
+ reqdata += '\r\n'
+ # Tell the HTTP Server to Upgrade the connection to a WebSocket
+ reqdata += 'Upgrade: websocket\r\nConnection: Upgrade\r\n'
+ # The token=xxx is sent as a Cookie not in the URI
+ reqdata += 'Cookie: %s\r\n' % url.query
+ # Use a hard-coded WebSocket key since a test program
+ reqdata += 'Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n'
+ reqdata += 'Sec-WebSocket-Version: 13\r\n'
+ # We are choosing to use binary even though browser may do Base64
+ reqdata += 'Sec-WebSocket-Protocol: binary\r\n\r\n'
+ # Send the HTTP GET request and get the response back
+ self._socket.sendall(reqdata.encode('utf8'))
+ self.response = data = self._socket.recv(4096)
+ # Loop through & concatenate all of the data in the response body
+ end_loc = self.response.find(b'\r\n\r\n')
+ while data and end_loc < 0:
+ data = self._socket.recv(4096)
+ self.response += data
+ end_loc = self.response.find(b'\r\n\r\n')
+
+ if len(self.response) > end_loc + 4:
+ # In case some frames (e.g. the first RFP negotiation) have
+ # arrived, cache it for next reading.
+ self.cached_stream = self.response[end_loc + 4:]
+ # ensure response ends with '\r\n\r\n'.
+ self.response = self.response[:end_loc + 4]
diff --git a/tempest/common/cred_client.py b/tempest/common/cred_client.py
deleted file mode 100644
index 5d15988..0000000
--- a/tempest/common/cred_client.py
+++ /dev/null
@@ -1,172 +0,0 @@
-# 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 abc
-
-from oslo_log import log as logging
-import six
-from tempest_lib import auth
-from tempest_lib import exceptions as lib_exc
-
-from tempest.services.identity.v2.json import identity_client as v2_identity
-
-LOG = logging.getLogger(__name__)
-
-
-@six.add_metaclass(abc.ABCMeta)
-class CredsClient(object):
- """This class is a wrapper around the identity clients
-
- to provide a single interface for managing credentials in both v2 and v3
- cases. It's not bound to created credentials, only to a specific set of
- admin credentials used for generating credentials.
- """
-
- def __init__(self, identity_client, projects_client=None,
- roles_client=None):
- # The client implies version and credentials
- self.identity_client = identity_client
- # this is temporary until the v3 project client is
- # separated, then projects_client will become mandatory
- self.projects_client = projects_client or identity_client
- self.roles_client = roles_client or identity_client
-
- def create_user(self, username, password, project, email):
- user = self.identity_client.create_user(
- username, password, project['id'], email)
- if 'user' in user:
- user = user['user']
- return user
-
- @abc.abstractmethod
- def create_project(self, name, description):
- pass
-
- def _check_role_exists(self, role_name):
- try:
- roles = self._list_roles()
- role = next(r for r in roles if r['name'] == role_name)
- except StopIteration:
- return None
- return role
-
- def create_user_role(self, role_name):
- if not self._check_role_exists(role_name):
- self.roles_client.create_role(name=role_name)
-
- def assign_user_role(self, user, project, role_name):
- role = self._check_role_exists(role_name)
- if not role:
- msg = 'No "%s" role found' % role_name
- raise lib_exc.NotFound(msg)
- try:
- self.roles_client.assign_user_role(project['id'], user['id'],
- role['id'])
- except lib_exc.Conflict:
- LOG.debug("Role %s already assigned on project %s for user %s" % (
- role['id'], project['id'], user['id']))
-
- @abc.abstractmethod
- def get_credentials(self, user, project, password):
- """Produces a Credentials object from the details provided
-
- :param user: a user dict
- :param project: a project dict
- :param password: the password as a string
- :return: a Credentials object with all the available credential details
- """
- pass
-
- def delete_user(self, user_id):
- self.identity_client.delete_user(user_id)
-
- def _list_roles(self):
- roles = self.roles_client.list_roles()['roles']
- return roles
-
-
-class V2CredsClient(CredsClient):
-
- def __init__(self, identity_client, projects_client, roles_client):
- super(V2CredsClient, self).__init__(identity_client,
- projects_client,
- roles_client)
-
- def create_project(self, name, description):
- tenant = self.projects_client.create_tenant(
- name=name, description=description)['tenant']
- return tenant
-
- def get_credentials(self, user, project, password):
- # User and project already include both ID and name here,
- # so there's no need to use the fill_in mode
- return auth.get_credentials(
- auth_url=None,
- fill_in=False,
- identity_version='v2',
- username=user['name'], user_id=user['id'],
- tenant_name=project['name'], tenant_id=project['id'],
- password=password)
-
- def delete_project(self, project_id):
- self.projects_client.delete_tenant(project_id)
-
-
-class V3CredsClient(CredsClient):
-
- def __init__(self, identity_client, domain_name):
- super(V3CredsClient, self).__init__(identity_client)
- try:
- # Domain names must be unique, in any case a list is returned,
- # selecting the first (and only) element
- self.creds_domain = self.identity_client.list_domains(
- params={'name': domain_name})['domains'][0]
- except lib_exc.NotFound:
- # TODO(andrea) we could probably create the domain on the fly
- msg = "Requested domain %s could not be found" % domain_name
- raise lib_exc.InvalidCredentials(msg)
-
- def create_project(self, name, description):
- project = self.identity_client.create_project(
- name=name, description=description,
- domain_id=self.creds_domain['id'])['project']
- return project
-
- def get_credentials(self, user, project, password):
- # User, project and domain already include both ID and name here,
- # so there's no need to use the fill_in mode.
- return auth.get_credentials(
- auth_url=None,
- fill_in=False,
- identity_version='v3',
- username=user['name'], user_id=user['id'],
- project_name=project['name'], project_id=project['id'],
- password=password,
- project_domain_id=self.creds_domain['id'],
- project_domain_name=self.creds_domain['name'])
-
- def delete_project(self, project_id):
- self.identity_client.delete_project(project_id)
-
- def _list_roles(self):
- roles = self.identity_client.list_roles()['roles']
- return roles
-
-
-def get_creds_client(identity_client,
- projects_client=None,
- roles_client=None,
- project_domain_name=None):
- if isinstance(identity_client, v2_identity.IdentityClient):
- return V2CredsClient(identity_client, projects_client, roles_client)
- else:
- return V3CredsClient(identity_client, project_domain_name)
diff --git a/tempest/common/cred_provider.py b/tempest/common/cred_provider.py
deleted file mode 100644
index aa237e0..0000000
--- a/tempest/common/cred_provider.py
+++ /dev/null
@@ -1,101 +0,0 @@
-# Copyright (c) 2014 Deutsche Telekom AG
-# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
-# 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 abc
-
-from oslo_log import log as logging
-import six
-from tempest_lib import auth
-
-from tempest import exceptions
-
-LOG = logging.getLogger(__name__)
-
-
-@six.add_metaclass(abc.ABCMeta)
-class CredentialProvider(object):
- def __init__(self, identity_version, name=None, network_resources=None,
- credentials_domain=None, admin_role=None):
- """A CredentialProvider supplies credentials to test classes.
-
- :param identity_version: Identity version of the credentials provided
- :param name: Name of the calling test. Included in provisioned
- credentials when credentials are provisioned on the fly
- :param network_resources: Network resources required for the
- credentials
- :param credentials_domain: Domain credentials belong to
- :param admin_role: Name of the role of the admin account
- """
- self.identity_version = identity_version
- self.name = name or "test_creds"
- self.network_resources = network_resources
- self.credentials_domain = credentials_domain or 'Default'
- self.admin_role = admin_role
- if not auth.is_identity_version_supported(self.identity_version):
- raise exceptions.InvalidIdentityVersion(
- identity_version=self.identity_version)
-
- @abc.abstractmethod
- def get_primary_creds(self):
- return
-
- @abc.abstractmethod
- def get_admin_creds(self):
- return
-
- @abc.abstractmethod
- def get_alt_creds(self):
- return
-
- @abc.abstractmethod
- def clear_creds(self):
- return
-
- @abc.abstractmethod
- def is_multi_user(self):
- return
-
- @abc.abstractmethod
- def is_multi_tenant(self):
- return
-
- @abc.abstractmethod
- def get_creds_by_roles(self, roles, force_new=False):
- return
-
- @abc.abstractmethod
- def is_role_available(self, role):
- return
-
-
-class TestResources(object):
- """Readonly Credentials, with network resources added."""
-
- def __init__(self, credentials):
- self._credentials = credentials
- self.network = None
- self.subnet = None
- self.router = None
-
- def __getattr__(self, item):
- return getattr(self._credentials, item)
-
- def set_resources(self, **kwargs):
- for key in kwargs.keys():
- if hasattr(self, key):
- setattr(self, key, kwargs[key])
-
- @property
- def credentials(self):
- return self._credentials
diff --git a/tempest/common/credentials_factory.py b/tempest/common/credentials_factory.py
index 5d290d4..75db155 100644
--- a/tempest/common/credentials_factory.py
+++ b/tempest/common/credentials_factory.py
@@ -12,23 +12,20 @@
# limitations under the License.
from oslo_concurrency import lockutils
-from oslo_log import log as logging
-from tempest_lib import auth
from tempest import clients
-from tempest.common import cred_provider
-from tempest.common import dynamic_creds
-from tempest.common import preprov_creds
from tempest import config
-from tempest import exceptions
+from tempest.lib import auth
+from tempest.lib.common import dynamic_creds
+from tempest.lib.common import preprov_creds
+from tempest.lib import exceptions
CONF = config.CONF
-LOG = logging.getLogger(__name__)
"""This module provides factories of credential and credential providers
-Credentials providers and clients are (going to be) part of tempest-lib,
+Credentials providers and clients are (going to be) part of tempest.lib,
and so they may not hold any dependency to tempest configuration.
Methods in this module collect the relevant configuration details and pass
@@ -42,19 +39,69 @@
# Subset of the parameters of credential providers that depend on configuration
-def _get_common_provider_params():
+def _get_common_provider_params(identity_version):
+ if identity_version == 'v3':
+ identity_uri = CONF.identity.uri_v3
+ elif identity_version == 'v2':
+ identity_uri = CONF.identity.uri
+ else:
+ raise exceptions.InvalidIdentityVersion(
+ identity_version=identity_version)
return {
+ 'identity_version': identity_version,
+ 'identity_uri': identity_uri,
'credentials_domain': CONF.auth.default_credentials_domain_name,
'admin_role': CONF.identity.admin_role
}
-def _get_dynamic_provider_params():
- return _get_common_provider_params()
+def get_dynamic_provider_params(identity_version, admin_creds=None):
+ """Dynamic provider parameters setup from config
+
+ This helper returns a dict of parameter that can be used to initialise
+ a `DynamicCredentialProvider` according to tempest configuration.
+ Parameters that are not configuration specific (name, network_resources)
+ are not returned.
+
+ :param identity_version: 'v2' or 'v3'
+ :param admin_creds: An object of type `auth.Credentials`. If None, it
+ is built from the configuration file as well.
+ :return: A dict with the parameters
+ """
+ _common_params = _get_common_provider_params(identity_version)
+ admin_creds = admin_creds or get_configured_admin_credentials(
+ fill_in=True, identity_version=identity_version)
+ if identity_version == 'v3':
+ endpoint_type = CONF.identity.v3_endpoint_type
+ elif identity_version == 'v2':
+ endpoint_type = CONF.identity.v2_admin_endpoint_type
+ return dict(_common_params, **dict([
+ ('admin_creds', admin_creds),
+ ('identity_admin_domain_scope', CONF.identity.admin_domain_scope),
+ ('identity_admin_role', CONF.identity.admin_role),
+ ('extra_roles', CONF.auth.tempest_roles),
+ ('neutron_available', CONF.service_available.neutron),
+ ('project_network_cidr', CONF.network.project_network_cidr),
+ ('project_network_mask_bits', CONF.network.project_network_mask_bits),
+ ('public_network_id', CONF.network.public_network_id),
+ ('create_networks', (CONF.auth.create_isolated_networks and not
+ CONF.network.shared_physical_network)),
+ ('resource_prefix', 'tempest'),
+ ('identity_admin_endpoint_type', endpoint_type)
+ ]))
-def _get_preprov_provider_params():
- _common_params = _get_common_provider_params()
+def get_preprov_provider_params(identity_version):
+ """Pre-provisioned provider parameters setup from config
+
+ This helper returns a dict of parameter that can be used to initialise
+ a `PreProvisionedCredentialProvider` according to tempest configuration.
+ Parameters that are not configuration specific (name) are not returned.
+
+ :param identity_version: 'v2' or 'v3'
+ :return: A dict with the parameters
+ """
+ _common_params = _get_common_provider_params(identity_version)
reseller_admin_role = CONF.object_storage.reseller_admin_role
return dict(_common_params, **dict([
('accounts_lock_dir', lockutils.get_lock_path(CONF)),
@@ -64,127 +111,55 @@
]))
-class LegacyCredentialProvider(cred_provider.CredentialProvider):
-
- def __init__(self, identity_version):
- """Credentials provider which returns credentials from tempest.conf
-
- Credentials provider which always returns the first and second
- configured accounts as primary and alt users.
- Credentials from tempest.conf are deprecated, and this credential
- provider is also accordingly.
-
- This credential provider can be used in case of serial test execution
- to preserve the current behaviour of the serial tempest run.
-
- :param identity_version: Version of the identity API
- :return: CredentialProvider
- """
- super(LegacyCredentialProvider, self).__init__(
- identity_version=identity_version)
- self._creds = {}
-
- def _unique_creds(self, cred_arg=None):
- """Verify that the configured credentials are valid and distinct """
- try:
- user = self.get_primary_creds()
- alt_user = self.get_alt_creds()
- return getattr(user, cred_arg) != getattr(alt_user, cred_arg)
- except exceptions.InvalidCredentials as ic:
- msg = "At least one of the configured credentials is " \
- "not valid: %s" % ic.message
- raise exceptions.InvalidConfiguration(msg)
-
- def is_multi_user(self):
- return self._unique_creds('username')
-
- def is_multi_tenant(self):
- return self._unique_creds('tenant_id')
-
- def get_primary_creds(self):
- if self._creds.get('primary'):
- return self._creds.get('primary')
- primary_credential = get_configured_credentials(
- credential_type='user', fill_in=False,
- identity_version=self.identity_version)
- self._creds['primary'] = cred_provider.TestResources(
- primary_credential)
- return self._creds['primary']
-
- def get_alt_creds(self):
- if self._creds.get('alt'):
- return self._creds.get('alt')
- alt_credential = get_configured_credentials(
- credential_type='alt_user', fill_in=False,
- identity_version=self.identity_version)
- self._creds['alt'] = cred_provider.TestResources(
- alt_credential)
- return self._creds['alt']
-
- def clear_creds(self):
- self._creds = {}
-
- def get_admin_creds(self):
- if self._creds.get('admin'):
- return self._creds.get('admin')
- creds = get_configured_credentials(
- "identity_admin", fill_in=False)
- self._creds['admin'] = cred_provider.TestResources(creds)
- return self._creds['admin']
-
- def get_creds_by_roles(self, roles, force_new=False):
- msg = "Credentials being specified through the config file can not be"\
- " used with tests that specify using credentials by roles. "\
- "Either exclude/skip the tests doing this or use either an "\
- "test_accounts_file or dynamic credentials."
- raise exceptions.InvalidConfiguration(msg)
-
- def is_role_available(self, role):
- # NOTE(andreaf) LegacyCredentialProvider does not support credentials
- # by role, so returning always False.
- # Test that rely on credentials by role should use this to skip
- # when this is credential provider is used
- return False
-
-
-# Return the right implementation of CredentialProvider based on config
-# Dropping interface and password, as they are never used anyways
-# TODO(andreaf) Drop them from the CredentialsProvider interface completely
def get_credentials_provider(name, network_resources=None,
force_tenant_isolation=False,
identity_version=None):
+ """Return the right implementation of CredentialProvider based on config
+
+ This helper returns the right implementation of CredentialProvider based on
+ config and on the value of force_tenant_isolation.
+
+ :param name: When provided, it makes it possible to associate credential
+ artifacts back to the owner (test class).
+ :param network_resources: Dictionary of network resources to be allocated
+ for each test account. Only valid for the dynamic
+ credentials provider.
+ :param force_tenant_isolation: Always return a `DynamicCredentialProvider`,
+ regardless of the configuration.
+ :param identity_version: Use the specified identity API version, regardless
+ of the configuration. Valid values are 'v2', 'v3'.
+ """
# If a test requires a new account to work, it can have it via forcing
# dynamic credentials. A new account will be produced only for that test.
# In case admin credentials are not available for the account creation,
# the test should be skipped else it would fail.
identity_version = identity_version or CONF.identity.auth_version
if CONF.auth.use_dynamic_credentials or force_tenant_isolation:
- admin_creds = get_configured_credentials(
- 'identity_admin', fill_in=True, identity_version=identity_version)
return dynamic_creds.DynamicCredentialProvider(
name=name,
network_resources=network_resources,
- identity_version=identity_version,
- admin_creds=admin_creds,
- **_get_dynamic_provider_params())
+ **get_dynamic_provider_params(identity_version))
else:
if CONF.auth.test_accounts_file:
# Most params are not relevant for pre-created accounts
return preprov_creds.PreProvisionedCredentialProvider(
- name=name, identity_version=identity_version,
- **_get_preprov_provider_params())
+ name=name,
+ **get_preprov_provider_params(identity_version))
else:
- # Dynamic credentials are disabled, and the account file is not
- # defined - we fall back on credentials configured in tempest.conf
- return LegacyCredentialProvider(identity_version=identity_version)
+ raise exceptions.InvalidConfiguration(
+ 'A valid credential provider is needed')
-# We want a helper function here to check and see if admin credentials
-# are available so we can do a single call from skip_checks if admin
-# creds area available.
-# This depends on identity_version as there may be admin credentials
-# available for v2 but not for v3.
def is_admin_available(identity_version):
+ """Helper to check for admin credentials
+
+ Helper function to check if a set of admin credentials is available so we
+ can do a single call from skip_checks.
+ This helper depends on identity_version as there may be admin credentials
+ available for v2 but not for v3.
+
+ :param identity_version: 'v2' or 'v3'
+ """
is_admin = True
# If dynamic credentials is enabled admin will be available
if CONF.auth.use_dynamic_credentials:
@@ -192,35 +167,41 @@
# Check whether test accounts file has the admin specified or not
elif CONF.auth.test_accounts_file:
check_accounts = preprov_creds.PreProvisionedCredentialProvider(
- identity_version=identity_version, name='check_admin',
- **_get_preprov_provider_params())
+ name='check_admin',
+ **get_preprov_provider_params(identity_version))
if not check_accounts.admin_available():
is_admin = False
else:
try:
- get_configured_credentials('identity_admin', fill_in=False,
- identity_version=identity_version)
+ get_configured_admin_credentials(fill_in=False,
+ identity_version=identity_version)
except exceptions.InvalidConfiguration:
is_admin = False
return is_admin
-# We want a helper function here to check and see if alt credentials
-# are available so we can do a single call from skip_checks if alt
-# creds area available.
-# This depends on identity_version as there may be alt credentials
-# available for v2 but not for v3.
def is_alt_available(identity_version):
+ """Helper to check for alt credentials
+
+ Helper function to check if a second set of credentials is available (aka
+ alt credentials) so we can do a single call from skip_checks.
+ This helper depends on identity_version as there may be alt credentials
+ available for v2 but not for v3.
+
+ :param identity_version: 'v2' or 'v3'
+ """
# If dynamic credentials is enabled alt will be available
if CONF.auth.use_dynamic_credentials:
return True
# Check whether test accounts file has the admin specified or not
if CONF.auth.test_accounts_file:
check_accounts = preprov_creds.PreProvisionedCredentialProvider(
- identity_version=identity_version, name='check_alt',
- **_get_preprov_provider_params())
+ name='check_alt',
+ **get_preprov_provider_params(identity_version))
else:
- check_accounts = LegacyCredentialProvider(identity_version)
+ raise exceptions.InvalidConfiguration(
+ 'A valid credential provider is needed')
+
try:
if not check_accounts.is_multi_user():
return False
@@ -238,64 +219,75 @@
'alt_user': ('identity', 'alt')
}
-DEFAULT_PARAMS = {
- 'disable_ssl_certificate_validation':
- CONF.identity.disable_ssl_certificate_validation,
- 'ca_certs': CONF.identity.ca_certificates_file,
- 'trace_requests': CONF.debug.trace_requests
-}
+def get_configured_admin_credentials(fill_in=True, identity_version=None):
+ """Get admin credentials from the config file
-# Read credentials from configuration, builds a Credentials object
-# based on the specified or configured version
-def get_configured_credentials(credential_type, fill_in=True,
- identity_version=None):
+ Read credentials from configuration, builds a Credentials object based on
+ the specified or configured version
+
+ :param fill_in: If True, a request to the Token API is submitted, and the
+ credential object is filled in with all names and IDs from
+ the token API response.
+ :param identity_version: The identity version to talk to and the type of
+ credentials object to be created. 'v2' or 'v3'.
+ :returns: An object of a sub-type of `auth.Credentials`
+ """
identity_version = identity_version or CONF.identity.auth_version
if identity_version not in ('v2', 'v3'):
raise exceptions.InvalidConfiguration(
'Unsupported auth version: %s' % identity_version)
- if credential_type not in CREDENTIAL_TYPES:
- raise exceptions.InvalidCredentials()
- conf_attributes = ['username', 'password', 'tenant_name']
+ conf_attributes = ['username', 'password',
+ 'project_name']
if identity_version == 'v3':
conf_attributes.append('domain_name')
# Read the parts of credentials from config
- params = DEFAULT_PARAMS.copy()
- section, prefix = CREDENTIAL_TYPES[credential_type]
+ params = config.service_client_config()
for attr in conf_attributes:
- _section = getattr(CONF, section)
- if prefix is None:
- params[attr] = getattr(_section, attr)
- else:
- params[attr] = getattr(_section, prefix + "_" + attr)
+ params[attr] = getattr(CONF.auth, 'admin_' + attr)
# Build and validate credentials. We are reading configured credentials,
# so validate them even if fill_in is False
credentials = get_credentials(fill_in=fill_in,
identity_version=identity_version, **params)
if not fill_in:
if not credentials.is_valid():
- msg = ("The %s credentials are incorrectly set in the config file."
- " Double check that all required values are assigned" %
- credential_type)
- raise exceptions.InvalidConfiguration(msg)
+ msg = ("The admin credentials are incorrectly set in the config "
+ "file for identity version %s. Double check that all "
+ "required values are assigned.")
+ raise exceptions.InvalidConfiguration(msg % identity_version)
return credentials
-# Wrapper around auth.get_credentials to use the configured identity version
-# is none is specified
def get_credentials(fill_in=True, identity_version=None, **kwargs):
- params = dict(DEFAULT_PARAMS, **kwargs)
+ """Get credentials from dict based on config
+
+ Wrapper around auth.get_credentials to use the configured identity version
+ if none is specified.
+
+ :param fill_in: If True, a request to the Token API is submitted, and the
+ credential object is filled in with all names and IDs from
+ the token API response.
+ :param identity_version: The identity version to talk to and the type of
+ credentials object to be created. 'v2' or 'v3'.
+ :param kwargs: Attributes to be used to build the Credentials object.
+ :returns: An object of a sub-type of `auth.Credentials`
+ """
+ params = dict(config.service_client_config(), **kwargs)
identity_version = identity_version or CONF.identity.auth_version
# In case of "v3" add the domain from config if not specified
+ # To honour the "default_credentials_domain_name", if not domain
+ # field is specified at all, add it the credential dict.
if identity_version == 'v3':
domain_fields = set(x for x in auth.KeystoneV3Credentials.ATTRIBUTES
if 'domain' in x)
if not domain_fields.intersection(kwargs.keys()):
domain_name = CONF.auth.default_credentials_domain_name
- params['user_domain_name'] = domain_name
+ # NOTE(andreaf) Setting domain_name implicitly sets user and
+ # project domain names, if they are None
+ params['domain_name'] = domain_name
auth_url = CONF.identity.uri_v3
else:
@@ -308,19 +300,9 @@
# === Credential / client managers
-class ConfiguredUserManager(clients.Manager):
- """Manager that uses user credentials for its managed client objects"""
-
- def __init__(self, service=None):
- super(ConfiguredUserManager, self).__init__(
- credentials=get_configured_credentials('user'),
- service=service)
-
-
class AdminManager(clients.Manager):
"""Manager that uses admin credentials for its managed client objects"""
- def __init__(self, service=None):
+ def __init__(self):
super(AdminManager, self).__init__(
- credentials=get_configured_credentials('identity_admin'),
- service=service)
+ credentials=get_configured_admin_credentials())
diff --git a/tempest/common/custom_matchers.py b/tempest/common/custom_matchers.py
index 8ba33ed..ed11b21 100644
--- a/tempest/common/custom_matchers.py
+++ b/tempest/common/custom_matchers.py
@@ -14,7 +14,6 @@
import re
-import six
from testtools import helpers
@@ -28,7 +27,7 @@
checked in each test code.
"""
- def __init__(self, target, method):
+ def __init__(self, target, method, policies=None):
"""Initialization of ExistsAllResponseHeaders
param: target Account/Container/Object
@@ -36,14 +35,34 @@
"""
self.target = target
self.method = method
+ self.policies = policies or []
+
+ def _content_length_required(self, resp):
+ # Verify whether given HTTP response must contain content-length.
+ # Take into account the exceptions defined in RFC 7230.
+ if resp.status in range(100, 200) or resp.status == 204:
+ return False
+
+ return True
def match(self, actual):
"""Check headers
- param: actual HTTP response headers
+ param: actual HTTP response object containing headers and status
"""
- # Check common headers for all HTTP methods
- if 'content-length' not in actual:
+ # Check common headers for all HTTP methods.
+ #
+ # Please note that for 1xx and 204 responses Content-Length presence
+ # is not checked intensionally. According to RFC 7230 a server MUST
+ # NOT send the header in such responses. Thus, clients should not
+ # depend on this header. However, the standard does not require them
+ # to validate the server's behavior. We leverage that to not refuse
+ # any implementation violating it like Swift [1] or some versions of
+ # Ceph RadosGW [2].
+ # [1] https://bugs.launchpad.net/swift/+bug/1537811
+ # [2] http://tracker.ceph.com/issues/13582
+ if ('content-length' not in actual and
+ self._content_length_required(actual)):
return NonExistentHeader('content-length')
if 'content-type' not in actual:
return NonExistentHeader('content-type')
@@ -65,11 +84,63 @@
return NonExistentHeader('x-account-container-count')
if 'x-account-object-count' not in actual:
return NonExistentHeader('x-account-object-count')
+ if int(actual['x-account-container-count']) > 0:
+ acct_header = "x-account-storage-policy-"
+ matched_policy_count = 0
+
+ # Loop through the policies and look for account
+ # usage data. There should be at least 1 set
+ for policy in self.policies:
+ front_header = acct_header + policy['name'].lower()
+
+ usage_policies = [
+ front_header + '-bytes-used',
+ front_header + '-object-count',
+ front_header + '-container-count'
+ ]
+
+ # There should be 3 usage values for a give storage
+ # policy in an account bytes, object count, and
+ # container count
+ policy_hdrs = sum(1 for use_hdr in usage_policies
+ if use_hdr in actual)
+
+ # If there are less than 3 headers here then 1 is
+ # missing, let's figure out which one and report
+ if policy_hdrs == 3:
+ matched_policy_count = matched_policy_count + 1
+ else:
+ if policy_hdrs > 0 and policy_hdrs < 3:
+ for use_hdr in usage_policies:
+ if use_hdr not in actual:
+ return NonExistentHeader(use_hdr)
+
+ # Only flag an error if actual policies have been read and
+ # no usage has been found
+ if self.policies and matched_policy_count == 0:
+ return GenericError("No storage policy usage headers")
+
elif self.target == 'Container':
if 'x-container-bytes-used' not in actual:
return NonExistentHeader('x-container-bytes-used')
if 'x-container-object-count' not in actual:
return NonExistentHeader('x-container-object-count')
+ if 'x-storage-policy' not in actual:
+ return NonExistentHeader('x-storage-policy')
+ else:
+ policy_name = actual['x-storage-policy']
+
+ # loop through the policies and ensure that
+ # the value in the container header matches
+ # one of the storage policies
+ for policy in self.policies:
+ if policy['name'] == policy_name:
+ break
+ else:
+ # Ensure that there are actual policies stored
+ if self.policies:
+ return InvalidHeaderValue('x-storage-policy',
+ policy_name)
elif self.target == 'Object':
if 'etag' not in actual:
return NonExistentHeader('etag')
@@ -95,6 +166,19 @@
return None
+class GenericError(object):
+ """Informs an error message of a generic error during header evaluation"""
+
+ def __init__(self, body):
+ self.body = body
+
+ def describe(self):
+ return "%s" % self.body
+
+ def get_details(self):
+ return {}
+
+
class NonExistentHeader(object):
"""Informs an error message in the case of missing a certain header"""
@@ -108,6 +192,20 @@
return {}
+class InvalidHeaderValue(object):
+ """Informs an error message when a header contains a bad value"""
+
+ def __init__(self, header, value):
+ self.header = header
+ self.value = value
+
+ def describe(self):
+ return "InvalidValue (%s, %s)" % (self.header, self.value)
+
+ def get_details(self):
+ return {}
+
+
class AreAllWellFormatted(object):
"""Specific matcher to check the correctness of formats of values
@@ -118,7 +216,7 @@
"""
def match(self, actual):
- for key, value in six.iteritems(actual):
+ for key, value in actual.items():
if key in ('content-length', 'x-account-bytes-used',
'x-account-container-count', 'x-account-object-count',
'x-container-bytes-used', 'x-container-object-count')\
diff --git a/tempest/common/dynamic_creds.py b/tempest/common/dynamic_creds.py
deleted file mode 100644
index 0af07f0..0000000
--- a/tempest/common/dynamic_creds.py
+++ /dev/null
@@ -1,375 +0,0 @@
-# Copyright 2013 IBM Corp.
-#
-# 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 netaddr
-from oslo_log import log as logging
-import six
-from tempest_lib import exceptions as lib_exc
-
-from tempest import clients
-from tempest.common import cred_client
-from tempest.common import cred_provider
-from tempest.common.utils import data_utils
-from tempest import config
-from tempest import exceptions
-
-CONF = config.CONF
-LOG = logging.getLogger(__name__)
-
-
-class DynamicCredentialProvider(cred_provider.CredentialProvider):
-
- def __init__(self, identity_version, name=None, network_resources=None,
- credentials_domain=None, admin_role=None, admin_creds=None):
- """Creates credentials dynamically for tests
-
- A credential provider that, based on an initial set of
- admin credentials, creates new credentials on the fly for
- tests to use and then discard.
-
- :param str identity_version: identity API version to use `v2` or `v3`
- :param str admin_role: name of the admin role added to admin users
- :param str name: names of dynamic resources include this parameter
- when specified
- :param str credentials_domain: name of the domain where the users
- are created. If not defined, the project
- domain from admin_credentials is used
- :param dict network_resources: network resources to be created for
- the created credentials
- :param Credentials admin_creds: initial admin credentials
- """
- super(DynamicCredentialProvider, self).__init__(
- identity_version=identity_version, admin_role=admin_role,
- name=name, credentials_domain=credentials_domain,
- network_resources=network_resources)
- self.network_resources = network_resources
- self._creds = {}
- self.ports = []
- self.default_admin_creds = admin_creds
- (self.identity_admin_client, self.tenants_admin_client,
- self.roles_admin_client,
- self.network_admin_client,
- self.networks_admin_client,
- self.subnets_admin_client,
- self.ports_admin_client,
- self.security_groups_admin_client) = self._get_admin_clients()
- # Domain where isolated credentials are provisioned (v3 only).
- # Use that of the admin account is None is configured.
- self.creds_domain_name = None
- if self.identity_version == 'v3':
- self.creds_domain_name = (
- self.default_admin_creds.project_domain_name or
- self.credentials_domain)
- self.creds_client = cred_client.get_creds_client(
- self.identity_admin_client,
- self.tenants_admin_client,
- self.roles_admin_client,
- self.creds_domain_name)
-
- def _get_admin_clients(self):
- """Returns a tuple with instances of the following admin clients
-
- (in this order):
- identity
- network
- """
- os = clients.Manager(self.default_admin_creds)
- if self.identity_version == 'v2':
- return (os.identity_client, os.tenants_client, os.roles_client,
- os.network_client, os.networks_client, os.subnets_client,
- os.ports_client, os.security_groups_client)
- else:
- return (os.identity_v3_client, None, None, os.network_client,
- os.networks_client, os.subnets_client, os.ports_client,
- os.security_groups_client)
-
- def _create_creds(self, suffix="", admin=False, roles=None):
- """Create random credentials under the following schema.
-
- If the name contains a '.' is the full class path of something, and
- we don't really care. If it isn't, it's probably a meaningful name,
- so use it.
-
- For logging purposes, -user and -tenant are long and redundant,
- don't use them. The user# will be sufficient to figure it out.
- """
- if '.' in self.name:
- root = ""
- else:
- root = self.name
-
- project_name = data_utils.rand_name(root) + suffix
- project_desc = project_name + "-desc"
- project = self.creds_client.create_project(
- name=project_name, description=project_desc)
-
- username = data_utils.rand_name(root) + suffix
- user_password = data_utils.rand_password()
- email = data_utils.rand_name(root) + suffix + "@example.com"
- user = self.creds_client.create_user(
- username, user_password, project, email)
- if 'user' in user:
- user = user['user']
- role_assigned = False
- if admin:
- self.creds_client.assign_user_role(user, project,
- self.admin_role)
- role_assigned = True
- # Add roles specified in config file
- for conf_role in CONF.auth.tempest_roles:
- self.creds_client.assign_user_role(user, project, conf_role)
- role_assigned = True
- # Add roles requested by caller
- if roles:
- for role in roles:
- self.creds_client.assign_user_role(user, project, role)
- role_assigned = True
- # NOTE(mtreinish) For a user to have access to a project with v3 auth
- # it must beassigned a role on the project. So we need to ensure that
- # our newly created user has a role on the newly created project.
- if self.identity_version == 'v3' and not role_assigned:
- self.creds_client.create_user_role('Member')
- self.creds_client.assign_user_role(user, project, 'Member')
-
- creds = self.creds_client.get_credentials(user, project, user_password)
- return cred_provider.TestResources(creds)
-
- def _create_network_resources(self, tenant_id):
- network = None
- subnet = None
- router = None
- # Make sure settings
- if self.network_resources:
- if self.network_resources['router']:
- if (not self.network_resources['subnet'] or
- not self.network_resources['network']):
- raise exceptions.InvalidConfiguration(
- 'A router requires a subnet and network')
- elif self.network_resources['subnet']:
- if not self.network_resources['network']:
- raise exceptions.InvalidConfiguration(
- 'A subnet requires a network')
- elif self.network_resources['dhcp']:
- raise exceptions.InvalidConfiguration('DHCP requires a subnet')
-
- data_utils.rand_name_root = data_utils.rand_name(self.name)
- if not self.network_resources or self.network_resources['network']:
- network_name = data_utils.rand_name_root + "-network"
- network = self._create_network(network_name, tenant_id)
- try:
- if not self.network_resources or self.network_resources['subnet']:
- subnet_name = data_utils.rand_name_root + "-subnet"
- subnet = self._create_subnet(subnet_name, tenant_id,
- network['id'])
- if not self.network_resources or self.network_resources['router']:
- router_name = data_utils.rand_name_root + "-router"
- router = self._create_router(router_name, tenant_id)
- self._add_router_interface(router['id'], subnet['id'])
- except Exception:
- if router:
- self._clear_isolated_router(router['id'], router['name'])
- if subnet:
- self._clear_isolated_subnet(subnet['id'], subnet['name'])
- if network:
- self._clear_isolated_network(network['id'], network['name'])
- raise
- return network, subnet, router
-
- def _create_network(self, name, tenant_id):
- resp_body = self.networks_admin_client.create_network(
- name=name, tenant_id=tenant_id)
- return resp_body['network']
-
- def _create_subnet(self, subnet_name, tenant_id, network_id):
- base_cidr = netaddr.IPNetwork(CONF.network.tenant_network_cidr)
- mask_bits = CONF.network.tenant_network_mask_bits
- for subnet_cidr in base_cidr.subnet(mask_bits):
- try:
- if self.network_resources:
- resp_body = self.subnets_admin_client.\
- create_subnet(
- network_id=network_id, cidr=str(subnet_cidr),
- name=subnet_name,
- tenant_id=tenant_id,
- enable_dhcp=self.network_resources['dhcp'],
- ip_version=4)
- else:
- resp_body = self.subnets_admin_client.\
- create_subnet(network_id=network_id,
- cidr=str(subnet_cidr),
- name=subnet_name,
- tenant_id=tenant_id,
- ip_version=4)
- break
- except lib_exc.BadRequest as e:
- if 'overlaps with another subnet' not in str(e):
- raise
- else:
- message = 'Available CIDR for subnet creation could not be found'
- raise Exception(message)
- return resp_body['subnet']
-
- def _create_router(self, router_name, tenant_id):
- external_net_id = dict(
- network_id=CONF.network.public_network_id)
- resp_body = self.network_admin_client.create_router(
- router_name,
- external_gateway_info=external_net_id,
- tenant_id=tenant_id)
- return resp_body['router']
-
- def _add_router_interface(self, router_id, subnet_id):
- self.network_admin_client.add_router_interface_with_subnet_id(
- router_id, subnet_id)
-
- def get_credentials(self, credential_type):
- if self._creds.get(str(credential_type)):
- credentials = self._creds[str(credential_type)]
- else:
- if credential_type in ['primary', 'alt', 'admin']:
- is_admin = (credential_type == 'admin')
- credentials = self._create_creds(admin=is_admin)
- else:
- 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 (CONF.service_available.neutron and
- not CONF.baremetal.driver_enabled and
- CONF.auth.create_isolated_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)
- return credentials
-
- def get_primary_creds(self):
- return self.get_credentials('primary')
-
- def get_admin_creds(self):
- return self.get_credentials('admin')
-
- def get_alt_creds(self):
- return self.get_credentials('alt')
-
- def get_creds_by_roles(self, roles, force_new=False):
- roles = list(set(roles))
- # The roles list as a str will become the index as the dict key for
- # the created credentials set in the dynamic_creds dict.
- exist_creds = self._creds.get(str(roles))
- # If force_new flag is True 2 cred sets with the same roles are needed
- # handle this by creating a separate index for old one to store it
- # separately for cleanup
- if exist_creds and force_new:
- new_index = str(roles) + '-' + str(len(self._creds))
- self._creds[new_index] = exist_creds
- del self._creds[str(roles)]
- return self.get_credentials(roles)
-
- def _clear_isolated_router(self, router_id, router_name):
- net_client = self.network_admin_client
- try:
- net_client.delete_router(router_id)
- except lib_exc.NotFound:
- LOG.warning('router with name: %s not found for delete' %
- router_name)
-
- def _clear_isolated_subnet(self, subnet_id, subnet_name):
- client = self.subnets_admin_client
- try:
- client.delete_subnet(subnet_id)
- except lib_exc.NotFound:
- LOG.warning('subnet with name: %s not found for delete' %
- subnet_name)
-
- def _clear_isolated_network(self, network_id, network_name):
- net_client = self.networks_admin_client
- try:
- net_client.delete_network(network_id)
- except lib_exc.NotFound:
- LOG.warning('network with name: %s not found for delete' %
- network_name)
-
- def _cleanup_default_secgroup(self, tenant):
- nsg_client = self.security_groups_admin_client
- resp_body = nsg_client.list_security_groups(tenant_id=tenant,
- name="default")
- secgroups_to_delete = resp_body['security_groups']
- for secgroup in secgroups_to_delete:
- try:
- nsg_client.delete_security_group(secgroup['id'])
- except lib_exc.NotFound:
- LOG.warning('Security group %s, id %s not found for clean-up' %
- (secgroup['name'], secgroup['id']))
-
- def _clear_isolated_net_resources(self):
- net_client = self.network_admin_client
- for cred in self._creds:
- creds = self._creds.get(cred)
- if (not creds or not any([creds.router, creds.network,
- creds.subnet])):
- continue
- LOG.debug("Clearing network: %(network)s, "
- "subnet: %(subnet)s, router: %(router)s",
- {'network': creds.network, 'subnet': creds.subnet,
- 'router': creds.router})
- if (not self.network_resources or
- (self.network_resources.get('router') and creds.subnet)):
- try:
- net_client.remove_router_interface_with_subnet_id(
- creds.router['id'], creds.subnet['id'])
- except lib_exc.NotFound:
- LOG.warning('router with name: %s not found for delete' %
- creds.router['name'])
- self._clear_isolated_router(creds.router['id'],
- creds.router['name'])
- if (not self.network_resources or
- self.network_resources.get('subnet')):
- self._clear_isolated_subnet(creds.subnet['id'],
- creds.subnet['name'])
- if (not self.network_resources or
- self.network_resources.get('network')):
- self._clear_isolated_network(creds.network['id'],
- creds.network['name'])
-
- def clear_creds(self):
- if not self._creds:
- return
- self._clear_isolated_net_resources()
- for creds in six.itervalues(self._creds):
- try:
- self.creds_client.delete_user(creds.user_id)
- except lib_exc.NotFound:
- LOG.warning("user with name: %s not found for delete" %
- creds.username)
- try:
- if CONF.service_available.neutron:
- self._cleanup_default_secgroup(creds.tenant_id)
- self.creds_client.delete_project(creds.tenant_id)
- except lib_exc.NotFound:
- LOG.warning("tenant with name: %s not found for delete" %
- creds.tenant_name)
- self._creds = {}
-
- def is_multi_user(self):
- return True
-
- def is_multi_tenant(self):
- return True
-
- def is_role_available(self, role):
- return True
diff --git a/tempest/common/fixed_network.py b/tempest/common/fixed_network.py
deleted file mode 100644
index 3fc1365..0000000
--- a/tempest/common/fixed_network.py
+++ /dev/null
@@ -1,127 +0,0 @@
-# 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 oslo_log import log as logging
-
-from tempest_lib.common.utils import misc as misc_utils
-
-from tempest import exceptions
-
-LOG = logging.getLogger(__name__)
-
-
-def get_network_from_name(name, compute_networks_client):
- """Get a full network dict from just a network name
-
- :param str name: the name of the network to use
- :param NetworksClient compute_networks_client: The network client
- object to use for making the network lists api request
- :return: The full dictionary for the network in question
- :rtype: dict
- :raises InvalidTestResource: If the name provided is invalid, the networks
- list returns a 404, there are no found networks, or the found network
- is invalid
- """
- caller = misc_utils.find_test_caller()
-
- if not name:
- raise exceptions.InvalidTestResource(type='network', name=name)
-
- networks = compute_networks_client.list_networks()['networks']
- networks = [n for n in networks if n['label'] == name]
-
- # Check that a network exists, else raise an InvalidConfigurationException
- if len(networks) == 1:
- network = sorted(networks)[0]
- elif len(networks) > 1:
- msg = ("Network with name: %s had multiple matching networks in the "
- "list response: %s\n Unable to specify a single network" % (
- name, networks))
- if caller:
- msg = '(%s) %s' % (caller, msg)
- LOG.warning(msg)
- raise exceptions.InvalidTestResource(type='network', name=name)
- else:
- msg = "Network with name: %s not found" % name
- if caller:
- msg = '(%s) %s' % (caller, msg)
- LOG.warning(msg)
- raise exceptions.InvalidTestResource(type='network', name=name)
- # To be consistent between neutron and nova network always use name even
- # if label is used in the api response. If neither is present than then
- # the returned network is invalid.
- name = network.get('name') or network.get('label')
- if not name:
- msg = "Network found from list doesn't contain a valid name or label"
- if caller:
- msg = '(%s) %s' % (caller, msg)
- LOG.warning(msg)
- raise exceptions.InvalidTestResource(type='network', name=name)
- network['name'] = name
- return network
-
-
-def get_tenant_network(creds_provider, compute_networks_client,
- shared_network_name):
- """Get a network usable by the primary tenant
-
- :param creds_provider: instance of credential provider
- :param compute_networks_client: compute network client. We want to have the
- compute network client so we can have use a common approach for both
- neutron and nova-network cases. If this is not an admin network
- client, set_network_kwargs might fail in case fixed_network_name
- is the network to be used, and it's not visible to the tenant
- :param shared_network_name: name of the shared network to be used if no
- tenant network is available in the creds provider
- :returns: a dict with 'id' and 'name' of the network
- """
- caller = misc_utils.find_test_caller()
- net_creds = creds_provider.get_primary_creds()
- network = getattr(net_creds, 'network', None)
- if not network or not network.get('name'):
- if shared_network_name:
- msg = ('No valid network provided or created, defaulting to '
- 'fixed_network_name')
- if caller:
- msg = '(%s) %s' % (caller, msg)
- LOG.debug(msg)
- try:
- network = get_network_from_name(shared_network_name,
- compute_networks_client)
- except exceptions.InvalidTestResource:
- network = {}
- msg = ('Found network %s available for tenant' % network)
- if caller:
- msg = '(%s) %s' % (caller, msg)
- LOG.info(msg)
- return network
-
-
-def set_networks_kwarg(network, kwargs=None):
- """Set 'networks' kwargs for a server create if missing
-
- :param network: dict of network to be used with 'id' and 'name'
- :param kwargs: server create kwargs to be enhanced
- :return: new dict of kwargs updated to include networks
- """
- params = copy.copy(kwargs) or {}
- if kwargs and 'networks' in kwargs:
- return params
-
- if network:
- if 'id' in network.keys():
- params.update({"networks": [{'uuid': network['id']}]})
- else:
- LOG.warning('The provided network dict: %s was invalid and did '
- 'not contain an id' % network)
- return params
diff --git a/tempest/common/generator/__init__.py b/tempest/common/generator/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/common/generator/__init__.py
+++ /dev/null
diff --git a/tempest/common/generator/base_generator.py b/tempest/common/generator/base_generator.py
deleted file mode 100644
index a66002f..0000000
--- a/tempest/common/generator/base_generator.py
+++ /dev/null
@@ -1,179 +0,0 @@
-# Copyright 2014 Deutsche Telekom AG
-# 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 copy
-import functools
-
-import jsonschema
-from oslo_log import log as logging
-import six
-
-LOG = logging.getLogger(__name__)
-
-
-def _check_for_expected_result(name, schema):
- expected_result = None
- if "results" in schema:
- if name in schema["results"]:
- expected_result = schema["results"][name]
- return expected_result
-
-
-def generator_type(*args, **kwargs):
- def wrapper(func):
- func.types = args
- for key in kwargs:
- setattr(func, key, kwargs[key])
- return func
- return wrapper
-
-
-def simple_generator(fn):
- """Decorator for simple generators that return one value"""
- @functools.wraps(fn)
- def wrapped(self, schema):
- result = fn(self, schema)
- if result is not None:
- expected_result = _check_for_expected_result(fn.__name__, schema)
- return (fn.__name__, result, expected_result)
- return
- return wrapped
-
-
-class BasicGeneratorSet(object):
- _instance = None
-
- schema = {
- "type": "object",
- "properties": {
- "name": {"type": "string"},
- "http-method": {
- "enum": ["GET", "PUT", "HEAD",
- "POST", "PATCH", "DELETE", 'COPY']
- },
- "admin_client": {"type": "boolean"},
- "url": {"type": "string"},
- "default_result_code": {"type": "integer"},
- "json-schema": {},
- "resources": {
- "type": "array",
- "items": {
- "oneOf": [
- {"type": "string"},
- {
- "type": "object",
- "properties": {
- "name": {"type": "string"},
- "expected_result": {"type": "integer"}
- }
- }
- ]
- }
- },
- "results": {
- "type": "object",
- "properties": {}
- }
- },
- "required": ["name", "http-method", "url"],
- "additionalProperties": False,
- }
-
- def __init__(self):
- self.types_dict = {}
- for m in dir(self):
- if callable(getattr(self, m)) and not'__' in m:
- method = getattr(self, m)
- if hasattr(method, "types"):
- for type in method.types:
- if type not in self.types_dict:
- self.types_dict[type] = []
- self.types_dict[type].append(method)
-
- def validate_schema(self, schema):
- if "json-schema" in schema:
- jsonschema.Draft4Validator.check_schema(schema['json-schema'])
- jsonschema.validate(schema, self.schema)
-
- def generate_scenarios(self, schema, path=None):
- """Generate scenario (all possible test cases) out of the given schema
-
- :param schema: a dict style schema (see ``BasicGeneratorSet.schema``)
- :param path: the schema path if the given schema is a subschema
- """
- schema_type = schema['type']
- scenarios = []
-
- if schema_type == 'object':
- properties = schema["properties"]
- for attribute, definition in six.iteritems(properties):
- current_path = copy.copy(path)
- if path is not None:
- current_path.append(attribute)
- else:
- current_path = [attribute]
- scenarios.extend(
- self.generate_scenarios(definition, current_path))
- elif isinstance(schema_type, list):
- if "integer" in schema_type:
- schema_type = "integer"
- else:
- raise Exception("non-integer list types not supported")
- for generator in self.types_dict[schema_type]:
- if hasattr(generator, "needed_property"):
- prop = generator.needed_property
- if (prop not in schema or
- schema[prop] is None or
- schema[prop] is False):
- continue
-
- name = generator.__name__
- if ("exclude_tests" in schema and
- name in schema["exclude_tests"]):
- continue
- if path is not None:
- name = "%s_%s" % ("_".join(path), name)
- scenarios.append({
- "_negtest_name": name,
- "_negtest_generator": generator,
- "_negtest_schema": schema,
- "_negtest_path": path})
- return scenarios
-
- def generate_payload(self, test, schema):
- """Generates one jsonschema out of the given test.
-
- It's mandatory to use generate_scenarios before to register all needed
- variables to the test.
-
- :param test: A test object (scenario) with all _negtest variables on it
- :param schema: schema for the test
- """
- generator = test._negtest_generator
- ret = generator(test._negtest_schema)
- path = copy.copy(test._negtest_path)
- expected_result = None
-
- if ret is not None:
- generator_result = generator(test._negtest_schema)
- invalid_snippet = generator_result[1]
- expected_result = generator_result[2]
- element = path.pop()
- if len(path) > 0:
- schema_snip = reduce(dict.get, path, schema)
- schema_snip[element] = invalid_snippet
- else:
- schema[element] = invalid_snippet
- return expected_result
diff --git a/tempest/common/generator/negative_generator.py b/tempest/common/generator/negative_generator.py
deleted file mode 100644
index 17997a5..0000000
--- a/tempest/common/generator/negative_generator.py
+++ /dev/null
@@ -1,79 +0,0 @@
-# Copyright 2014 Deutsche Telekom AG
-# 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 copy
-
-from oslo_log import log as logging
-
-import tempest.common.generator.base_generator as base
-import tempest.common.generator.valid_generator as valid
-
-LOG = logging.getLogger(__name__)
-
-
-class NegativeTestGenerator(base.BasicGeneratorSet):
- @base.generator_type("string")
- @base.simple_generator
- def gen_int(self, _):
- return 4
-
- @base.generator_type("integer")
- @base.simple_generator
- def gen_string(self, _):
- return "XXXXXX"
-
- @base.generator_type("integer", "string")
- def gen_none(self, schema):
- # Note(mkoderer): it's not using the decorator otherwise it'd be
- # filtered
- expected_result = base._check_for_expected_result('gen_none', schema)
- return ('gen_none', None, expected_result)
-
- @base.generator_type("string")
- @base.simple_generator
- def gen_str_min_length(self, schema):
- min_length = schema.get("minLength", 0)
- if min_length > 0:
- return "x" * (min_length - 1)
-
- @base.generator_type("string", needed_property="maxLength")
- @base.simple_generator
- def gen_str_max_length(self, schema):
- max_length = schema.get("maxLength", -1)
- return "x" * (max_length + 1)
-
- @base.generator_type("integer", needed_property="minimum")
- @base.simple_generator
- def gen_int_min(self, schema):
- minimum = schema["minimum"]
- if "exclusiveMinimum" not in schema:
- minimum -= 1
- return minimum
-
- @base.generator_type("integer", needed_property="maximum")
- @base.simple_generator
- def gen_int_max(self, schema):
- maximum = schema["maximum"]
- if "exclusiveMaximum" not in schema:
- maximum += 1
- return maximum
-
- @base.generator_type("object", needed_property="additionalProperties")
- @base.simple_generator
- def gen_obj_add_attr(self, schema):
- valid_schema = valid.ValidTestGenerator().generate_valid(schema)
- new_valid = copy.deepcopy(valid_schema)
- new_valid["$$$$$$$$$$"] = "xxx"
- return new_valid
diff --git a/tempest/common/generator/valid_generator.py b/tempest/common/generator/valid_generator.py
deleted file mode 100644
index 2213b4a..0000000
--- a/tempest/common/generator/valid_generator.py
+++ /dev/null
@@ -1,83 +0,0 @@
-# Copyright 2014 Deutsche Telekom AG
-# 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_log import log as logging
-import six
-
-import tempest.common.generator.base_generator as base
-
-
-LOG = logging.getLogger(__name__)
-
-
-class ValidTestGenerator(base.BasicGeneratorSet):
- @base.generator_type("string")
- @base.simple_generator
- def generate_valid_string(self, schema):
- size = schema.get("minLength", 1)
- # TODO(dkr mko): handle format and pattern
- return "x" * size
-
- @base.generator_type("integer")
- @base.simple_generator
- def generate_valid_integer(self, schema):
- # TODO(dkr mko): handle multipleOf
- if "minimum" in schema:
- minimum = schema["minimum"]
- if "exclusiveMinimum" not in schema:
- return minimum
- else:
- return minimum + 1
- if "maximum" in schema:
- maximum = schema["maximum"]
- if "exclusiveMaximum" not in schema:
- return maximum
- else:
- return maximum - 1
- return 0
-
- @base.generator_type("object")
- @base.simple_generator
- def generate_valid_object(self, schema):
- obj = {}
- for k, v in six.iteritems(schema["properties"]):
- obj[k] = self.generate_valid(v)
- return obj
-
- def generate(self, schema):
- schema_type = schema["type"]
- if isinstance(schema_type, list):
- if "integer" in schema_type:
- schema_type = "integer"
- else:
- raise Exception("non-integer list types not supported")
- result = []
- if schema_type not in self.types_dict:
- raise TypeError("generator (%s) doesn't support type: %s"
- % (self.__class__.__name__, schema_type))
- for generator in self.types_dict[schema_type]:
- ret = generator(schema)
- if ret is not None:
- if isinstance(ret, list):
- result.extend(ret)
- elif isinstance(ret, tuple):
- result.append(ret)
- else:
- raise Exception("generator (%s) returns invalid result: %s"
- % (generator, ret))
- return result
-
- def generate_valid(self, schema):
- return self.generate(schema)[0][1]
diff --git a/tempest/common/glance_http.py b/tempest/common/glance_http.py
deleted file mode 100644
index 800e977..0000000
--- a/tempest/common/glance_http.py
+++ /dev/null
@@ -1,343 +0,0 @@
-# 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.
-
-# Originally copied from python-glanceclient
-
-import copy
-import hashlib
-import posixpath
-import re
-import socket
-import struct
-
-import OpenSSL
-from oslo_log import log as logging
-import six
-from six import moves
-from six.moves import http_client as httplib
-from six.moves.urllib import parse as urlparse
-
-from tempest import exceptions as exc
-
-LOG = logging.getLogger(__name__)
-USER_AGENT = 'tempest'
-CHUNKSIZE = 1024 * 64 # 64kB
-TOKEN_CHARS_RE = re.compile('^[-A-Za-z0-9+/=]*$')
-
-
-class HTTPClient(object):
-
- def __init__(self, auth_provider, filters, **kwargs):
- self.auth_provider = auth_provider
- self.filters = filters
- self.endpoint = auth_provider.base_url(filters)
- endpoint_parts = urlparse.urlparse(self.endpoint)
- self.endpoint_scheme = endpoint_parts.scheme
- self.endpoint_hostname = endpoint_parts.hostname
- self.endpoint_port = endpoint_parts.port
- self.endpoint_path = endpoint_parts.path
-
- self.connection_class = self._get_connection_class(
- self.endpoint_scheme)
- self.connection_kwargs = self._get_connection_kwargs(
- self.endpoint_scheme, **kwargs)
-
- @staticmethod
- def _get_connection_class(scheme):
- if scheme == 'https':
- return VerifiedHTTPSConnection
- else:
- return httplib.HTTPConnection
-
- @staticmethod
- def _get_connection_kwargs(scheme, **kwargs):
- _kwargs = {'timeout': float(kwargs.get('timeout', 600))}
-
- if scheme == 'https':
- _kwargs['ca_certs'] = kwargs.get('ca_certs', None)
- _kwargs['cert_file'] = kwargs.get('cert_file', None)
- _kwargs['key_file'] = kwargs.get('key_file', None)
- _kwargs['insecure'] = kwargs.get('insecure', False)
- _kwargs['ssl_compression'] = kwargs.get('ssl_compression', True)
-
- return _kwargs
-
- def _get_connection(self):
- _class = self.connection_class
- try:
- return _class(self.endpoint_hostname, self.endpoint_port,
- **self.connection_kwargs)
- except httplib.InvalidURL:
- raise exc.EndpointNotFound
-
- def _http_request(self, url, method, **kwargs):
- """Send an http request with the specified characteristics.
-
- Wrapper around httplib.HTTP(S)Connection.request to handle tasks such
- as setting headers and error handling.
- """
- # Copy the kwargs so we can reuse the original in case of redirects
- kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {}))
- kwargs['headers'].setdefault('User-Agent', USER_AGENT)
-
- self._log_request(method, url, kwargs['headers'])
-
- conn = self._get_connection()
-
- try:
- url_parts = urlparse.urlparse(url)
- conn_url = posixpath.normpath(url_parts.path)
- LOG.debug('Actual Path: {path}'.format(path=conn_url))
- if kwargs['headers'].get('Transfer-Encoding') == 'chunked':
- conn.putrequest(method, conn_url)
- for header, value in kwargs['headers'].items():
- conn.putheader(header, value)
- conn.endheaders()
- chunk = kwargs['body'].read(CHUNKSIZE)
- # Chunk it, baby...
- while chunk:
- conn.send('%x\r\n%s\r\n' % (len(chunk), chunk))
- chunk = kwargs['body'].read(CHUNKSIZE)
- conn.send('0\r\n\r\n')
- else:
- conn.request(method, conn_url, **kwargs)
- resp = conn.getresponse()
- except socket.gaierror as e:
- message = ("Error finding address for %(url)s: %(e)s" %
- {'url': url, 'e': e})
- raise exc.EndpointNotFound(message)
- except (socket.error, socket.timeout) as e:
- message = ("Error communicating with %(endpoint)s %(e)s" %
- {'endpoint': self.endpoint, 'e': e})
- raise exc.TimeoutException(message)
-
- body_iter = ResponseBodyIterator(resp)
- # Read body into string if it isn't obviously image data
- if resp.getheader('content-type', None) != 'application/octet-stream':
- body_str = ''.join([body_chunk for body_chunk in body_iter])
- body_iter = six.StringIO(body_str)
- self._log_response(resp, None)
- else:
- self._log_response(resp, body_iter)
-
- return resp, body_iter
-
- def _log_request(self, method, url, headers):
- LOG.info('Request: ' + method + ' ' + url)
- if headers:
- headers_out = headers
- if 'X-Auth-Token' in headers and headers['X-Auth-Token']:
- token = headers['X-Auth-Token']
- if len(token) > 64 and TOKEN_CHARS_RE.match(token):
- headers_out = headers.copy()
- headers_out['X-Auth-Token'] = "<Token omitted>"
- LOG.info('Request Headers: ' + str(headers_out))
-
- def _log_response(self, resp, body):
- status = str(resp.status)
- LOG.info("Response Status: " + status)
- if resp.getheaders():
- LOG.info('Response Headers: ' + str(resp.getheaders()))
- if body:
- str_body = str(body)
- length = len(body)
- LOG.info('Response Body: ' + str_body[:2048])
- if length >= 2048:
- self.LOG.debug("Large body (%d) md5 summary: %s", length,
- hashlib.md5(str_body).hexdigest())
-
- def raw_request(self, method, url, **kwargs):
- kwargs.setdefault('headers', {})
- kwargs['headers'].setdefault('Content-Type',
- 'application/octet-stream')
- if 'body' in kwargs:
- if (hasattr(kwargs['body'], 'read')
- and method.lower() in ('post', 'put')):
- # We use 'Transfer-Encoding: chunked' because
- # body size may not always be known in advance.
- kwargs['headers']['Transfer-Encoding'] = 'chunked'
-
- # Decorate the request with auth
- req_url, kwargs['headers'], kwargs['body'] = \
- self.auth_provider.auth_request(
- method=method, url=url, headers=kwargs['headers'],
- body=kwargs.get('body', None), filters=self.filters)
- return self._http_request(req_url, method, **kwargs)
-
-
-class OpenSSLConnectionDelegator(object):
- """An OpenSSL.SSL.Connection delegator.
-
- Supplies an additional 'makefile' method which httplib requires
- and is not present in OpenSSL.SSL.Connection.
-
- Note: Since it is not possible to inherit from OpenSSL.SSL.Connection
- a delegator must be used.
- """
- def __init__(self, *args, **kwargs):
- self.connection = OpenSSL.SSL.Connection(*args, **kwargs)
-
- def __getattr__(self, name):
- return getattr(self.connection, name)
-
- def makefile(self, *args, **kwargs):
- # Ensure the socket is closed when this file is closed
- kwargs['close'] = True
- return socket._fileobject(self.connection, *args, **kwargs)
-
-
-class VerifiedHTTPSConnection(httplib.HTTPSConnection):
- """Extended HTTPSConnection which uses OpenSSL library for enhanced SSL
-
- Note: Much of this functionality can eventually be replaced
- with native Python 3.3 code.
- """
- def __init__(self, host, port=None, key_file=None, cert_file=None,
- ca_certs=None, timeout=None, insecure=False,
- ssl_compression=True):
- httplib.HTTPSConnection.__init__(self, host, port,
- key_file=key_file,
- cert_file=cert_file)
- self.key_file = key_file
- self.cert_file = cert_file
- self.timeout = timeout
- self.insecure = insecure
- self.ssl_compression = ssl_compression
- self.ca_certs = ca_certs
- self.setcontext()
-
- @staticmethod
- def host_matches_cert(host, x509):
- """Verify that the x509 certificate we have received from 'host'
-
- Identifies the server we are connecting to, ie that the certificate's
- Common Name or a Subject Alternative Name matches 'host'.
- """
- # First see if we can match the CN
- if x509.get_subject().commonName == host:
- return True
-
- # Also try Subject Alternative Names for a match
- san_list = None
- for i in moves.xrange(x509.get_extension_count()):
- ext = x509.get_extension(i)
- if ext.get_short_name() == 'subjectAltName':
- san_list = str(ext)
- for san in ''.join(san_list.split()).split(','):
- if san == "DNS:%s" % host:
- return True
-
- # Server certificate does not match host
- msg = ('Host "%s" does not match x509 certificate contents: '
- 'CommonName "%s"' % (host, x509.get_subject().commonName))
- if san_list is not None:
- msg = msg + ', subjectAltName "%s"' % san_list
- raise exc.SSLCertificateError(msg)
-
- def verify_callback(self, connection, x509, errnum,
- depth, preverify_ok):
- if x509.has_expired():
- msg = "SSL Certificate expired on '%s'" % x509.get_notAfter()
- raise exc.SSLCertificateError(msg)
-
- if depth == 0 and preverify_ok is True:
- # We verify that the host matches against the last
- # certificate in the chain
- return self.host_matches_cert(self.host, x509)
- else:
- # Pass through OpenSSL's default result
- return preverify_ok
-
- def setcontext(self):
- """Set up the OpenSSL context."""
- self.context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
-
- if self.ssl_compression is False:
- self.context.set_options(0x20000) # SSL_OP_NO_COMPRESSION
-
- if self.insecure is not True:
- self.context.set_verify(OpenSSL.SSL.VERIFY_PEER,
- self.verify_callback)
- else:
- self.context.set_verify(OpenSSL.SSL.VERIFY_NONE,
- self.verify_callback)
-
- if self.cert_file:
- try:
- self.context.use_certificate_file(self.cert_file)
- except Exception as e:
- msg = 'Unable to load cert from "%s" %s' % (self.cert_file, e)
- raise exc.SSLConfigurationError(msg)
- if self.key_file is None:
- # We support having key and cert in same file
- try:
- self.context.use_privatekey_file(self.cert_file)
- except Exception as e:
- msg = ('No key file specified and unable to load key '
- 'from "%s" %s' % (self.cert_file, e))
- raise exc.SSLConfigurationError(msg)
-
- if self.key_file:
- try:
- self.context.use_privatekey_file(self.key_file)
- except Exception as e:
- msg = 'Unable to load key from "%s" %s' % (self.key_file, e)
- raise exc.SSLConfigurationError(msg)
-
- if self.ca_certs:
- try:
- self.context.load_verify_locations(self.ca_certs)
- except Exception as e:
- msg = 'Unable to load CA from "%s" %s' % (self.ca_certs, e)
- raise exc.SSLConfigurationError(msg)
- else:
- self.context.set_default_verify_paths()
-
- def connect(self):
- """Connect to SSL port and apply per-connection parameters."""
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- if self.timeout is not None:
- # '0' microseconds
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO,
- struct.pack('LL', self.timeout, 0))
- self.sock = OpenSSLConnectionDelegator(self.context, sock)
- self.sock.connect((self.host, self.port))
-
- def close(self):
- if self.sock:
- # Remove the reference to the socket but don't close it yet.
- # Response close will close both socket and associated
- # file. Closing socket too soon will cause response
- # reads to fail with socket IO error 'Bad file descriptor'.
- self.sock = None
- httplib.HTTPSConnection.close(self)
-
-
-class ResponseBodyIterator(object):
- """A class that acts as an iterator over an HTTP response."""
-
- def __init__(self, resp):
- self.resp = resp
-
- def __iter__(self):
- while True:
- yield self.next()
-
- def next(self):
- chunk = self.resp.read(CHUNKSIZE)
- if chunk:
- return chunk
- else:
- raise StopIteration()
diff --git a/tempest/common/identity.py b/tempest/common/identity.py
index 2179363..eaf651b 100644
--- a/tempest/common/identity.py
+++ b/tempest/common/identity.py
@@ -13,7 +13,20 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import exceptions as lib_exc
+from tempest import config
+from tempest.lib.common import cred_client
+from tempest.lib import exceptions as lib_exc
+
+CONF = config.CONF
+
+
+def get_project_by_name(client, project_name):
+ projects = client.list_projects({'name': project_name})['projects']
+ for project in projects:
+ if project['name'] == project_name:
+ return project
+ raise lib_exc.NotFound('No such project(%s) in %s' % (project_name,
+ projects))
def get_tenant_by_name(client, tenant_name):
@@ -30,3 +43,49 @@
if user['name'] == username:
return user
raise lib_exc.NotFound('No such user(%s) in %s' % (username, users))
+
+
+def get_user_by_project(users_client, roles_client, project_id, username):
+ users = users_client.list_users(**{'name': username})['users']
+ users_in_project = roles_client.list_role_assignments(
+ **{'scope.project.id': project_id})['role_assignments']
+ for user in users:
+ if user['name'] == username:
+ for u in users_in_project:
+ if u['user']['id'] == user['id']:
+ return user
+ raise lib_exc.NotFound('No such user(%s) in %s' % (username, users))
+
+
+def identity_utils(clients):
+ """A client that abstracts v2 and v3 identity operations.
+
+ This can be used for creating and tearing down projects in tests. It
+ should not be used for testing identity features.
+
+ :param clients: a client manager.
+ :return
+ """
+ if CONF.identity.auth_version == 'v2':
+ client = clients.identity_client
+ users_client = clients.users_client
+ project_client = clients.tenants_client
+ roles_client = clients.roles_client
+ domains_client = None
+ else:
+ client = clients.identity_v3_client
+ users_client = clients.users_v3_client
+ project_client = clients.projects_client
+ roles_client = clients.roles_v3_client
+ domains_client = clients.domains_client
+
+ try:
+ domain = client.auth_provider.credentials.project_domain_name
+ except AttributeError:
+ domain = CONF.auth.default_credentials_domain_name
+
+ return cred_client.get_creds_client(client, project_client,
+ users_client,
+ roles_client,
+ domains_client,
+ project_domain_name=domain)
diff --git a/tempest/common/image.py b/tempest/common/image.py
new file mode 100644
index 0000000..3618f7e
--- /dev/null
+++ b/tempest/common/image.py
@@ -0,0 +1,65 @@
+# Copyright 2016 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.
+
+import copy
+
+
+def get_image_meta_from_headers(resp):
+ meta = {'properties': {}}
+ for key in resp.response:
+ value = resp.response[key]
+ if key.startswith('x-image-meta-property-'):
+ _key = key[22:]
+ meta['properties'][_key] = value
+ elif key.startswith('x-image-meta-'):
+ _key = key[13:]
+ meta[_key] = value
+
+ for key in ['is_public', 'protected', 'deleted']:
+ if key in meta:
+ meta[key] = meta[key].strip().lower() in ('t', 'true', 'yes', '1')
+
+ for key in ['size', 'min_ram', 'min_disk']:
+ if key in meta:
+ try:
+ meta[key] = int(meta[key])
+ except ValueError:
+ pass
+ return meta
+
+
+def image_meta_to_headers(**metadata):
+ headers = {}
+ fields_copy = copy.deepcopy(metadata)
+
+ copy_from = fields_copy.pop('copy_from', None)
+ purge = fields_copy.pop('purge_props', None)
+
+ if purge is not None:
+ headers['x-glance-registry-purge-props'] = purge
+
+ if copy_from is not None:
+ headers['x-glance-api-copy-from'] = copy_from
+
+ for key, value in fields_copy.pop('properties', {}).items():
+ headers['x-image-meta-property-%s' % key] = str(value)
+
+ for key, value in fields_copy.pop('api', {}).items():
+ headers['x-glance-api-property-%s' % key] = str(value)
+
+ for key, value in fields_copy.items():
+ headers['x-image-meta-%s' % key] = str(value)
+
+ return headers
diff --git a/tempest/common/negative_rest_client.py b/tempest/common/negative_rest_client.py
deleted file mode 100644
index d97411c..0000000
--- a/tempest/common/negative_rest_client.py
+++ /dev/null
@@ -1,81 +0,0 @@
-# (c) 2014 Deutsche Telekom AG
-# Copyright 2014 Red Hat, Inc.
-# Copyright 2014 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.common import service_client
-from tempest import config
-
-CONF = config.CONF
-
-
-class NegativeRestClient(service_client.ServiceClient):
- """Version of RestClient that does not raise exceptions."""
- def __init__(self, auth_provider, service,
- build_interval=None, build_timeout=None,
- disable_ssl_certificate_validation=None,
- ca_certs=None, trace_requests=None):
- region, endpoint_type = self._get_region_and_endpoint_type(service)
- super(NegativeRestClient, self).__init__(
- auth_provider,
- service,
- region,
- endpoint_type=endpoint_type,
- build_interval=build_interval,
- build_timeout=build_timeout,
- disable_ssl_certificate_validation=(
- disable_ssl_certificate_validation),
- ca_certs=ca_certs,
- trace_requests=trace_requests)
-
- def _get_region_and_endpoint_type(self, service):
- """Returns the region for a specific service"""
- service_region = None
- service_endpoint_type = None
- for cfgname in dir(CONF._config):
- # Find all config.FOO.catalog_type and assume FOO is a service.
- cfg = getattr(CONF, cfgname)
- catalog_type = getattr(cfg, 'catalog_type', None)
- if catalog_type == service:
- service_region = getattr(cfg, 'region', None)
- service_endpoint_type = getattr(cfg, 'endpoint_type', None)
- if not service_region:
- service_region = CONF.identity.region
- return service_region, service_endpoint_type
-
- def _error_checker(self, method, url,
- headers, body, resp, resp_body):
- pass
-
- def send_request(self, method, url_template, resources, body=None):
- url = url_template % tuple(resources)
- if method == "GET":
- resp, body = self.get(url)
- elif method == "POST":
- resp, body = self.post(url, body)
- elif method == "PUT":
- resp, body = self.put(url, body)
- elif method == "PATCH":
- resp, body = self.patch(url, body)
- elif method == "HEAD":
- resp, body = self.head(url)
- elif method == "DELETE":
- resp, body = self.delete(url)
- elif method == "COPY":
- resp, body = self.copy(url)
- else:
- assert False
-
- return resp, body
diff --git a/tempest/common/preprov_creds.py b/tempest/common/preprov_creds.py
deleted file mode 100644
index 34af31e..0000000
--- a/tempest/common/preprov_creds.py
+++ /dev/null
@@ -1,359 +0,0 @@
-# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
-#
-# 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 hashlib
-import os
-
-from oslo_concurrency import lockutils
-from oslo_log import log as logging
-import six
-from tempest_lib import auth
-from tempest_lib import exceptions as lib_exc
-import yaml
-
-from tempest import clients
-from tempest.common import cred_provider
-from tempest.common import fixed_network
-from tempest import exceptions
-
-LOG = logging.getLogger(__name__)
-
-
-def read_accounts_yaml(path):
- try:
- with open(path, 'r') as yaml_file:
- accounts = yaml.load(yaml_file)
- except IOError:
- raise exceptions.InvalidConfiguration(
- 'The path for the test accounts file: %s '
- 'could not be found' % path)
- return accounts
-
-
-class PreProvisionedCredentialProvider(cred_provider.CredentialProvider):
-
- def __init__(self, identity_version, test_accounts_file,
- accounts_lock_dir, name=None, credentials_domain=None,
- admin_role=None, object_storage_operator_role=None,
- object_storage_reseller_admin_role=None):
- """Credentials provider using pre-provisioned accounts
-
- This credentials provider loads the details of pre-provisioned
- accounts from a YAML file, in the format specified by
- `etc/accounts.yaml.sample`. It locks accounts while in use, using the
- external locking mechanism, allowing for multiple python processes
- to share a single account file, and thus running tests in parallel.
-
- The accounts_lock_dir must be generated using `lockutils.get_lock_path`
- from the oslo.concurrency library. For instance:
-
- accounts_lock_dir = os.path.join(lockutils.get_lock_path(CONF),
- 'test_accounts')
-
- Role names for object storage are optional as long as the
- `operator` and `reseller_admin` credential types are not used in the
- accounts file.
-
- :param identity_version: identity version of the credentials
- :param admin_role: name of the admin role
- :param test_accounts_file: path to the accounts YAML file
- :param accounts_lock_dir: the directory for external locking
- :param name: name of the hash file (optional)
- :param credentials_domain: name of the domain credentials belong to
- (if no domain is configured)
- :param object_storage_operator_role: name of the role
- :param object_storage_reseller_admin_role: name of the role
- """
- super(PreProvisionedCredentialProvider, self).__init__(
- identity_version=identity_version, name=name,
- admin_role=admin_role, credentials_domain=credentials_domain)
- self.test_accounts_file = test_accounts_file
- if test_accounts_file:
- accounts = read_accounts_yaml(self.test_accounts_file)
- self.use_default_creds = False
- else:
- accounts = {}
- self.use_default_creds = True
- self.hash_dict = self.get_hash_dict(
- accounts, admin_role, object_storage_operator_role,
- object_storage_reseller_admin_role)
- self.accounts_dir = accounts_lock_dir
- self._creds = {}
-
- @classmethod
- def _append_role(cls, role, account_hash, hash_dict):
- if role in hash_dict['roles']:
- hash_dict['roles'][role].append(account_hash)
- else:
- hash_dict['roles'][role] = [account_hash]
- return hash_dict
-
- @classmethod
- def get_hash_dict(cls, accounts, admin_role,
- object_storage_operator_role=None,
- object_storage_reseller_admin_role=None):
- hash_dict = {'roles': {}, 'creds': {}, 'networks': {}}
- # Loop over the accounts read from the yaml file
- for account in accounts:
- roles = []
- types = []
- resources = []
- if 'roles' in account:
- roles = account.pop('roles')
- if 'types' in account:
- types = account.pop('types')
- if 'resources' in account:
- resources = account.pop('resources')
- temp_hash = hashlib.md5()
- temp_hash.update(six.text_type(account).encode('utf-8'))
- temp_hash_key = temp_hash.hexdigest()
- hash_dict['creds'][temp_hash_key] = account
- for role in roles:
- hash_dict = cls._append_role(role, temp_hash_key,
- hash_dict)
- # If types are set for the account append the matching role
- # subdict with the hash
- for type in types:
- if type == 'admin':
- hash_dict = cls._append_role(admin_role, temp_hash_key,
- hash_dict)
- elif type == 'operator':
- if object_storage_operator_role:
- hash_dict = cls._append_role(
- object_storage_operator_role, temp_hash_key,
- hash_dict)
- else:
- msg = ("Type 'operator' configured, but no "
- "object_storage_operator_role specified")
- raise lib_exc.InvalidCredentials(msg)
- elif type == 'reseller_admin':
- if object_storage_reseller_admin_role:
- hash_dict = cls._append_role(
- object_storage_reseller_admin_role,
- temp_hash_key,
- hash_dict)
- else:
- msg = ("Type 'reseller_admin' configured, but no "
- "object_storage_reseller_admin_role specified")
- raise lib_exc.InvalidCredentials(msg)
- # Populate the network subdict
- for resource in resources:
- if resource == 'network':
- hash_dict['networks'][temp_hash_key] = resources[resource]
- else:
- LOG.warning('Unknown resource type %s, ignoring this field'
- % resource)
- return hash_dict
-
- def is_multi_user(self):
- # Default credentials is not a valid option with locking Account
- if self.use_default_creds:
- raise lib_exc.InvalidCredentials(
- "Account file %s doesn't exist" % self.test_accounts_file)
- else:
- return len(self.hash_dict['creds']) > 1
-
- def is_multi_tenant(self):
- return self.is_multi_user()
-
- def _create_hash_file(self, hash_string):
- path = os.path.join(os.path.join(self.accounts_dir, hash_string))
- if not os.path.isfile(path):
- with open(path, 'w') as fd:
- fd.write(self.name)
- return True
- return False
-
- @lockutils.synchronized('test_accounts_io', external=True)
- def _get_free_hash(self, hashes):
- # Cast as a list because in some edge cases a set will be passed in
- hashes = list(hashes)
- if not os.path.isdir(self.accounts_dir):
- os.mkdir(self.accounts_dir)
- # Create File from first hash (since none are in use)
- self._create_hash_file(hashes[0])
- return hashes[0]
- names = []
- for _hash in hashes:
- res = self._create_hash_file(_hash)
- if res:
- return _hash
- else:
- path = os.path.join(os.path.join(self.accounts_dir,
- _hash))
- with open(path, 'r') as fd:
- names.append(fd.read())
- msg = ('Insufficient number of users provided. %s have allocated all '
- 'the credentials for this allocation request' % ','.join(names))
- raise lib_exc.InvalidCredentials(msg)
-
- def _get_match_hash_list(self, roles=None):
- hashes = []
- if roles:
- # Loop over all the creds for each role in the subdict and generate
- # a list of cred lists for each role
- for role in roles:
- temp_hashes = self.hash_dict['roles'].get(role, None)
- if not temp_hashes:
- raise lib_exc.InvalidCredentials(
- "No credentials with role: %s specified in the "
- "accounts ""file" % role)
- hashes.append(temp_hashes)
- # Take the list of lists and do a boolean and between each list to
- # find the creds which fall under all the specified roles
- temp_list = set(hashes[0])
- for hash_list in hashes[1:]:
- temp_list = temp_list & set(hash_list)
- hashes = temp_list
- else:
- hashes = self.hash_dict['creds'].keys()
- # NOTE(mtreinish): admin is a special case because of the increased
- # privlege set which could potentially cause issues on tests where that
- # is not expected. So unless the admin role isn't specified do not
- # allocate admin.
- admin_hashes = self.hash_dict['roles'].get(self.admin_role,
- None)
- if ((not roles or self.admin_role not in roles) and
- admin_hashes):
- useable_hashes = [x for x in hashes if x not in admin_hashes]
- else:
- useable_hashes = hashes
- return useable_hashes
-
- def _sanitize_creds(self, creds):
- temp_creds = creds.copy()
- temp_creds.pop('password')
- return temp_creds
-
- def _get_creds(self, roles=None):
- if self.use_default_creds:
- raise lib_exc.InvalidCredentials(
- "Account file %s doesn't exist" % self.test_accounts_file)
- useable_hashes = self._get_match_hash_list(roles)
- free_hash = self._get_free_hash(useable_hashes)
- clean_creds = self._sanitize_creds(
- self.hash_dict['creds'][free_hash])
- LOG.info('%s allocated creds:\n%s' % (self.name, clean_creds))
- return self._wrap_creds_with_network(free_hash)
-
- @lockutils.synchronized('test_accounts_io', external=True)
- def remove_hash(self, hash_string):
- hash_path = os.path.join(self.accounts_dir, hash_string)
- if not os.path.isfile(hash_path):
- LOG.warning('Expected an account lock file %s to remove, but '
- 'one did not exist' % hash_path)
- else:
- os.remove(hash_path)
- if not os.listdir(self.accounts_dir):
- os.rmdir(self.accounts_dir)
-
- def get_hash(self, creds):
- for _hash in self.hash_dict['creds']:
- # Comparing on the attributes that are expected in the YAML
- init_attributes = creds.get_init_attributes()
- hash_attributes = self.hash_dict['creds'][_hash].copy()
- if ('user_domain_name' in init_attributes and 'user_domain_name'
- not in hash_attributes):
- # Allow for the case of domain_name populated from config
- domain_name = self.credentials_domain
- hash_attributes['user_domain_name'] = domain_name
- if all([getattr(creds, k) == hash_attributes[k] for
- k in init_attributes]):
- return _hash
- raise AttributeError('Invalid credentials %s' % creds)
-
- def remove_credentials(self, creds):
- _hash = self.get_hash(creds)
- clean_creds = self._sanitize_creds(self.hash_dict['creds'][_hash])
- self.remove_hash(_hash)
- LOG.info("%s returned allocated creds:\n%s" % (self.name, clean_creds))
-
- def get_primary_creds(self):
- if self._creds.get('primary'):
- return self._creds.get('primary')
- net_creds = self._get_creds()
- self._creds['primary'] = net_creds
- return net_creds
-
- def get_alt_creds(self):
- if self._creds.get('alt'):
- return self._creds.get('alt')
- net_creds = self._get_creds()
- self._creds['alt'] = net_creds
- return net_creds
-
- def get_creds_by_roles(self, roles, force_new=False):
- roles = list(set(roles))
- exist_creds = self._creds.get(six.text_type(roles).encode(
- 'utf-8'), None)
- # The force kwarg is used to allocate an additional set of creds with
- # the same role list. The index used for the previously allocation
- # in the _creds dict will be moved.
- if exist_creds and not force_new:
- return exist_creds
- elif exist_creds and force_new:
- new_index = six.text_type(roles).encode('utf-8') + '-' + \
- six.text_type(len(self._creds)).encode('utf-8')
- self._creds[new_index] = exist_creds
- net_creds = self._get_creds(roles=roles)
- self._creds[six.text_type(roles).encode('utf-8')] = net_creds
- return net_creds
-
- def clear_creds(self):
- for creds in self._creds.values():
- self.remove_credentials(creds)
-
- def get_admin_creds(self):
- return self.get_creds_by_roles([self.admin_role])
-
- def is_role_available(self, role):
- if self.use_default_creds:
- return False
- else:
- if self.hash_dict['roles'].get(role):
- return True
- return False
-
- def admin_available(self):
- return self.is_role_available(self.admin_role)
-
- def _wrap_creds_with_network(self, hash):
- creds_dict = self.hash_dict['creds'][hash]
- # Make sure a domain scope if defined for users in case of V3
- creds_dict = self._extend_credentials(creds_dict)
- # This just builds a Credentials object, it does not validate
- # nor fill with missing fields.
- credential = auth.get_credentials(
- auth_url=None, fill_in=False,
- identity_version=self.identity_version, **creds_dict)
- net_creds = cred_provider.TestResources(credential)
- net_clients = clients.Manager(credentials=credential)
- compute_network_client = net_clients.compute_networks_client
- net_name = self.hash_dict['networks'].get(hash, None)
- try:
- network = fixed_network.get_network_from_name(
- net_name, compute_network_client)
- except exceptions.InvalidTestResource:
- network = {}
- net_creds.set_resources(network=network)
- return net_creds
-
- def _extend_credentials(self, creds_dict):
- # In case of v3, adds a user_domain_name field to the creds
- # dict if not defined
- if self.identity_version == 'v3':
- user_domain_fields = set(['user_domain_name', 'user_domain_id'])
- if not user_domain_fields.intersection(set(creds_dict.keys())):
- creds_dict['user_domain_name'] = self.credentials_domain
- return creds_dict
diff --git a/tempest/common/service_client.py b/tempest/common/service_client.py
deleted file mode 100644
index b3a5a09..0000000
--- a/tempest/common/service_client.py
+++ /dev/null
@@ -1,84 +0,0 @@
-# Copyright 2015 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.common import rest_client
-
-
-class ServiceClient(rest_client.RestClient):
-
- def __init__(self, auth_provider, service, region,
- endpoint_type=None, build_interval=None, build_timeout=None,
- disable_ssl_certificate_validation=None, ca_certs=None,
- trace_requests=None):
-
- dscv = disable_ssl_certificate_validation
- params = {
- 'disable_ssl_certificate_validation': dscv,
- 'ca_certs': ca_certs,
- 'trace_requests': trace_requests
- }
-
- if endpoint_type is not None:
- params.update({'endpoint_type': endpoint_type})
- if build_interval is not None:
- params.update({'build_interval': build_interval})
- if build_timeout is not None:
- params.update({'build_timeout': build_timeout})
- super(ServiceClient, self).__init__(auth_provider, service, region,
- **params)
-
-
-class ResponseBody(dict):
- """Class that wraps an http response and dict body into a single value.
-
- Callers that receive this object will normally use it as a dict but
- can extract the response if needed.
- """
-
- def __init__(self, response, body=None):
- body_data = body or {}
- self.update(body_data)
- self.response = response
-
- def __str__(self):
- body = super(ResponseBody, self).__str__()
- return "response: %s\nBody: %s" % (self.response, body)
-
-
-class ResponseBodyData(object):
- """Class that wraps an http response and string data into a single value"""
-
- def __init__(self, response, data):
- self.response = response
- self.data = data
-
- def __str__(self):
- return "response: %s\nBody: %s" % (self.response, self.data)
-
-
-class ResponseBodyList(list):
- """Class that wraps an http response and list body into a single value.
-
- Callers that receive this object will normally use it as a list but
- can extract the response if needed.
- """
-
- def __init__(self, response, body=None):
- body_data = body or []
- self.extend(body_data)
- self.response = response
-
- def __str__(self):
- body = super(ResponseBodyList, self).__str__()
- return "response: %s\nBody: %s" % (self.response, body)
diff --git a/tempest/common/utils/__init__.py b/tempest/common/utils/__init__.py
index 81b8110..225a713 100644
--- a/tempest/common/utils/__init__.py
+++ b/tempest/common/utils/__init__.py
@@ -12,30 +12,28 @@
# License for the specific language governing permissions and limitations
# under the License.
+import functools
from functools import partial
-from tempest import config
+import testtools
-from tempest_lib.common.utils import data_utils as lib_data_utils
+from tempest import config
+from tempest.exceptions import InvalidServiceTag
+from tempest.lib.common.utils import data_utils as lib_data_utils
+from tempest.lib import decorators
+
CONF = config.CONF
-PING_IPV4_COMMAND = 'ping -c 3 '
-PING_IPV6_COMMAND = 'ping6 -c 3 '
-PING_PACKET_LOSS_REGEX = '(\d{1,3})\.?\d*\% packet loss'
-
class DataUtils(object):
def __getattr__(self, attr):
- if attr in self.__dict__:
- return self.__dict__[attr]
if attr == 'rand_name':
# NOTE(flwang): This is a proxy to generate a random name that
- # includes a random number and a prefix if one is configured in
- # CONF.resources_prefix
+ # includes a random number and a prefix 'tempest'
attr_obj = partial(lib_data_utils.rand_name,
- prefix=CONF.resources_prefix)
+ prefix='tempest')
else:
attr_obj = getattr(lib_data_utils, attr)
@@ -43,3 +41,89 @@
return attr_obj
data_utils = DataUtils()
+
+
+def get_service_list():
+ service_list = {
+ 'compute': CONF.service_available.nova,
+ 'image': CONF.service_available.glance,
+ 'volume': CONF.service_available.cinder,
+ # NOTE(masayukig): We have two network services which are neutron and
+ # nova-network. And we have no way to know whether nova-network is
+ # available or not. After the pending removal of nova-network from
+ # nova, we can treat the network/neutron case in the same manner as
+ # the other services.
+ 'network': True,
+ # NOTE(masayukig): Tempest tests always require the identity service.
+ # So we should set this True here.
+ 'identity': True,
+ 'object_storage': CONF.service_available.swift,
+ }
+ return service_list
+
+
+def services(*args):
+ """A decorator used to set an attr for each service used in a test case
+
+ This decorator applies a testtools attr for each service that gets
+ exercised by a test case.
+ """
+ def decorator(f):
+ known_services = get_service_list()
+
+ for service in args:
+ if service not in known_services:
+ raise InvalidServiceTag('%s is not a valid service' % service)
+ decorators.attr(type=list(args))(f)
+
+ @functools.wraps(f)
+ def wrapper(*func_args, **func_kwargs):
+ service_list = get_service_list()
+
+ for service in args:
+ if not service_list[service]:
+ msg = 'Skipped because the %s service is not available' % (
+ service)
+ raise testtools.TestCase.skipException(msg)
+ return f(*func_args, **func_kwargs)
+ return wrapper
+ return decorator
+
+
+def requires_ext(**kwargs):
+ """A decorator to skip tests if an extension is not enabled
+
+ @param extension
+ @param service
+ """
+ def decorator(func):
+ @functools.wraps(func)
+ def wrapper(*func_args, **func_kwargs):
+ if not is_extension_enabled(kwargs['extension'],
+ kwargs['service']):
+ msg = "Skipped because %s extension: %s is not enabled" % (
+ kwargs['service'], kwargs['extension'])
+ raise testtools.TestCase.skipException(msg)
+ return func(*func_args, **func_kwargs)
+ return wrapper
+ return decorator
+
+
+def is_extension_enabled(extension_name, service):
+ """A function that will check the list of enabled extensions from config
+
+ """
+ config_dict = {
+ 'compute': CONF.compute_feature_enabled.api_extensions,
+ 'volume': CONF.volume_feature_enabled.api_extensions,
+ 'network': CONF.network_feature_enabled.api_extensions,
+ 'object': CONF.object_storage_feature_enabled.discoverable_apis,
+ 'identity': CONF.identity_feature_enabled.api_extensions
+ }
+ if not config_dict[service]:
+ return False
+ if config_dict[service][0] == 'all':
+ return True
+ if extension_name in config_dict[service]:
+ return True
+ return False
diff --git a/tempest/common/utils/file_utils.py b/tempest/common/utils/file_utils.py
deleted file mode 100644
index 43083f4..0000000
--- a/tempest/common/utils/file_utils.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# 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.
-
-
-def have_effective_read_access(path):
- try:
- fh = open(path, "rb")
- except IOError:
- return False
- fh.close()
- return True
diff --git a/tempest/common/utils/linux/remote_client.py b/tempest/common/utils/linux/remote_client.py
index b76c356..52ccfa9 100644
--- a/tempest/common/utils/linux/remote_client.py
+++ b/tempest/common/utils/linux/remote_client.py
@@ -10,77 +10,68 @@
# License for the specific language governing permissions and limitations
# under the License.
-import netaddr
import re
import time
from oslo_log import log as logging
-import six
-from tempest_lib.common import ssh
from tempest import config
-from tempest import exceptions
+from tempest.lib.common.utils.linux import remote_client
+import tempest.lib.exceptions
CONF = config.CONF
LOG = logging.getLogger(__name__)
-class RemoteClient(object):
+class RemoteClient(remote_client.RemoteClient):
- # NOTE(afazekas): It should always get an address instead of server
- def __init__(self, server, username, password=None, pkey=None):
- ssh_timeout = CONF.validation.ssh_timeout
- network = CONF.validation.network_for_ssh
- ip_version = CONF.validation.ip_version_for_ssh
- connect_timeout = CONF.validation.connect_timeout
- if isinstance(server, six.string_types):
- ip_address = server
- else:
- addresses = server['addresses'][network]
- for address in addresses:
- if address['version'] == ip_version:
- ip_address = address['addr']
- break
- else:
- raise exceptions.ServerUnreachable()
- self.ssh_client = ssh.Client(ip_address, username, password,
- ssh_timeout, pkey=pkey,
- channel_timeout=connect_timeout)
+ # TODO(oomichi): Make this class deprecated after migrating
+ # necessary methods to tempest.lib and cleaning
+ # unnecessary methods up from this class.
+ def __init__(self, ip_address, username, password=None, pkey=None,
+ server=None, servers_client=None):
+ """Executes commands in a VM over ssh
- def exec_command(self, cmd):
- # Shell options below add more clearness on failures,
- # path is extended for some non-cirros guest oses (centos7)
- cmd = CONF.validation.ssh_shell_prologue + " " + cmd
- LOG.debug("Remote command: %s" % cmd)
- return self.ssh_client.exec_command(cmd)
-
- def validate_authentication(self):
- """Validate ssh connection and authentication
-
- This method raises an Exception when the validation fails.
+ :param ip_address: IP address to ssh to
+ :param username: ssh username
+ :param password: ssh password (optional)
+ :param pkey: ssh public key (optional)
+ :param server: server dict, used for debugging purposes
+ :param servers_client: servers client, used for debugging purposes
"""
- self.ssh_client.test_connection_auth()
+ super(RemoteClient, self).__init__(
+ ip_address, username, password=password, pkey=pkey,
+ server=server, servers_client=servers_client,
+ ssh_timeout=CONF.validation.ssh_timeout,
+ connect_timeout=CONF.validation.connect_timeout,
+ console_output_enabled=CONF.compute_feature_enabled.console_output,
+ ssh_shell_prologue=CONF.validation.ssh_shell_prologue,
+ ping_count=CONF.validation.ping_count,
+ ping_size=CONF.validation.ping_size)
- def hostname_equals_servername(self, expected_hostname):
- # Get host name using command "hostname"
- actual_hostname = self.exec_command("hostname").rstrip()
- return expected_hostname == actual_hostname
-
- def get_ram_size_in_mb(self):
- output = self.exec_command('free -m | grep Mem')
- if output:
- return output.split()[1]
-
- def get_number_of_vcpus(self):
- output = self.exec_command('grep -c ^processor /proc/cpuinfo')
- return int(output)
-
- def get_partitions(self):
- # Return the contents of /proc/partitions
- command = 'cat /proc/partitions'
+ # Note that this method will not work on SLES11 guests, as they do
+ # not support the TYPE column on lsblk
+ def get_disks(self):
+ # Select root disk devices as shown by lsblk
+ command = 'lsblk -lb --nodeps'
output = self.exec_command(command)
- return output
+ selected = []
+ pos = None
+ for l in output.splitlines():
+ if pos is None and l.find("TYPE") > 0:
+ pos = l.find("TYPE")
+ # Show header line too
+ selected.append(l)
+ # lsblk lists disk type in a column right-aligned with TYPE
+ elif pos is not None and pos > 0 and l[pos:pos + 4] == "disk":
+ selected.append(l)
+
+ if selected:
+ return "\n".join(selected)
+ else:
+ msg = "'TYPE' column is required but the output doesn't have it: "
+ raise tempest.lib.exceptions.TempestException(msg + output)
def get_boot_time(self):
cmd = 'cut -f1 -d. /proc/uptime'
@@ -94,50 +85,20 @@
cmd = 'sudo sh -c "echo \\"%s\\" >/dev/console"' % message
return self.exec_command(cmd)
- def ping_host(self, host, count=CONF.validation.ping_count,
- size=CONF.validation.ping_size, nic=None):
- addr = netaddr.IPAddress(host)
- cmd = 'ping6' if addr.version == 6 else 'ping'
- if nic:
- cmd = 'sudo {cmd} -I {nic}'.format(cmd=cmd, nic=nic)
- cmd += ' -c{0} -w{0} -s{1} {2}'.format(count, size, host)
- return self.exec_command(cmd)
-
- def set_mac_address(self, nic, address):
- self.set_nic_state(nic=nic, state="down")
- cmd = "sudo ip link set dev {0} address {1}".format(nic, address)
- self.exec_command(cmd)
- self.set_nic_state(nic=nic, state="up")
-
def get_mac_address(self, nic=""):
show_nic = "show {nic} ".format(nic=nic) if nic else ""
cmd = "ip addr %s| awk '/ether/ {print $2}'" % show_nic
return self.exec_command(cmd).strip().lower()
- def get_nic_name(self, address):
+ def get_nic_name_by_mac(self, address):
+ cmd = "ip -o link | awk '/%s/ {print $2}'" % address
+ nic = self.exec_command(cmd)
+ return nic.strip().strip(":").split('@')[0].lower()
+
+ def get_nic_name_by_ip(self, address):
cmd = "ip -o addr | awk '/%s/ {print $2}'" % address
nic = self.exec_command(cmd)
- return nic.strip().strip(":").lower()
-
- def get_ip_list(self):
- cmd = "ip address"
- return self.exec_command(cmd)
-
- def assign_static_ip(self, nic, addr):
- cmd = "sudo ip addr add {ip}/{mask} dev {nic}".format(
- ip=addr, mask=CONF.network.tenant_network_mask_bits,
- nic=nic
- )
- return self.exec_command(cmd)
-
- def set_nic_state(self, nic, state="up"):
- cmd = "sudo ip link set {nic} {state}".format(nic=nic, state=state)
- return self.exec_command(cmd)
-
- def get_pids(self, pr_name):
- # Get pid(s) of a process/program
- cmd = "ps -ef | grep %s | grep -v 'grep' | awk {'print $1'}" % pr_name
- return self.exec_command(cmd).split('\n')
+ return nic.strip().strip(":").split('@')[0].lower()
def get_dns_servers(self):
cmd = 'cat /etc/resolv.conf'
@@ -147,25 +108,22 @@
if len(l) and l[0] == 'nameserver']
return dns_servers
- def send_signal(self, pid, signum):
- cmd = 'sudo /bin/kill -{sig} {pid}'.format(pid=pid, sig=signum)
- return self.exec_command(cmd)
-
def _renew_lease_udhcpc(self, fixed_ip=None):
"""Renews DHCP lease via udhcpc client. """
file_path = '/var/run/udhcpc.'
- nic_name = self.get_nic_name(fixed_ip)
+ nic_name = self.get_nic_name_by_ip(fixed_ip)
pid = self.exec_command('cat {path}{nic}.pid'.
format(path=file_path, nic=nic_name))
pid = pid.strip()
- self.send_signal(pid, 'USR1')
+ cmd = 'sudo /bin/kill -{sig} {pid}'.format(pid=pid, sig='USR1')
+ self.exec_command(cmd)
def _renew_lease_dhclient(self, fixed_ip=None):
"""Renews DHCP lease via dhclient client. """
cmd = "sudo /sbin/dhclient -r && sudo /sbin/dhclient"
self.exec_command(cmd)
- def renew_lease(self, fixed_ip=None):
+ def renew_lease(self, fixed_ip=None, dhcp_client='udhcpc'):
"""Wrapper method for renewing DHCP lease via given client
Supporting:
@@ -174,10 +132,9 @@
"""
# TODO(yfried): add support for dhcpcd
supported_clients = ['udhcpc', 'dhclient']
- dhcp_client = CONF.scenario.dhcp_client
if dhcp_client not in supported_clients:
- raise exceptions.InvalidConfiguration('%s DHCP client unsupported'
- % dhcp_client)
+ raise tempest.lib.exceptions.InvalidConfiguration(
+ '%s DHCP client unsupported' % dhcp_client)
if dhcp_client == 'udhcpc' and not fixed_ip:
raise ValueError("need to set 'fixed_ip' for udhcpc client")
return getattr(self, '_renew_lease_' + dhcp_client)(fixed_ip=fixed_ip)
@@ -191,4 +148,10 @@
def make_fs(self, dev_name, fs='ext4'):
cmd_mkfs = 'sudo /usr/sbin/mke2fs -t %s /dev/%s' % (fs, dev_name)
- self.exec_command(cmd_mkfs)
+ try:
+ self.exec_command(cmd_mkfs)
+ except tempest.lib.exceptions.SSHExecCommandFailed:
+ LOG.error("Couldn't mke2fs")
+ cmd_why = 'sudo ls -lR /dev'
+ LOG.info("Contents of /dev: %s", self.exec_command(cmd_why))
+ raise
diff --git a/tempest/common/utils/net_info.py b/tempest/common/utils/net_info.py
new file mode 100644
index 0000000..9b0a083
--- /dev/null
+++ b/tempest/common/utils/net_info.py
@@ -0,0 +1,25 @@
+# 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 re
+
+RE_OWNER = re.compile('^network:.*router_.*interface.*')
+
+
+def _is_owner_router_interface(owner):
+ return bool(RE_OWNER.match(owner))
+
+
+def is_router_interface_port(port):
+ """Based on the port attributes determines is it a router interface."""
+ return _is_owner_router_interface(port['device_owner'])
diff --git a/tempest/common/utils/net_utils.py b/tempest/common/utils/net_utils.py
new file mode 100644
index 0000000..867b3dd
--- /dev/null
+++ b/tempest/common/utils/net_utils.py
@@ -0,0 +1,73 @@
+# Copyright 2016 Hewlett Packard Enterprise Development Company
+#
+# 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 netaddr
+
+from tempest.lib import exceptions as lib_exc
+
+
+def get_unused_ip_addresses(ports_client, subnets_client,
+ network_id, subnet_id, count):
+
+ """Return a list with the specified number of unused IP addresses
+
+ This method uses the given ports_client to find the specified number of
+ unused IP addresses on the given subnet using the supplied subnets_client
+ """
+
+ ports = ports_client.list_ports(network_id=network_id)['ports']
+ subnet = subnets_client.show_subnet(subnet_id)
+ ip_net = netaddr.IPNetwork(subnet['subnet']['cidr'])
+ subnet_set = netaddr.IPSet(ip_net.iter_hosts())
+ alloc_set = netaddr.IPSet()
+
+ # prune out any addresses already allocated to existing ports
+ for port in ports:
+ for fixed_ip in port.get('fixed_ips'):
+ alloc_set.add(fixed_ip['ip_address'])
+
+ # exclude gateway_ip of subnet
+ gateway_ip = subnet['subnet']['gateway_ip']
+ if gateway_ip:
+ alloc_set.add(gateway_ip)
+
+ av_set = subnet_set - alloc_set
+ addrs = []
+ for cidr in reversed(av_set.iter_cidrs()):
+ for ip in reversed(cidr):
+ addrs.append(str(ip))
+ if len(addrs) == count:
+ return addrs
+ msg = "Insufficient IP addresses available"
+ raise lib_exc.BadRequest(message=msg)
+
+
+def get_ping_payload_size(mtu, ip_version):
+ """Return the maximum size of ping payload that will fit into MTU."""
+ if not mtu:
+ return None
+ if ip_version == 4:
+ ip_header = 20
+ icmp_header = 8
+ else:
+ ip_header = 40
+ icmp_header = 4
+ res = mtu - ip_header - icmp_header
+ if res < 0:
+ raise lib_exc.BadRequest(
+ message='MTU = %(mtu)d is too low for IPv%(ip_version)d' % {
+ 'mtu': mtu,
+ 'ip_version': ip_version,
+ })
+ return res
diff --git a/tempest/common/validation_resources.py b/tempest/common/validation_resources.py
deleted file mode 100644
index bb71a51..0000000
--- a/tempest/common/validation_resources.py
+++ /dev/null
@@ -1,116 +0,0 @@
-# Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
-# 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_log import log as logging
-
-from tempest import config
-from tempest_lib import exceptions as lib_exc
-
-from tempest.common.utils import data_utils
-
-CONF = config.CONF
-LOG = logging.getLogger(__name__)
-
-
-def create_ssh_security_group(os, add_rule=False):
- security_groups_client = os.compute_security_groups_client
- security_group_rules_client = os.security_group_rules_client
- sg_name = data_utils.rand_name('securitygroup-')
- sg_description = data_utils.rand_name('description-')
- security_group = security_groups_client.create_security_group(
- name=sg_name, description=sg_description)['security_group']
- if add_rule:
- security_group_rules_client.create_security_group_rule(
- parent_group_id=security_group['id'], ip_protocol='tcp',
- from_port=22, to_port=22)
- security_group_rules_client.create_security_group_rule(
- parent_group_id=security_group['id'], ip_protocol='icmp',
- from_port=-1, to_port=-1)
- LOG.debug("SSH Validation resource security group with tcp and icmp "
- "rules %s created"
- % sg_name)
- return security_group
-
-
-def create_validation_resources(os, validation_resources=None):
- # Create and Return the validation resources required to validate a VM
- validation_data = {}
- if validation_resources:
- if validation_resources['keypair']:
- keypair_name = data_utils.rand_name('keypair')
- validation_data.update(os.keypairs_client.create_keypair(
- name=keypair_name))
- LOG.debug("Validation resource key %s created" % keypair_name)
- add_rule = False
- if validation_resources['security_group']:
- if validation_resources['security_group_rules']:
- add_rule = True
- validation_data['security_group'] = \
- create_ssh_security_group(os, add_rule)
- if validation_resources['floating_ip']:
- floating_client = os.compute_floating_ips_client
- validation_data.update(floating_client.create_floating_ip())
- return validation_data
-
-
-def clear_validation_resources(os, validation_data=None):
- # Cleanup the vm validation resources
- has_exception = None
- if validation_data:
- if 'keypair' in validation_data:
- keypair_client = os.keypairs_client
- keypair_name = validation_data['keypair']['name']
- try:
- keypair_client.delete_keypair(keypair_name)
- except lib_exc.NotFound:
- LOG.warning("Keypair %s is not found when attempting to delete"
- % keypair_name)
- except Exception as exc:
- LOG.exception('Exception raised while deleting key %s'
- % keypair_name)
- if not has_exception:
- has_exception = exc
- if 'security_group' in validation_data:
- security_group_client = os.compute_security_groups_client
- sec_id = validation_data['security_group']['id']
- try:
- security_group_client.delete_security_group(sec_id)
- security_group_client.wait_for_resource_deletion(sec_id)
- except lib_exc.NotFound:
- LOG.warning("Security group %s is not found when attempting "
- "to delete" % sec_id)
- except lib_exc.Conflict as exc:
- LOG.exception('Conflict while deleting security '
- 'group %s VM might not be deleted ' % sec_id)
- if not has_exception:
- has_exception = exc
- except Exception as exc:
- LOG.exception('Exception raised while deleting security '
- 'group %s ' % sec_id)
- if not has_exception:
- has_exception = exc
- if 'floating_ip' in validation_data:
- floating_client = os.compute_floating_ips_client
- fip_id = validation_data['floating_ip']['id']
- try:
- floating_client.delete_floating_ip(fip_id)
- except lib_exc.NotFound:
- LOG.warning('Floating ip %s not found while attempting to '
- 'delete' % fip_id)
- except Exception as exc:
- LOG.exception('Exception raised while deleting ip %s '
- % fip_id)
- if not has_exception:
- has_exception = exc
- if has_exception:
- raise has_exception
diff --git a/tempest/common/waiters.py b/tempest/common/waiters.py
index 867d3f6..0e86f05 100644
--- a/tempest/common/waiters.py
+++ b/tempest/common/waiters.py
@@ -10,28 +10,31 @@
# License for the specific language governing permissions and limitations
# under the License.
-
+import re
import time
from oslo_log import log as logging
-from tempest_lib.common.utils import misc as misc_utils
-from tempest_lib import exceptions as lib_exc
+from tempest.common import image as common_image
from tempest import config
from tempest import exceptions
+from tempest.lib.common.utils import test_utils
+from tempest.lib import exceptions as lib_exc
+from tempest.lib.services.image.v1 import images_client as images_v1_client
CONF = config.CONF
LOG = logging.getLogger(__name__)
+def _get_task_state(body):
+ return body.get('OS-EXT-STS:task_state', None)
+
+
# NOTE(afazekas): This function needs to know a token and a subject.
def wait_for_server_status(client, server_id, status, ready_wait=True,
extra_timeout=0, raise_on_error=True):
"""Waits for a server to reach a given status."""
- def _get_task_state(body):
- return body.get('OS-EXT-STS:task_state', None)
-
# NOTE(afazekas): UNKNOWN status possible on ERROR
# or in a very early stage.
body = client.show_server(server_id)['server']
@@ -51,9 +54,7 @@
return
# NOTE(afazekas): The instance is in "ready for action state"
# when no task in progress
- # NOTE(afazekas): Converted to string because of the XML
- # responses
- if str(task_state) == "None":
+ if task_state is None:
# without state api extension 3 sec usually enough
time.sleep(CONF.compute.ready_wait)
return
@@ -89,31 +90,43 @@
'timeout': timeout})
message += ' Current status: %s.' % server_status
message += ' Current task state: %s.' % task_state
- caller = misc_utils.find_test_caller()
+ caller = test_utils.find_test_caller()
if caller:
message = '(%s) %s' % (caller, message)
- raise exceptions.TimeoutException(message)
+ raise lib_exc.TimeoutException(message)
old_status = server_status
old_task_state = task_state
def wait_for_server_termination(client, server_id, ignore_error=False):
"""Waits for server to reach termination."""
+ try:
+ body = client.show_server(server_id)['server']
+ except lib_exc.NotFound:
+ return
+ old_status = server_status = body['status']
+ old_task_state = task_state = _get_task_state(body)
start_time = int(time.time())
while True:
+ time.sleep(client.build_interval)
try:
body = client.show_server(server_id)['server']
except lib_exc.NotFound:
return
-
server_status = body['status']
+ task_state = _get_task_state(body)
+ if (server_status != old_status) or (task_state != old_task_state):
+ LOG.info('State transition "%s" ==> "%s" after %d second wait',
+ '/'.join((old_status, str(old_task_state))),
+ '/'.join((server_status, str(task_state))),
+ time.time() - start_time)
if server_status == 'ERROR' and not ignore_error:
- raise exceptions.BuildErrorException(server_id=server_id)
+ raise lib_exc.DeleteErrorException(resource_id=server_id)
if int(time.time()) - start_time >= client.build_timeout:
- raise exceptions.TimeoutException
-
- time.sleep(client.build_interval)
+ raise lib_exc.TimeoutException
+ old_status = server_status
+ old_task_state = task_state
def wait_for_image_status(client, image_id, status):
@@ -122,112 +135,176 @@
The client should have a show_image(image_id) method to get the image.
The client should also have build_interval and build_timeout attributes.
"""
- image = client.show_image(image_id)
- # Compute image client return response wrapped in 'image' element
- # which is not case with glance image client.
- if 'image' in image:
- image = image['image']
- start = int(time.time())
+ if isinstance(client, images_v1_client.ImagesClient):
+ # The 'check_image' method is used here because the show_image method
+ # returns image details plus the image itself which is very expensive.
+ # The 'check_image' method returns just image details.
+ def _show_image_v1(image_id):
+ resp = client.check_image(image_id)
+ return common_image.get_image_meta_from_headers(resp)
- while image['status'] != status:
- time.sleep(client.build_interval)
- image = client.show_image(image_id)
- # Compute image client return response wrapped in 'image' element
- # which is not case with glance image client.
+ show_image = _show_image_v1
+ else:
+ show_image = client.show_image
+
+ current_status = 'An unknown status'
+ start = int(time.time())
+ while int(time.time()) - start < client.build_timeout:
+ image = show_image(image_id)
+ # Compute image client returns response wrapped in 'image' element
+ # which is not the case with Glance image client.
if 'image' in image:
image = image['image']
- status_curr = image['status']
- if status_curr == 'ERROR':
+
+ current_status = image['status']
+ if current_status == status:
+ return
+ if current_status.lower() == 'killed':
+ raise exceptions.ImageKilledException(image_id=image_id,
+ status=status)
+ if current_status.lower() == 'error':
raise exceptions.AddImageException(image_id=image_id)
- # check the status again to avoid a false negative where we hit
- # the timeout at the same time that the image reached the expected
- # status
- if status_curr == status:
- return
+ time.sleep(client.build_interval)
- if int(time.time()) - start >= client.build_timeout:
- message = ('Image %(image_id)s failed to reach %(status)s state'
- '(current state %(status_curr)s) '
- 'within the required time (%(timeout)s s).' %
- {'image_id': image_id,
- 'status': status,
- 'status_curr': status_curr,
- 'timeout': client.build_timeout})
- caller = misc_utils.find_test_caller()
- if caller:
- message = '(%s) %s' % (caller, message)
- raise exceptions.TimeoutException(message)
+ message = ('Image %(image_id)s failed to reach %(status)s state '
+ '(current state %(current_status)s) within the required '
+ 'time (%(timeout)s s).' % {'image_id': image_id,
+ 'status': status,
+ 'current_status': current_status,
+ 'timeout': client.build_timeout})
+ caller = test_utils.find_test_caller()
+ if caller:
+ message = '(%s) %s' % (caller, message)
+ raise lib_exc.TimeoutException(message)
-def wait_for_volume_status(client, volume_id, status):
- """Waits for a Volume to reach a given status."""
- body = client.show_volume(volume_id)['volume']
- volume_status = body['status']
+def wait_for_volume_resource_status(client, resource_id, status):
+ """Waits for a volume resource to reach a given status.
+
+ This function is a common function for volume, snapshot and backup
+ resources. The function extracts the name of the desired resource from
+ the client class name of the resource.
+ """
+ resource_name = re.findall(
+ r'(volume|group-snapshot|snapshot|backup|group)',
+ client.resource_type)[-1].replace('-', '_')
+ show_resource = getattr(client, 'show_' + resource_name)
+ resource_status = show_resource(resource_id)[resource_name]['status']
start = int(time.time())
- while volume_status != status:
+ while resource_status != status:
+ time.sleep(client.build_interval)
+ resource_status = show_resource(resource_id)[
+ '{}'.format(resource_name)]['status']
+ if resource_status == 'error' and resource_status != status:
+ raise exceptions.VolumeResourceBuildErrorException(
+ resource_name=resource_name, resource_id=resource_id)
+ if resource_name == 'volume' and resource_status == 'error_restoring':
+ raise exceptions.VolumeRestoreErrorException(volume_id=resource_id)
+
+ if int(time.time()) - start >= client.build_timeout:
+ message = ('%s %s failed to reach %s status (current %s) '
+ 'within the required time (%s s).' %
+ (resource_name, resource_id, status, resource_status,
+ client.build_timeout))
+ raise lib_exc.TimeoutException(message)
+ LOG.info('%s %s reached %s after waiting for %f seconds',
+ resource_name, resource_id, status, time.time() - start)
+
+
+def wait_for_volume_retype(client, volume_id, new_volume_type):
+ """Waits for a Volume to have a new volume type."""
+ body = client.show_volume(volume_id)['volume']
+ current_volume_type = body['volume_type']
+ start = int(time.time())
+
+ while current_volume_type != new_volume_type:
time.sleep(client.build_interval)
body = client.show_volume(volume_id)['volume']
- volume_status = body['status']
- if volume_status == 'error':
- raise exceptions.VolumeBuildErrorException(volume_id=volume_id)
- if volume_status == 'error_restoring':
- raise exceptions.VolumeRestoreErrorException(volume_id=volume_id)
+ current_volume_type = body['volume_type']
if int(time.time()) - start >= client.build_timeout:
- message = ('Volume %s failed to reach %s status (current %s) '
+ message = ('Volume %s failed to reach %s volume type (current %s) '
'within the required time (%s s).' %
- (volume_id, status, volume_status,
+ (volume_id, new_volume_type, current_volume_type,
client.build_timeout))
- raise exceptions.TimeoutException(message)
+ raise lib_exc.TimeoutException(message)
-def wait_for_snapshot_status(client, snapshot_id, status):
- """Waits for a Snapshot to reach a given status."""
- body = client.show_snapshot(snapshot_id)['snapshot']
- snapshot_status = body['status']
- start = int(time.time())
+def wait_for_qos_operations(client, qos_id, operation, args=None):
+ """Waits for a qos operations to be completed.
- while snapshot_status != status:
- time.sleep(client.build_interval)
- body = client.show_snapshot(snapshot_id)['snapshot']
- snapshot_status = body['status']
- if snapshot_status == 'error':
- raise exceptions.SnapshotBuildErrorException(
- snapshot_id=snapshot_id)
- if int(time.time()) - start >= client.build_timeout:
- message = ('Snapshot %s failed to reach %s status (current %s) '
- 'within the required time (%s s).' %
- (snapshot_id, status, snapshot_status,
- client.build_timeout))
- raise exceptions.TimeoutException(message)
-
-
-def wait_for_bm_node_status(client, node_id, attr, status):
- """Waits for a baremetal node attribute to reach given status.
-
- The client should have a show_node(node_uuid) method to get the node.
+ NOTE : operation value is required for wait_for_qos_operations()
+ operation = 'qos-key' / 'disassociate' / 'disassociate-all'
+ args = keys[] when operation = 'qos-key'
+ args = volume-type-id disassociated when operation = 'disassociate'
+ args = None when operation = 'disassociate-all'
"""
- _, node = client.show_node(node_id)
+ start_time = int(time.time())
+ while True:
+ if operation == 'qos-key-unset':
+ body = client.show_qos(qos_id)['qos_specs']
+ if not any(key in body['specs'] for key in args):
+ return
+ elif operation == 'disassociate':
+ body = client.show_association_qos(qos_id)['qos_associations']
+ if not any(args in body[i]['id'] for i in range(0, len(body))):
+ return
+ elif operation == 'disassociate-all':
+ body = client.show_association_qos(qos_id)['qos_associations']
+ if not body:
+ return
+ else:
+ msg = (" operation value is either not defined or incorrect.")
+ raise lib_exc.UnprocessableEntity(msg)
+
+ if int(time.time()) - start_time >= client.build_timeout:
+ raise lib_exc.TimeoutException
+ time.sleep(client.build_interval)
+
+
+def wait_for_interface_status(client, server_id, port_id, status):
+ """Waits for an interface to reach a given status."""
+ body = (client.show_interface(server_id, port_id)
+ ['interfaceAttachment'])
+ interface_status = body['port_state']
start = int(time.time())
- while node[attr] != status:
+ while(interface_status != status):
time.sleep(client.build_interval)
- _, node = client.show_node(node_id)
- status_curr = node[attr]
- if status_curr == status:
- return
+ body = (client.show_interface(server_id, port_id)
+ ['interfaceAttachment'])
+ interface_status = body['port_state']
- if int(time.time()) - start >= client.build_timeout:
- message = ('Node %(node_id)s failed to reach %(attr)s=%(status)s '
- 'within the required time (%(timeout)s s).' %
- {'node_id': node_id,
- 'attr': attr,
- 'status': status,
- 'timeout': client.build_timeout})
- message += ' Current state of %s: %s.' % (attr, status_curr)
- caller = misc_utils.find_test_caller()
- if caller:
- message = '(%s) %s' % (caller, message)
- raise exceptions.TimeoutException(message)
+ timed_out = int(time.time()) - start >= client.build_timeout
+
+ if interface_status != status and timed_out:
+ message = ('Interface %s failed to reach %s status '
+ '(current %s) within the required time (%s s).' %
+ (port_id, status, interface_status,
+ client.build_timeout))
+ 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 1fc55d9..1fb5c8e 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -15,7 +15,6 @@
from __future__ import print_function
-import logging as std_logging
import os
import tempfile
@@ -23,6 +22,8 @@
from oslo_config import cfg
from oslo_log import log as logging
+from tempest.lib import exceptions
+from tempest.lib.services import clients
from tempest.test_discover import plugins
@@ -56,7 +57,7 @@
"number of concurrent test processes."),
cfg.BoolOpt('use_dynamic_credentials',
default=True,
- help="Allows test cases to create/destroy tenants and "
+ help="Allows test cases to create/destroy projects and "
"users. This option requires that OpenStack Identity "
"API admin credentials are known. If false, isolated "
"test cases and parallel execution, can still be "
@@ -64,9 +65,7 @@
deprecated_opts=[cfg.DeprecatedOpt('allow_tenant_isolation',
group='auth'),
cfg.DeprecatedOpt('allow_tenant_isolation',
- group='compute'),
- cfg.DeprecatedOpt('allow_tenant_isolation',
- group='orchestration')]),
+ group='compute')]),
cfg.ListOpt('tempest_roles',
help="Roles to assign to all users created by tempest",
default=[]),
@@ -81,27 +80,31 @@
default=True,
help="If use_dynamic_credentials is set to True and Neutron "
"is enabled Tempest will try to create a usable network, "
- "subnet, and router when needed for each tenant it "
+ "subnet, and router when needed for each project it "
"creates. However in some neutron configurations, like "
"with VLAN provider networks, this doesn't work. So if "
"set to False the isolated networks will not be created"),
cfg.StrOpt('admin_username',
help="Username for an administrative user. This is needed for "
- "authenticating requests made by tenant isolation to "
+ "authenticating requests made by project isolation to "
"create users and projects",
deprecated_group='identity'),
- cfg.StrOpt('admin_tenant_name',
- help="Tenant name to use for an administrative user. This is "
- "needed for authenticating requests made by tenant "
+ cfg.StrOpt('admin_project_name',
+ help="Project name to use for an administrative user. This is "
+ "needed for authenticating requests made by project "
"isolation to create users and projects",
- deprecated_group='identity'),
+ deprecated_opts=[cfg.DeprecatedOpt('admin_tenant_name',
+ group='auth'),
+ cfg.DeprecatedOpt('admin_tenant_name',
+ group='identity')]),
cfg.StrOpt('admin_password',
- help="Password to use for an administrative user. This is "
- "needed for authenticating requests made by tenant "
+ help="Password to use for an administrative user. This is "
+ "needed for authenticating requests made by project "
"isolation to create users and projects",
secret=True,
deprecated_group='identity'),
cfg.StrOpt('admin_domain_name',
+ default='Default',
help="Admin domain name for authentication (Keystone V3)."
"The same domain applies to user and project",
deprecated_group='identity'),
@@ -126,7 +129,7 @@
cfg.StrOpt('uri_v3',
help='Full URI of the OpenStack Identity API (Keystone), v3'),
cfg.StrOpt('auth_version',
- default='v2',
+ default='v3',
help="Identity API version to be used for authentication "
"for API tests."),
cfg.StrOpt('region',
@@ -154,43 +157,44 @@
choices=['public', 'admin', 'internal',
'publicURL', 'adminURL', 'internalURL'],
help="The endpoint type to use for OpenStack Identity "
- "(Keystone) API v3"),
- cfg.StrOpt('username',
- help="Username to use for Nova API requests.",
- deprecated_for_removal=True),
- cfg.StrOpt('tenant_name',
- help="Tenant name to use for Nova API requests.",
- deprecated_for_removal=True),
+ "(Keystone) API v3. The default value adminURL is "
+ "deprecated and will be modified to publicURL in "
+ "the next release."),
cfg.StrOpt('admin_role',
default='admin',
help="Role required to administrate keystone."),
- cfg.StrOpt('password',
- help="API key to use when authenticating.",
- secret=True,
- deprecated_for_removal=True),
- cfg.StrOpt('domain_name',
- help="Domain name for authentication (Keystone V3)."
- "The same domain applies to user and project",
- deprecated_for_removal=True),
- cfg.StrOpt('alt_username',
- help="Username of alternate user to use for Nova API "
- "requests.",
- deprecated_for_removal=True),
- cfg.StrOpt('alt_tenant_name',
- help="Alternate user's Tenant name to use for Nova API "
- "requests.",
- deprecated_for_removal=True),
- cfg.StrOpt('alt_password',
- help="API key to use when authenticating as alternate user.",
- secret=True,
- deprecated_for_removal=True),
- cfg.StrOpt('alt_domain_name',
- help="Alternate domain name for authentication (Keystone V3)."
- "The same domain applies to user and project",
- deprecated_for_removal=True),
cfg.StrOpt('default_domain_id',
default='default',
help="ID of the default domain"),
+ cfg.BoolOpt('admin_domain_scope',
+ default=False,
+ help="Whether keystone identity v3 policy required "
+ "a domain scoped token to use admin APIs"),
+ # Security Compliance (PCI-DSS)
+ cfg.IntOpt('user_lockout_failure_attempts',
+ default=2,
+ help="The number of unsuccessful login attempts the user is "
+ "allowed before having the account locked."),
+ cfg.IntOpt('user_lockout_duration',
+ default=5,
+ help="The number of seconds a user account will remain "
+ "locked."),
+ cfg.IntOpt('user_unique_last_password_count',
+ default=2,
+ help="The number of passwords for a user that must be unique "
+ "before an old password can be reused."),
+]
+
+service_clients_group = cfg.OptGroup(name='service-clients',
+ title="Service Clients Options")
+
+ServiceClientsGroup = [
+ cfg.IntOpt('http_timeout',
+ default=60,
+ help='Timeout in seconds to wait for the http request to '
+ 'return'),
+ cfg.StrOpt('proxy_url',
+ help='Specify an http proxy to use.')
]
identity_feature_group = cfg.OptGroup(name='identity-feature-enabled',
@@ -202,8 +206,18 @@
help='Does the identity service have delegation and '
'impersonation enabled'),
cfg.BoolOpt('api_v2',
+ default=False,
+ help='Is the v2 identity API enabled',
+ deprecated_for_removal=True,
+ deprecated_reason='The identity v2.0 API was removed in the '
+ 'Queens release. Tests that exercise the '
+ 'v2.0 API will be removed from tempest in '
+ 'the v22.0.0 release. They are kept only to '
+ 'test stable branches.'),
+ cfg.BoolOpt('api_v2_admin',
default=True,
- help='Is the v2 identity API enabled'),
+ help="Is the v2 identity admin API available? This setting "
+ "only applies if api_v2 is set to True."),
cfg.BoolOpt('api_v3',
default=True,
help='Is the v3 identity API enabled'),
@@ -212,7 +226,27 @@
help="A list of enabled identity extensions with a special "
"entry all which indicates every extension is enabled. "
"Empty list indicates all extensions are disabled. "
- "To get the list of extensions run: 'keystone discover'")
+ "To get the list of extensions run: "
+ "'openstack extension list --identity'"),
+ cfg.BoolOpt('domain_specific_drivers',
+ default=False,
+ help='Are domain specific drivers enabled? '
+ 'This configuration value should be same as '
+ '[identity]->domain_specific_drivers_enabled '
+ 'in keystone.conf.'),
+ cfg.BoolOpt('security_compliance',
+ default=False,
+ help='Does the environment have the security compliance '
+ 'settings enabled?'),
+ cfg.BoolOpt('project_tags',
+ default=False,
+ 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',
@@ -246,10 +280,10 @@
"no OS-EXT-STS extension available"),
cfg.StrOpt('fixed_network_name',
help="Name of the fixed network that is visible to all test "
- "tenants. If multiple networks are available for a tenant"
- " this is the network which will be used for creating "
- "servers if tempest does not create a network or a "
- "network is not specified elsewhere. It may be used for "
+ "projects. If multiple networks are available for a "
+ "project, this is the network which will be used for "
+ "creating servers if tempest does not create a network or "
+ "a network is not specified elsewhere. It may be used for "
"ssh validation only if floating IPs are disabled."),
cfg.StrOpt('catalog_type',
default='compute',
@@ -268,26 +302,27 @@
cfg.StrOpt('volume_device_name',
default='vdb',
help="Expected device name when a volume is attached to "
- "an instance"),
+ "an instance. Not all hypervisors guarantee that they "
+ "will respect the user defined device name, tests may "
+ "fail if inappropriate device name is set."),
cfg.IntOpt('shelved_offload_time',
default=0,
help='Time in seconds before a shelved instance is eligible '
'for removing from a host. -1 never offload, 0 offload '
- 'when shelved. This time should be the same as the time '
- 'of nova.conf, and some tests will run for as long as the '
- 'time.'),
+ 'when shelved. This configuration value should be same as '
+ '[nova.DEFAULT]->shelved_offload_time in nova.conf, and '
+ 'some tests will run for as long as the time.'),
cfg.IntOpt('min_compute_nodes',
default=1,
help=('The minimum number of compute nodes expected. This will '
'be utilized by some multinode specific tests to ensure '
'that requests match the expected size of the cluster '
- 'you are testing with.'))
-]
-
-compute_features_group = cfg.OptGroup(name='compute-feature-enabled',
- title="Enabled Compute Service Features")
-
-ComputeFeaturesGroup = [
+ 'you are testing with.')),
+ cfg.StrOpt('hypervisor_type',
+ default=None,
+ help="Hypervisor type of the test target on heterogeneous "
+ "compute environment. The value can be 'QEMU', 'xen' or "
+ "something."),
cfg.StrOpt('min_microversion',
default=None,
help="Lower version of the test target microversion range. "
@@ -306,6 +341,12 @@
"If both values are not specified, Tempest avoids tests "
"which require a microversion. Valid values are string "
"with format 'X.Y' or string 'latest'"),
+]
+
+compute_features_group = cfg.OptGroup(name='compute-feature-enabled',
+ title="Enabled Compute Service Features")
+
+ComputeFeaturesGroup = [
cfg.BoolOpt('disk_config',
default=True,
help="If false, skip disk config tests"),
@@ -314,7 +355,13 @@
help='A list of enabled compute extensions with a special '
'entry all which indicates every extension is enabled. '
'Each extension should be specified with alias name. '
- 'Empty list indicates all extensions are disabled'),
+ 'Empty list indicates all extensions are disabled',
+ deprecated_for_removal=True,
+ deprecated_reason='The Nova extensions API and mechanism '
+ 'is deprecated. This option will be '
+ 'removed when all releases supported '
+ 'by tempest no longer contain the Nova '
+ 'extensions API and mechanism.'),
cfg.BoolOpt('change_password',
default=False,
help="Does the test environment support changing the admin "
@@ -325,7 +372,10 @@
"serial console output?"),
cfg.BoolOpt('resize',
default=False,
- help="Does the test environment support resizing?"),
+ help="Does the test environment support resizing? When you "
+ "enable this feature, 'flavor_ref_alt' should be set and "
+ "it should refer to a larger flavor than 'flavor_ref' "
+ "one."),
cfg.BoolOpt('pause',
default=True,
help="Does the test environment support pausing?"),
@@ -335,10 +385,17 @@
cfg.BoolOpt('suspend',
default=True,
help="Does the test environment support suspend/resume?"),
+ cfg.BoolOpt('cold_migration',
+ default=True,
+ help="Does the test environment support cold migration?"),
cfg.BoolOpt('live_migration',
default=True,
- help="Does the test environment support live migration "
- "available?"),
+ help="Does the test environment support live migration?"),
+ cfg.BoolOpt('live_migrate_back_and_forth',
+ default=False,
+ help="Does the test environment support live migrating "
+ "VM back and forth between different versions of "
+ "nova-compute?"),
cfg.BoolOpt('metadata_service',
default=True,
help="Does the test environment support metadata service? "
@@ -349,20 +406,17 @@
"migration"),
cfg.BoolOpt('block_migrate_cinder_iscsi',
default=False,
- help="Does the test environment block migration support "
- "cinder iSCSI volumes. Note, libvirt doesn't support this, "
- "see https://bugs.launchpad.net/nova/+bug/1398999"),
- # TODO(gilliard): Remove live_migrate_paused_instances at juno-eol.
- cfg.BoolOpt('live_migrate_paused_instances',
- default=False,
- help="Does the test system allow live-migration of paused "
- "instances? Note, this is more than just the ANDing of "
- "paused and live_migrate, but all 3 should be set to True "
- "to run those tests"),
+ help="Does the test environment support block migration with "
+ "Cinder iSCSI volumes. Note: libvirt >= 1.2.17 is required "
+ "to support this if using the libvirt compute driver."),
cfg.BoolOpt('vnc_console',
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 '
@@ -371,6 +425,11 @@
default=False,
help='Enable RDP console. This configuration value should '
'be same as [nova.rdp]->enabled in nova.conf'),
+ cfg.BoolOpt('serial_console',
+ default=False,
+ help='Enable serial console. This configuration value '
+ 'should be the same as [nova.serial_console]->enabled '
+ 'in nova.conf'),
cfg.BoolOpt('rescue',
default=True,
help='Does the test environment support instance rescue '
@@ -379,7 +438,8 @@
default=True,
help='Enables returning of the instance password by the '
'relevant server API calls such as create, rebuild '
- 'or rescue.'),
+ 'or rescue. This configuration value should be same as '
+ 'nova.conf: DEFAULT.enable_instance_password'),
cfg.BoolOpt('interface_attach',
default=True,
help='Does the test environment support dynamic network '
@@ -389,34 +449,51 @@
help='Does the test environment support creating snapshot '
'images of running instances?'),
cfg.BoolOpt('nova_cert',
- default=True,
- help='Does the test environment have the nova cert running?'),
- cfg.BoolOpt('personality',
- default=True,
- help='Does the test environment support server personality'),
- # TODO(mriedem): Remove preserve_ports once juno-eol happens.
- cfg.BoolOpt('preserve_ports',
default=False,
- help='Does Nova preserve preexisting ports from Neutron '
- 'when deleting an instance? This should be set to True '
- 'if testing Kilo+ Nova.'),
+ help='Does the test environment have the nova cert running?',
+ deprecated_for_removal=True,
+ deprecated_reason="On Nova side, the nova-cert service is "
+ "deprecated and the service will be removed "
+ "as early as Ocata."),
+ cfg.BoolOpt('personality',
+ default=False,
+ help='Does the test environment support server personality'),
cfg.BoolOpt('attach_encrypted_volume',
default=True,
help='Does the test environment support attaching an '
'encrypted volume to a running server instance? This may '
'depend on the combination of compute_driver in nova and '
'the volume_driver(s) in cinder.'),
- # TODO(mriedem): Remove allow_duplicate_networks once kilo-eol happens
- # since the option was removed from nova in Liberty and is the default
- # behavior starting in Liberty.
- cfg.BoolOpt('allow_duplicate_networks',
- default=False,
- help='Does the test environment support creating instances '
- 'with multiple ports on the same network? This is only '
- 'valid when using Neutron.'),
cfg.BoolOpt('config_drive',
default=True,
help='Enable special configuration drive with metadata.'),
+ cfg.ListOpt('scheduler_available_filters',
+ default=['all'],
+ help="A list of enabled filters that nova will accept as hints"
+ " to the scheduler when creating a server. A special "
+ "entry 'all' indicates all filters that are included "
+ "with nova are enabled. Empty list indicates all filters "
+ "are disabled. The full list of available filters is in "
+ "nova.conf: DEFAULT.scheduler_available_filters. If the "
+ "default value is overridden in nova.conf by the test "
+ "environment (which means that a different set of "
+ "filters is enabled than what is included in Nova by "
+ "default) then, this option must be configured to "
+ "contain the same filters that Nova uses in the test "
+ "environment."),
+ cfg.BoolOpt('swap_volume',
+ default=False,
+ help='Does the test environment support in-place swapping of '
+ 'volumes attached to a server instance?'),
+ cfg.BoolOpt('volume_backed_live_migration',
+ default=False,
+ help='Does the test environment support volume-backed live '
+ 'migration?'),
+ cfg.BoolOpt('volume_multiattach',
+ default=False,
+ help='Does the test environment support attaching a volume to '
+ 'more than one instance? This depends on hypervisor and '
+ 'volume backend/type and compute API version 2.60.'),
]
@@ -456,7 +533,7 @@
"users can specify."),
cfg.ListOpt('disk_formats',
default=['ami', 'ari', 'aki', 'vhd', 'vmdk', 'raw', 'qcow2',
- 'vdi', 'iso'],
+ 'vdi', 'iso', 'vhdx'],
help="A list of image's disk formats "
"users can specify.")
]
@@ -467,14 +544,20 @@
ImageFeaturesGroup = [
cfg.BoolOpt('api_v2',
default=True,
- help="Is the v2 image API enabled"),
+ help="Is the v2 image API enabled",
+ deprecated_for_removal=True,
+ deprecated_reason='Glance v1 APIs are deprecated and v2 APIs '
+ 'are current one. In future, Tempest will '
+ 'test v2 APIs only so this config option '
+ 'will be removed.'),
cfg.BoolOpt('api_v1',
default=True,
- help="Is the v1 image API enabled"),
- cfg.BoolOpt('deactivate_image',
- default=False,
- help="Is the deactivate-image feature enabled."
- " The feature has been integrated since Kilo."),
+ help="Is the v1 image API enabled",
+ deprecated_for_removal=True,
+ deprecated_reason='Glance v1 APIs are deprecated and v2 APIs '
+ 'are current one. In future, Tempest will '
+ 'test v2 APIs only so this config option '
+ 'will be removed.'),
]
network_group = cfg.OptGroup(name='network',
@@ -495,23 +578,23 @@
choices=['public', 'admin', 'internal',
'publicURL', 'adminURL', 'internalURL'],
help="The endpoint type to use for the network service."),
- cfg.StrOpt('tenant_network_cidr',
+ cfg.StrOpt('project_network_cidr',
default="10.100.0.0/16",
- help="The cidr block to allocate tenant ipv4 subnets from"),
- cfg.IntOpt('tenant_network_mask_bits',
+ help="The cidr block to allocate project ipv4 subnets from"),
+ cfg.IntOpt('project_network_mask_bits',
default=28,
- help="The mask bits for tenant ipv4 subnets"),
- cfg.StrOpt('tenant_network_v6_cidr',
+ help="The mask bits for project ipv4 subnets"),
+ cfg.StrOpt('project_network_v6_cidr',
default="2003::/48",
- help="The cidr block to allocate tenant ipv6 subnets from"),
- cfg.IntOpt('tenant_network_v6_mask_bits',
+ help="The cidr block to allocate project ipv6 subnets from"),
+ cfg.IntOpt('project_network_v6_mask_bits',
default=64,
- help="The mask bits for tenant ipv6 subnets"),
- cfg.BoolOpt('tenant_networks_reachable',
+ help="The mask bits for project ipv6 subnets"),
+ cfg.BoolOpt('project_networks_reachable',
default=False,
- help="Whether tenant networks can be reached directly from "
+ help="Whether project networks can be reached directly from "
"the test client. This must be set to True when the "
- "'fixed' ssh_connect_method is selected."),
+ "'fixed' connect_method is selected."),
cfg.StrOpt('public_network_id',
default="",
help="Id of the public network that provides external "
@@ -539,14 +622,22 @@
" for subnet creation"),
cfg.StrOpt('port_vnic_type',
choices=[None, 'normal', 'direct', 'macvtap'],
- help="vnic_type to use when Launching instances"
+ help="vnic_type to use when launching instances"
" with pre-configured ports."
" Supported ports are:"
" ['normal','direct','macvtap']"),
+ cfg.DictOpt('port_profile',
+ default={},
+ help="port profile to use when launching instances"
+ " with pre-configured ports."),
cfg.ListOpt('default_network',
default=["1.0.0.0/16", "2.0.0.0/16"],
help="List of ip pools"
" for subnetpools creation"),
+ cfg.BoolOpt('shared_physical_network',
+ default=False,
+ help="The environment does not support network separation "
+ "between tenants."),
]
network_feature_group = cfg.OptGroup(name='network-feature-enabled',
@@ -572,41 +663,12 @@
default=True,
help="Does the test environment support changing"
" port admin state"),
-]
-
-messaging_group = cfg.OptGroup(name='messaging',
- title='Messaging Service')
-
-MessagingGroup = [
- cfg.StrOpt('catalog_type',
- default='messaging',
- help='Catalog type of the Messaging service.'),
- cfg.IntOpt('max_queues_per_page',
- default=20,
- help='The maximum number of queue records per page when '
- 'listing queues'),
- cfg.IntOpt('max_queue_metadata',
- default=65536,
- help='The maximum metadata size for a queue'),
- cfg.IntOpt('max_messages_per_page',
- default=20,
- help='The maximum number of queue message per page when '
- 'listing (or) posting messages'),
- cfg.IntOpt('max_message_size',
- default=262144,
- help='The maximum size of a message body'),
- cfg.IntOpt('max_messages_per_claim',
- default=20,
- help='The maximum number of messages per claim'),
- cfg.IntOpt('max_message_ttl',
- default=1209600,
- help='The maximum ttl for a message'),
- cfg.IntOpt('max_claim_ttl',
- default=43200,
- help='The maximum ttl for a claim'),
- cfg.IntOpt('max_claim_grace',
- default=43200,
- help='The maximum grace period for a claim'),
+ cfg.BoolOpt('port_security',
+ default=False,
+ help="Does the test environment support port security?"),
+ cfg.BoolOpt('floating_ips',
+ default=True,
+ help='Does the test environment support floating_ips')
]
validation_group = cfg.OptGroup(name='validation',
@@ -628,25 +690,19 @@
choices=['fixed', 'floating'],
help='Default IP type used for validation: '
'-fixed: uses the first IP belonging to the fixed network '
- '-floating: creates and uses a floating IP',
- deprecated_opts=[cfg.DeprecatedOpt('use_floatingip_for_ssh',
- group='compute')]),
+ '-floating: creates and uses a floating IP'),
cfg.StrOpt('auth_method',
default='keypair',
choices=['keypair'],
help='Default authentication method to the instance. '
'Only ssh via keypair is supported for now. '
- 'Additional methods will be handled in a separate spec.',
- deprecated_opts=[cfg.DeprecatedOpt('ssh_auth_method',
- group='compute')]),
+ 'Additional methods will be handled in a separate spec.'),
cfg.IntOpt('ip_version_for_ssh',
default=4,
help='Default IP version for ssh connections.'),
cfg.IntOpt('ping_timeout',
default=120,
- help='Timeout in seconds to wait for ping to succeed.',
- deprecated_opts=[cfg.DeprecatedOpt('ping_timeout',
- group='compute')]),
+ help='Timeout in seconds to wait for ping to succeed.'),
cfg.IntOpt('connect_timeout',
default=60,
help='Timeout in seconds to wait for the TCP connection to be '
@@ -656,50 +712,32 @@
help='Timeout in seconds to wait for the ssh banner.'),
cfg.StrOpt('image_ssh_user',
default="root",
- help="User name used to authenticate to an instance.",
- deprecated_opts=[cfg.DeprecatedOpt('image_ssh_user',
- group='compute'),
- cfg.DeprecatedOpt('ssh_user',
- group='compute'),
- cfg.DeprecatedOpt('ssh_user',
- group='scenario')]),
+ help="User name used to authenticate to an instance."),
cfg.StrOpt('image_ssh_password',
default="password",
- help="Password used to authenticate to an instance.",
- deprecated_opts=[cfg.DeprecatedOpt('image_ssh_password',
- group='compute')]),
+ help="Password used to authenticate to an instance."),
cfg.StrOpt('ssh_shell_prologue',
default="set -eu -o pipefail; PATH=$$PATH:/sbin;",
help="Shell fragments to use before executing a command "
- "when sshing to a guest.",
- deprecated_opts=[cfg.DeprecatedOpt('ssh_shell_prologue',
- group='compute')]),
+ "when sshing to a guest."),
cfg.IntOpt('ping_size',
default=56,
help="The packet size for ping packets originating "
- "from remote linux hosts",
- deprecated_opts=[cfg.DeprecatedOpt('ping_size',
- group='compute')]),
+ "from remote linux hosts"),
cfg.IntOpt('ping_count',
default=1,
help="The number of ping packets originating from remote "
- "linux hosts",
- deprecated_opts=[cfg.DeprecatedOpt('ping_count',
- group='compute')]),
+ "linux hosts"),
cfg.StrOpt('floating_ip_range',
default='10.0.0.0/29',
help='Unallocated floating IP range, which will be used to '
'test the floating IP bulk feature for CRUD operation. '
'This block must not overlap an existing floating IP '
- 'pool.',
- deprecated_opts=[cfg.DeprecatedOpt('floating_ip_range',
- group='compute')]),
+ 'pool.'),
cfg.StrOpt('network_for_ssh',
default='public',
help="Network used for SSH connections. Ignored if "
- "use_floatingip_for_ssh=true or run_validation=false.",
- deprecated_opts=[cfg.DeprecatedOpt('network_for_ssh',
- group='compute')]),
+ "connect_method=floating."),
]
volume_group = cfg.OptGroup(name='volume',
@@ -714,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='',
@@ -727,22 +765,10 @@
choices=['public', 'admin', 'internal',
'publicURL', 'adminURL', 'internalURL'],
help="The endpoint type to use for the volume service."),
- cfg.StrOpt('backend1_name',
- default='',
- help='Name of the backend1 (must be declared in cinder.conf)',
- deprecated_for_removal=True),
- cfg.StrOpt('backend2_name',
- default='',
- help='Name of the backend2 (must be declared in cinder.conf)',
- deprecated_for_removal=True),
cfg.ListOpt('backend_names',
default=['BACKEND_1', 'BACKEND_2'],
help='A list of backend names separated by comma. '
- 'The backend name must be declared in cinder.conf',
- deprecated_opts=[cfg.DeprecatedOpt('BACKEND_1',
- group='volume'),
- cfg.DeprecatedOpt('BACKEND_2',
- group='volume')]),
+ 'The backend name must be declared in cinder.conf'),
cfg.StrOpt('storage_protocol',
default='iSCSI',
help='Backend protocol to target when creating volume types'),
@@ -755,6 +781,36 @@
cfg.IntOpt('volume_size',
default=1,
help='Default size in GB for volumes created by volumes tests'),
+ cfg.ListOpt('manage_volume_ref',
+ default=['source-name', 'volume-%s'],
+ help="A reference to existing volume for volume manage. "
+ "It contains two elements, the first is ref type "
+ "(like 'source-name', 'source-id', etc), the second is "
+ "volume name template used in storage backend"),
+ cfg.ListOpt('manage_snapshot_ref',
+ default=['source-name', '_snapshot-%s'],
+ help="A reference to existing snapshot for snapshot manage. "
+ "It contains two elements, the first is ref type "
+ "(like 'source-name', 'source-id', etc), the second is "
+ "snapshot name template used in storage backend"),
+ cfg.StrOpt('min_microversion',
+ default=None,
+ help="Lower version of the test target microversion range. "
+ "The format is 'X.Y', where 'X' and 'Y' are int values. "
+ "Tempest selects tests based on the range between "
+ "min_microversion and max_microversion. "
+ "If both values are not specified, Tempest avoids tests "
+ "which require a microversion. Valid values are string "
+ "with format 'X.Y' or string 'latest'",),
+ cfg.StrOpt('max_microversion',
+ default=None,
+ help="Upper version of the test target microversion range. "
+ "The format is 'X.Y', where 'X' and 'Y' are int values. "
+ "Tempest selects tests based on the range between "
+ "min_microversion and max_microversion. "
+ "If both values are not specified, Tempest avoids tests "
+ "which require a microversion. Valid values are string "
+ "with format 'X.Y' or string 'latest'",),
]
volume_feature_group = cfg.OptGroup(name='volume-feature-enabled',
@@ -773,21 +829,37 @@
cfg.BoolOpt('clone',
default=True,
help='Runs Cinder volume clone test'),
+ cfg.BoolOpt('manage_snapshot',
+ default=False,
+ help='Runs Cinder manage snapshot tests'),
+ cfg.BoolOpt('manage_volume',
+ default=False,
+ help='Runs Cinder manage volume tests'),
cfg.ListOpt('api_extensions',
default=['all'],
help='A list of enabled volume extensions with a special '
'entry all which indicates every extension is enabled. '
'Empty list indicates all extensions are disabled'),
cfg.BoolOpt('api_v1',
- default=True,
- help="Is the v1 volume API enabled"),
+ default=False,
+ help="Is the v1 volume API enabled",
+ deprecated_for_removal=True,
+ deprecated_reason="The v1 volume API has been deprecated "
+ "since Juno release, and the API will be "
+ "removed."),
cfg.BoolOpt('api_v2',
default=True,
help="Is the v2 volume API enabled"),
- cfg.BoolOpt('bootable',
+ cfg.BoolOpt('api_v3',
+ default=True,
+ help="Is the v3 volume API enabled"),
+ cfg.BoolOpt('extend_attached_volume',
default=False,
- help='Update bootable status of a volume '
- 'Not implemented on icehouse ')
+ help='Does the cloud support extending the size of a volume '
+ 'which is currently attached to a server instance? This '
+ 'depends on the 3.42 volume API microversion and the '
+ '2.51 compute API microversion. Also, not all volume or '
+ 'compute backends support this operation.')
]
@@ -858,186 +930,6 @@
help="Execute discoverability tests"),
]
-database_group = cfg.OptGroup(name='database',
- title='Database Service Options')
-
-DatabaseGroup = [
- cfg.StrOpt('catalog_type',
- default='database',
- help="Catalog type of the Database service."),
- cfg.StrOpt('db_flavor_ref',
- default="1",
- help="Valid primary flavor to use in database tests."),
- cfg.StrOpt('db_current_version',
- default="v1.0",
- help="Current database version to use in database tests."),
-]
-
-orchestration_group = cfg.OptGroup(name='orchestration',
- title='Orchestration Service Options')
-
-OrchestrationGroup = [
- cfg.StrOpt('catalog_type',
- default='orchestration',
- help="Catalog type of the Orchestration service."),
- cfg.StrOpt('region',
- default='',
- help="The orchestration region name to use. If empty, the "
- "value of identity.region is used instead. If no such "
- "region is found in the service catalog, the first found "
- "one is used."),
- cfg.StrOpt('endpoint_type',
- default='publicURL',
- choices=['public', 'admin', 'internal',
- 'publicURL', 'adminURL', 'internalURL'],
- help="The endpoint type to use for the orchestration service."),
- cfg.StrOpt('stack_owner_role', default='heat_stack_owner',
- help='Role required for users to be able to manage stacks'),
- cfg.IntOpt('build_interval',
- default=1,
- help="Time in seconds between build status checks."),
- cfg.IntOpt('build_timeout',
- default=1200,
- help="Timeout in seconds to wait for a stack to build."),
- cfg.StrOpt('instance_type',
- default='m1.micro',
- help="Instance type for tests. Needs to be big enough for a "
- "full OS plus the test workload"),
- cfg.StrOpt('keypair_name',
- help="Name of existing keypair to launch servers with."),
- cfg.IntOpt('max_template_size',
- default=524288,
- help="Value must match heat configuration of the same name."),
- cfg.IntOpt('max_resources_per_stack',
- default=1000,
- help="Value must match heat configuration of the same name."),
-]
-
-
-telemetry_group = cfg.OptGroup(name='telemetry',
- title='Telemetry Service Options')
-
-TelemetryGroup = [
- cfg.StrOpt('catalog_type',
- default='metering',
- help="Catalog type of the Telemetry service."),
- cfg.StrOpt('endpoint_type',
- default='publicURL',
- choices=['public', 'admin', 'internal',
- 'publicURL', 'adminURL', 'internalURL'],
- help="The endpoint type to use for the telemetry service."),
- cfg.BoolOpt('too_slow_to_test',
- default=True,
- deprecated_for_removal=True,
- help="This variable is used as flag to enable "
- "notification tests")
-]
-
-alarming_group = cfg.OptGroup(name='alarming',
- title='Alarming Service Options')
-
-AlarmingGroup = [
- cfg.StrOpt('catalog_type',
- default='alarming',
- help="Catalog type of the Alarming service."),
- cfg.StrOpt('endpoint_type',
- default='publicURL',
- choices=['public', 'admin', 'internal',
- 'publicURL', 'adminURL', 'internalURL'],
- help="The endpoint type to use for the alarming service."),
-]
-
-
-telemetry_feature_group = cfg.OptGroup(name='telemetry-feature-enabled',
- title='Enabled Ceilometer Features')
-
-TelemetryFeaturesGroup = [
- cfg.BoolOpt('events',
- default=False,
- help="Runs Ceilometer event-related tests"),
-]
-
-
-dashboard_group = cfg.OptGroup(name="dashboard",
- title="Dashboard options")
-
-DashboardGroup = [
- cfg.StrOpt('dashboard_url',
- default='http://localhost/',
- help="Where the dashboard can be found"),
- cfg.StrOpt('login_url',
- default='http://localhost/auth/login/',
- help="Login page for the dashboard",
- deprecated_for_removal=True),
-]
-
-
-data_processing_group = cfg.OptGroup(name="data-processing",
- title="Data Processing options")
-
-DataProcessingGroup = [
- cfg.StrOpt('catalog_type',
- default='data-processing',
- deprecated_group="data_processing",
- help="Catalog type of the data processing service."),
- cfg.StrOpt('endpoint_type',
- default='publicURL',
- choices=['public', 'admin', 'internal',
- 'publicURL', 'adminURL', 'internalURL'],
- deprecated_group="data_processing",
- help="The endpoint type to use for the data processing "
- "service."),
-]
-
-
-data_processing_feature_group = cfg.OptGroup(
- name="data-processing-feature-enabled",
- title="Enabled Data Processing features")
-
-DataProcessingFeaturesGroup = [
- cfg.ListOpt('plugins',
- default=["vanilla", "hdp"],
- deprecated_group="data_processing-feature-enabled",
- help="List of enabled data processing plugins")
-]
-
-stress_group = cfg.OptGroup(name='stress', title='Stress Test Options')
-
-StressGroup = [
- cfg.StrOpt('nova_logdir',
- help='Directory containing log files on the compute nodes'),
- cfg.IntOpt('max_instances',
- default=16,
- help='Maximum number of instances to create during test.'),
- cfg.StrOpt('controller',
- help='Controller host.'),
- # new stress options
- cfg.StrOpt('target_controller',
- help='Controller host.'),
- cfg.StrOpt('target_ssh_user',
- help='ssh user.'),
- cfg.StrOpt('target_private_key_path',
- help='Path to private key.'),
- cfg.StrOpt('target_logfiles',
- help='regexp for list of log files.'),
- cfg.IntOpt('log_check_interval',
- default=60,
- help='time (in seconds) between log file error checks.'),
- cfg.IntOpt('default_thread_number_per_action',
- default=4,
- help='The number of threads created while stress test.'),
- cfg.BoolOpt('leave_dirty_stack',
- default=False,
- help='Prevent the cleaning (tearDownClass()) between'
- ' each stress test run if an exception occurs'
- ' during this run.'),
- cfg.BoolOpt('full_clean_stack',
- default=False,
- help='Allows a full cleaning process after a stress test.'
- ' Caution : this cleanup will remove every objects of'
- ' every tenant.')
-]
-
scenario_group = cfg.OptGroup(name='scenario', title='Scenario Test Options')
@@ -1045,7 +937,8 @@
cfg.StrOpt('img_dir',
default='/opt/stack/new/devstack/files/images/'
'cirros-0.3.1-x86_64-uec',
- help='Directory containing image files'),
+ help='Directory containing image files',
+ deprecated_for_removal=True),
cfg.StrOpt('img_file', deprecated_name='qcow2_img_file',
default='cirros-0.3.1-x86_64-disk.img',
help='Image file name'),
@@ -1059,18 +952,16 @@
'Use for custom images which require them'),
cfg.StrOpt('ami_img_file',
default='cirros-0.3.1-x86_64-blank.img',
- help='AMI image file name'),
+ help='AMI image file name',
+ deprecated_for_removal=True),
cfg.StrOpt('ari_img_file',
default='cirros-0.3.1-x86_64-initrd',
- help='ARI image file name'),
+ help='ARI image file name',
+ deprecated_for_removal=True),
cfg.StrOpt('aki_img_file',
default='cirros-0.3.1-x86_64-vmlinuz',
- help='AKI image file name'),
- cfg.IntOpt(
- 'large_ops_number',
- default=0,
- help="specifies how many resources to request at once. Used "
- "for large operations testing."),
+ help='AKI image file name',
+ deprecated_for_removal=True),
# TODO(yfried): add support for dhcpcd
cfg.StrOpt('dhcp_client',
default='udhcpc',
@@ -1100,30 +991,6 @@
cfg.BoolOpt('nova',
default=True,
help="Whether or not nova is expected to be available"),
- cfg.BoolOpt('heat',
- default=False,
- help="Whether or not Heat is expected to be available"),
- cfg.BoolOpt('ceilometer',
- default=True,
- help="Whether or not Ceilometer is expected to be available"),
- cfg.BoolOpt('aodh',
- default=False,
- help="Whether or not Aodh is expected to be available"),
- cfg.BoolOpt('horizon',
- default=True,
- help="Whether or not Horizon is expected to be available"),
- cfg.BoolOpt('sahara',
- default=False,
- help="Whether or not Sahara is expected to be available"),
- cfg.BoolOpt('ironic',
- default=False,
- help="Whether or not Ironic is expected to be available"),
- cfg.BoolOpt('trove',
- default=False,
- help="Whether or not Trove is expected to be available"),
- cfg.BoolOpt('zaqar',
- default=False,
- help="Whether or not Zaqar is expected to be available"),
]
debug_group = cfg.OptGroup(name="debug",
@@ -1152,87 +1019,16 @@
""")
]
-input_scenario_group = cfg.OptGroup(name="input-scenario",
- title="Filters and values for"
- " input scenarios")
-
-InputScenarioGroup = [
- cfg.StrOpt('image_regex',
- default='^cirros-0.3.1-x86_64-uec$',
- help="Matching images become parameters for scenario tests"),
- cfg.StrOpt('flavor_regex',
- default='^m1.nano$',
- help="Matching flavors become parameters for scenario tests"),
- cfg.StrOpt('non_ssh_image_regex',
- default='^.*[Ww]in.*$',
- help="SSH verification in tests is skipped"
- "for matching images"),
- cfg.StrOpt('ssh_user_regex',
- default="[[\"^.*[Cc]irros.*$\", \"cirros\"]]",
- help="List of user mapped to regex "
- "to matching image names."),
-]
-
-
-baremetal_group = cfg.OptGroup(name='baremetal',
- title='Baremetal provisioning service options',
- help='When enabling baremetal tests, Nova '
- 'must be configured to use the Ironic '
- 'driver. The following parameters for the '
- '[compute] section must be disabled: '
- 'console_output, interface_attach, '
- 'live_migration, pause, rescue, resize '
- 'shelve, snapshot, and suspend')
-
-BaremetalGroup = [
- cfg.StrOpt('catalog_type',
- default='baremetal',
- help="Catalog type of the baremetal provisioning service"),
- cfg.BoolOpt('driver_enabled',
- default=False,
- help="Whether the Ironic nova-compute driver is enabled"),
- cfg.StrOpt('driver',
- default='fake',
- help="Driver name which Ironic uses"),
- cfg.StrOpt('endpoint_type',
- default='publicURL',
- choices=['public', 'admin', 'internal',
- 'publicURL', 'adminURL', 'internalURL'],
- help="The endpoint type to use for the baremetal provisioning "
- "service"),
- cfg.IntOpt('active_timeout',
- default=300,
- help="Timeout for Ironic node to completely provision"),
- cfg.IntOpt('association_timeout',
- default=30,
- help="Timeout for association of Nova instance and Ironic "
- "node"),
- cfg.IntOpt('power_timeout',
- default=60,
- help="Timeout for Ironic power transitions."),
- cfg.IntOpt('unprovision_timeout',
- default=300,
- help="Timeout for unprovisioning an Ironic node. "
- "Takes longer since Kilo as Ironic performs an extra "
- "step in Node cleaning.")
-]
-
-negative_group = cfg.OptGroup(name='negative', title="Negative Test Options")
-
-NegativeGroup = [
- cfg.StrOpt('test_generator',
- default='tempest.common.' +
- 'generator.negative_generator.NegativeTestGenerator',
- help="Test generator class for all negative tests"),
-]
-
DefaultGroup = [
- cfg.StrOpt('resources_prefix',
- default='tempest',
- help="Prefix to be added when generating the name for "
- "test resources. It can be used to discover all "
- "resources associated with a specific test run when "
- "running tempest on a real-life cloud"),
+ cfg.BoolOpt('pause_teardown',
+ default=False,
+ help="""Whether to pause a test in global teardown.
+
+The best use case is investigating used resources of one test.
+A test can be run as follows:
+ $ ostestr --pdb TEST_ID
+or
+ $ python -m testtools.run TEST_ID"""),
]
_opts = [
@@ -1240,32 +1036,20 @@
(compute_group, ComputeGroup),
(compute_features_group, ComputeFeaturesGroup),
(identity_group, IdentityGroup),
+ (service_clients_group, ServiceClientsGroup),
(identity_feature_group, IdentityFeatureGroup),
(image_group, ImageGroup),
(image_feature_group, ImageFeaturesGroup),
(network_group, NetworkGroup),
(network_feature_group, NetworkFeaturesGroup),
- (messaging_group, MessagingGroup),
(validation_group, ValidationGroup),
(volume_group, VolumeGroup),
(volume_feature_group, VolumeFeaturesGroup),
(object_storage_group, ObjectStoreGroup),
(object_storage_feature_group, ObjectStoreFeaturesGroup),
- (database_group, DatabaseGroup),
- (orchestration_group, OrchestrationGroup),
- (telemetry_group, TelemetryGroup),
- (telemetry_feature_group, TelemetryFeaturesGroup),
- (alarming_group, AlarmingGroup),
- (dashboard_group, DashboardGroup),
- (data_processing_group, DataProcessingGroup),
- (data_processing_feature_group, DataProcessingFeaturesGroup),
- (stress_group, StressGroup),
(scenario_group, ScenarioGroup),
(service_available_group, ServiceAvailableGroup),
(debug_group, DebugGroup),
- (baremetal_group, BaremetalGroup),
- (input_scenario_group, InputScenarioGroup),
- (negative_group, NegativeGroup),
(None, DefaultGroup)
]
@@ -1286,12 +1070,15 @@
generator to discover the options exposed to users.
"""
ext_plugins = plugins.TempestTestPluginManager()
- opt_list = [(getattr(g, 'name', None), o) for g, o in _opts]
+ # Make a shallow copy of the options list that can be
+ # extended by plugins. Send back the group object
+ # to allow group help text to be generated.
+ opt_list = [(g, o) for g, o in _opts]
opt_list.extend(ext_plugins.get_plugin_options_list())
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."""
@@ -1304,10 +1091,17 @@
return getattr(_CONF, attr)
def _set_attrs(self):
+ # This methods ensures that config options in Tempest as well as
+ # in Tempest plugins can be accessed via:
+ # CONF.<normalised_group_name>.<key_name>
+ # where:
+ # normalised_group_name = group_name.replace('-', '_')
+ # Attributes are set at __init__ time *only* for known option groups
self.auth = _CONF.auth
self.compute = _CONF.compute
self.compute_feature_enabled = _CONF['compute-feature-enabled']
self.identity = _CONF.identity
+ self.service_clients = _CONF['service-clients']
self.identity_feature_enabled = _CONF['identity-feature-enabled']
self.image = _CONF.image
self.image_feature_enabled = _CONF['image-feature-enabled']
@@ -1319,29 +1113,27 @@
self.object_storage = _CONF['object-storage']
self.object_storage_feature_enabled = _CONF[
'object-storage-feature-enabled']
- self.database = _CONF.database
- self.orchestration = _CONF.orchestration
- self.messaging = _CONF.messaging
- self.telemetry = _CONF.telemetry
- self.telemetry_feature_enabled = _CONF['telemetry-feature-enabled']
- self.dashboard = _CONF.dashboard
- self.data_processing = _CONF['data-processing']
- self.data_processing_feature_enabled = _CONF[
- 'data-processing-feature-enabled']
- self.stress = _CONF.stress
self.scenario = _CONF.scenario
self.service_available = _CONF.service_available
self.debug = _CONF.debug
- self.baremetal = _CONF.baremetal
- self.input_scenario = _CONF['input-scenario']
- self.negative = _CONF.negative
- _CONF.set_default('domain_name',
- self.auth.default_credentials_domain_name,
- group='identity')
- _CONF.set_default('alt_domain_name',
- self.auth.default_credentials_domain_name,
- group='identity')
logging.tempest_set_log_file('tempest.log')
+ # Setting attributes for plugins
+ # NOTE(andreaf) Plugins have no access to the TempestConfigPrivate
+ # instance at discovery time, so they have no way of setting these
+ # aliases themselves.
+ ext_plugins = plugins.TempestTestPluginManager()
+ for group, _ in ext_plugins.get_plugin_options_list():
+ if isinstance(group, cfg.OptGroup):
+ # If we have an OptGroup
+ group_name = group.name
+ group_dest = group.dest
+ else:
+ # If we have a group name as a string
+ group_name = group
+ group_dest = group.replace('-', '_')
+ # NOTE(andreaf) We can set the attribute safely here since in
+ # case of name conflict we would not have reached this point.
+ setattr(self, group_dest, _CONF[group_name])
def __init__(self, parse_conf=True, config_path=None):
"""Initialize a configuration from a conf directory and conf file."""
@@ -1374,18 +1166,19 @@
_CONF([], project='tempest')
logging_cfg_path = "%s/logging.conf" % os.path.dirname(path)
- if (not hasattr(_CONF, 'log_config_append') and
+ if ((not hasattr(_CONF, 'log_config_append') or
+ _CONF.log_config_append is None) and
os.path.isfile(logging_cfg_path)):
# if logging conf is in place we need to set log_config_append
_CONF.log_config_append = logging_cfg_path
logging.setup(_CONF, 'tempest')
LOG = logging.getLogger('tempest')
- LOG.info("Using tempest config file %s" % path)
+ LOG.info("Using tempest config file %s", path)
register_opts()
self._set_attrs()
if parse_conf:
- _CONF.log_opt_values(LOG, std_logging.DEBUG)
+ _CONF.log_opt_values(LOG, logging.DEBUG)
class TempestConfigProxy(object):
@@ -1393,14 +1186,14 @@
_path = None
_extra_log_defaults = [
- ('paramiko.transport', std_logging.INFO),
- ('requests.packages.urllib3.connectionpool', std_logging.WARN),
+ ('paramiko.transport', logging.INFO),
+ ('requests.packages.urllib3.connectionpool', logging.WARN),
]
def _fix_log_levels(self):
"""Tweak the oslo log defaults."""
for name, level in self._extra_log_defaults:
- std_logging.getLogger(name).setLevel(level)
+ logging.getLogger(name).logger.setLevel(level)
def __getattr__(self, attr):
if not self._config:
@@ -1409,6 +1202,21 @@
lockutils.set_defaults(lock_dir)
self._config = TempestConfigPrivate(config_path=self._path)
+ # Pushing tempest internal service client configuration to the
+ # service clients register. Doing this in the config module ensures
+ # that the configuration is available by the time we register the
+ # service clients.
+ # NOTE(andreaf) This has to be done at the time the first
+ # attribute is accessed, to ensure all plugins have been already
+ # loaded, options registered, and _config is set.
+ _register_tempest_service_clients()
+
+ # Registering service clients and pushing their configuration to
+ # the service clients register. Doing this in the config module
+ # ensures that the configuration is available by the time we
+ # discover tests from plugins.
+ plugins.TempestTestPluginManager()._register_service_clients()
+
return getattr(self._config, attr)
def set_config_path(self, path):
@@ -1416,3 +1224,114 @@
CONF = TempestConfigProxy()
+
+
+def service_client_config(service_client_name=None):
+ """Return a dict with the parameters to init service clients
+
+ Extracts from CONF the settings specific to the service_client_name and
+ api_version, and formats them as dict ready to be passed to the service
+ clients __init__:
+
+ * `region` (default to identity)
+ * `catalog_type`
+ * `endpoint_type`
+ * `build_timeout` (object-storage and identity default to compute)
+ * `build_interval` (object-storage and identity default to compute)
+
+ The following common settings are always returned, even if
+ `service_client_name` is None:
+
+ * `disable_ssl_certificate_validation`
+ * `ca_certs`
+ * `trace_requests`
+ * `http_timeout`
+ * `proxy_url`
+
+ The dict returned by this does not fit a few service clients:
+
+ * The endpoint type is not returned for identity client, since it takes
+ three different values for v2 admin, v2 public and v3
+ * The `ServersClient` from compute accepts an optional
+ `enable_instance_password` parameter, which is not returned.
+ * The `VolumesClient` for both v1 and v2 volume accept an optional
+ `default_volume_size` parameter, which is not returned.
+ * The `TokenClient` and `V3TokenClient` have a very different
+ interface, only auth_url is needed for them.
+
+ :param service_client_name: str Name of the service. Supported values are
+ 'compute', 'identity', 'image', 'network', 'object-storage', 'volume'
+ :return: dictionary of __init__ parameters for the service clients
+ :rtype: dict
+ """
+ _parameters = {
+ 'disable_ssl_certificate_validation':
+ CONF.identity.disable_ssl_certificate_validation,
+ 'ca_certs': CONF.identity.ca_certificates_file,
+ 'trace_requests': CONF.debug.trace_requests,
+ 'http_timeout': CONF.service_clients.http_timeout,
+ 'proxy_url': CONF.service_clients.proxy_url,
+ }
+
+ if service_client_name is None:
+ return _parameters
+
+ # Get the group of options first, by normalising the service_group_name
+ # Services with a '-' in the name have an '_' in the option group name
+ config_group = service_client_name.replace('-', '_')
+ # NOTE(andreaf) Check if the config group exists. This allows for this
+ # helper to be used for settings from registered plugins as well
+ try:
+ options = getattr(CONF, config_group)
+ except cfg.NoSuchOptError:
+ # Option group not defined
+ raise exceptions.UnknownServiceClient(services=service_client_name)
+ # Set endpoint_type
+ # Identity uses different settings depending on API version, so do not
+ # return the endpoint at all.
+ if service_client_name != 'identity':
+ _parameters['endpoint_type'] = getattr(options, 'endpoint_type')
+ # Set build_*
+ # Object storage and identity groups do not have conf settings for
+ # build_* parameters, and we default to compute in any case
+ for setting in ['build_timeout', 'build_interval']:
+ if not hasattr(options, setting) or not getattr(options, setting):
+ _parameters[setting] = getattr(CONF.compute, setting)
+ else:
+ _parameters[setting] = getattr(options, setting)
+ # Set region
+ # If a service client does not define region or region is not set
+ # default to the identity region
+ if not hasattr(options, 'region') or not getattr(options, 'region'):
+ _parameters['region'] = CONF.identity.region
+ else:
+ _parameters['region'] = getattr(options, 'region')
+ # Set service
+ _parameters['service'] = getattr(options, 'catalog_type')
+ return _parameters
+
+
+def _register_tempest_service_clients():
+ # Register tempest own service clients using the same mechanism used
+ # for external plugins.
+ # The configuration data is pushed to the registry so that automatic
+ # configuration of tempest own service clients is possible both for
+ # tempest as well as for the plugins.
+ service_clients = clients.tempest_modules()
+ registry = clients.ClientsRegistry()
+ all_clients = []
+ for service_client in service_clients:
+ module = service_clients[service_client]
+ configs = service_client.split('.')[0]
+ service_client_data = dict(
+ name=service_client.replace('.', '_').replace('-', '_'),
+ service_version=service_client,
+ module_path=module.__name__,
+ client_names=module.__all__,
+ **service_client_config(configs)
+ )
+ all_clients.append(service_client_data)
+ # NOTE(andreaf) Internal service clients do not actually belong
+ # to a plugin, so using '__tempest__' to indicate a virtual plugin
+ # which holds internal service clients.
+ registry.register_service_client('__tempest__', all_clients)
diff --git a/tempest/exceptions.py b/tempest/exceptions.py
index 1d725af..a430d5d 100644
--- a/tempest/exceptions.py
+++ b/tempest/exceptions.py
@@ -13,186 +13,44 @@
# License for the specific language governing permissions and limitations
# under the License.
-import testtools
+
+from tempest.lib import exceptions
-class TempestException(Exception):
- """Base Tempest Exception
-
- To correctly use this class, inherit from it and define
- a 'message' property. That message will get printf'd
- with the keyword arguments provided to the constructor.
- """
- message = "An unknown exception occurred"
-
- def __init__(self, *args, **kwargs):
- super(TempestException, self).__init__()
- try:
- self._error_string = self.message % kwargs
- except Exception:
- # at least get the core message out if something happened
- self._error_string = self.message
- if len(args) > 0:
- # If there is a non-kwarg parameter, assume it's the error
- # message or reason description and tack it on to the end
- # of the exception message
- # Convert all arguments into their string representations...
- args = ["%s" % arg for arg in args]
- self._error_string = (self._error_string +
- "\nDetails: %s" % '\n'.join(args))
-
- def __str__(self):
- return self._error_string
-
-
-class RestClientException(TempestException,
- testtools.TestCase.failureException):
- pass
-
-
-class InvalidConfiguration(TempestException):
- message = "Invalid Configuration"
-
-
-class InvalidCredentials(TempestException):
- message = "Invalid Credentials"
-
-
-class InvalidServiceTag(TempestException):
- message = "Invalid service tag"
-
-
-class InvalidIdentityVersion(TempestException):
- message = "Invalid version %(identity_version)s of the identity service"
-
-
-class TimeoutException(TempestException):
- message = "Request timed out"
-
-
-class BuildErrorException(TempestException):
+class BuildErrorException(exceptions.TempestException):
message = "Server %(server_id)s failed to build and is in ERROR status"
-class ImageKilledException(TempestException):
+class SnapshotNotFoundException(exceptions.TempestException):
+ message = "Server snapshot image %(image_id)s not found."
+
+
+class ImageKilledException(exceptions.TempestException):
message = "Image %(image_id)s 'killed' while waiting for '%(status)s'"
-class AddImageException(TempestException):
+class AddImageException(exceptions.TempestException):
message = "Image %(image_id)s failed to become ACTIVE in the allotted time"
-class EC2RegisterImageException(TempestException):
- message = ("Image %(image_id)s failed to become 'available' "
- "in the allotted time")
+class VolumeResourceBuildErrorException(exceptions.TempestException):
+ message = ("%(resource_name)s %(resource_id)s failed to build and is in "
+ "ERROR status")
-class VolumeBuildErrorException(TempestException):
- message = "Volume %(volume_id)s failed to build and is in ERROR status"
-
-
-class VolumeRestoreErrorException(TempestException):
+class VolumeRestoreErrorException(exceptions.TempestException):
message = "Volume %(volume_id)s failed to restore and is in ERROR status"
-class SnapshotBuildErrorException(TempestException):
- message = "Snapshot %(snapshot_id)s failed to build and is in ERROR status"
-
-
-class VolumeBackupException(TempestException):
- message = "Volume backup %(backup_id)s failed and is in ERROR status"
-
-
-class StackBuildErrorException(TempestException):
+class StackBuildErrorException(exceptions.TempestException):
message = ("Stack %(stack_identifier)s is in %(stack_status)s status "
"due to '%(stack_status_reason)s'")
-class StackResourceBuildErrorException(TempestException):
- message = ("Resource %(resource_name)s in stack %(stack_identifier)s is "
- "in %(resource_status)s status due to "
- "'%(resource_status_reason)s'")
+class ServerUnreachable(exceptions.TempestException):
+ message = ("Server %(server_id)s is not reachable via "
+ "the configured network")
-class AuthenticationFailure(TempestException):
- message = ("Authentication with user %(user)s and password "
- "%(password)s failed auth using tenant %(tenant)s.")
-
-
-class EndpointNotFound(TempestException):
- message = "Endpoint not found"
-
-
-class ImageFault(TempestException):
- message = "Got image fault"
-
-
-class IdentityError(TempestException):
- message = "Got identity error"
-
-
-class ServerUnreachable(TempestException):
- message = "The server is not reachable via the configured network"
-
-
-class TearDownException(TempestException):
- message = "%(num)d cleanUp operation failed"
-
-
-# NOTE(andreaf) This exception is added here to facilitate the migration
-# of get_network_from_name and preprov_creds to tempest-lib, and it should
-# be migrated along with them
-class InvalidTestResource(TempestException):
- message = "%(name) is not a valid %(type), or the name is ambiguous"
-
-
-class RFCViolation(RestClientException):
- message = "RFC Violation"
-
-
-class InvalidHttpSuccessCode(RestClientException):
- message = "The success code is different than the expected one"
-
-
-class BadRequest(RestClientException):
- message = "Bad request"
-
-
-class ResponseWithNonEmptyBody(RFCViolation):
- message = ("RFC Violation! Response with %(status)d HTTP Status Code "
- "MUST NOT have a body")
-
-
-class ResponseWithEntity(RFCViolation):
- message = ("RFC Violation! Response with 205 HTTP Status Code "
- "MUST NOT have an entity")
-
-
-class InvalidHTTPResponseHeader(RestClientException):
- message = "HTTP response header is invalid"
-
-
-class InvalidStructure(TempestException):
- message = "Invalid structure of table with details"
-
-
-class InvalidAPIVersionString(TempestException):
- message = ("API Version String %(version)s is of invalid format. Must "
- "be of format MajorNum.MinorNum or string 'latest'.")
-
-
-class CommandFailed(Exception):
- def __init__(self, returncode, cmd, output, stderr):
- super(CommandFailed, self).__init__()
- self.returncode = returncode
- self.cmd = cmd
- self.stdout = output
- self.stderr = stderr
-
- def __str__(self):
- return ("Command '%s' returned non-zero exit status %d.\n"
- "stdout:\n%s\n"
- "stderr:\n%s" % (self.cmd,
- self.returncode,
- self.stdout,
- self.stderr))
+class InvalidServiceTag(exceptions.TempestException):
+ message = "Invalid service tag"
diff --git a/tempest/hacking/checks.py b/tempest/hacking/checks.py
index 1dad3ba..b6e7f8c 100644
--- a/tempest/hacking/checks.py
+++ b/tempest/hacking/checks.py
@@ -19,8 +19,7 @@
PYTHON_CLIENTS = ['cinder', 'glance', 'keystone', 'nova', 'swift', 'neutron',
- 'trove', 'ironic', 'savanna', 'heat', 'ceilometer',
- 'zaqar', 'sahara']
+ 'ironic', 'heat', 'sahara']
PYTHON_CLIENT_RE = re.compile('import (%s)client' % '|'.join(PYTHON_CLIENTS))
TEST_DEFINITION = re.compile(r'^\s*def test.*')
@@ -34,6 +33,7 @@
METHOD_GET_RESOURCE = re.compile(r"^\s*def (list|show)\_.+")
METHOD_DELETE_RESOURCE = re.compile(r"^\s*def delete_.+")
CLASS = re.compile(r"^class .+")
+EX_ATTRIBUTE = re.compile(r'(\s+|\()(e|ex|exc|exception).message(\s+|\))')
def import_no_clients_in_api_and_scenario_tests(physical_line, filename):
@@ -69,10 +69,12 @@
if pep8.noqa(physical_line):
return
- if 'tempest/test.py' not in filename:
- if SETUP_TEARDOWN_CLASS_DEFINITION.match(physical_line):
- return (physical_line.find('def'),
- "T105: (setUp|tearDown)Class can not be used in tests")
+ if 'tempest/test.py' in filename or 'tempest/lib/' in filename:
+ return
+
+ if SETUP_TEARDOWN_CLASS_DEFINITION.match(physical_line):
+ return (physical_line.find('def'),
+ "T105: (setUp|tearDown)Class can not be used in tests")
def no_vi_headers(physical_line, line_number, lines):
@@ -117,11 +119,6 @@
T108
"""
- if './tempest/api/network/' in filename:
- # Network API tests are migrating from Tempest to Neutron repo now.
- # So here should avoid network API tests checks.
- return
-
msg = "T108: hyphen should not be specified at the end of rand_name()"
if RAND_NAME_HYPHEN_RE.match(logical_line):
return 0, msg
@@ -144,12 +141,12 @@
"""
if TESTTOOLS_SKIP_DECORATOR.match(logical_line):
yield (0, "T109: Cannot use testtools.skip decorator; instead use "
- "decorators.skip_because from tempest-lib")
+ "decorators.skip_because from tempest.lib")
def _common_service_clients_check(logical_line, physical_line, filename,
ignored_list_file=None):
- if 'tempest/services/' not in filename:
+ if not re.match('tempest/(lib/)?services/.*', filename):
return False
if ignored_list_file is not None:
@@ -185,7 +182,8 @@
# the end of a method
return
- if 'self.get(' not in line:
+ if 'self.get(' not in line and ('self.show_resource(' not in line and
+ 'self.list_resources(' not in line):
continue
if METHOD_GET_RESOURCE.match(logical_line):
@@ -211,7 +209,7 @@
# the end of a method
return
- if 'self.delete(' not in line:
+ if 'self.delete(' not in line and 'self.delete_resource(' not in line:
continue
if METHOD_DELETE_RESOURCE.match(logical_line):
@@ -222,6 +220,92 @@
yield (0, msg)
+def dont_import_local_tempest_into_lib(logical_line, filename):
+ """Check that tempest.lib should not import local tempest code
+
+ T112
+ """
+ if 'tempest/lib/' not in filename:
+ return
+
+ 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):
+ return
+
+ msg = ("T112: tempest.lib should not import local tempest code to avoid "
+ "circular dependency")
+ yield (0, msg)
+
+
+def use_rand_uuid_instead_of_uuid4(logical_line, filename):
+ """Check that tests use data_utils.rand_uuid() instead of uuid.uuid4()
+
+ T113
+ """
+ if 'tempest/lib/' in filename:
+ return
+
+ if 'uuid.uuid4()' not in logical_line:
+ return
+
+ msg = ("T113: Tests should use data_utils.rand_uuid()/rand_uuid_hex() "
+ "instead of uuid.uuid4()/uuid.uuid4().hex")
+ yield (0, msg)
+
+
+def dont_use_config_in_tempest_lib(logical_line, filename):
+ """Check that tempest.lib doesn't use tempest config
+
+ T114
+ """
+
+ 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):
+ msg = ('T114: tempest.lib can not have any dependency on tempest '
+ 'config.')
+ yield(0, msg)
+
+
+def dont_put_admin_tests_on_nonadmin_path(logical_line, physical_line,
+ filename):
+ """Check admin tests should exist under admin path
+
+ T115
+ """
+
+ if 'tempest/api/' not in filename:
+ return
+
+ if pep8.noqa(physical_line):
+ return
+
+ if not re.match('class .*Test.*\(.*Admin.*\):', logical_line):
+ return
+
+ if not re.match('.\/tempest\/api\/.*\/admin\/.*', filename):
+ msg = 'T115: All admin tests should exist under admin path.'
+ yield(0, msg)
+
+
+def unsupported_exception_attribute_PY3(logical_line):
+ """Check Unsupported 'message' exception attribute in PY3
+
+ T116
+ """
+ result = EX_ATTRIBUTE.search(logical_line)
+ msg = ("[T116] Unsupported 'message' Exception attribute in PY3")
+ if result:
+ yield(0, msg)
+
+
def factory(register):
register(import_no_clients_in_api_and_scenario_tests)
register(scenario_tests_need_service_tags)
@@ -233,3 +317,8 @@
register(no_testtools_skip_decorator)
register(get_resources_on_service_clients)
register(delete_resources_on_service_clients)
+ register(dont_import_local_tempest_into_lib)
+ register(dont_use_config_in_tempest_lib)
+ register(use_rand_uuid_instead_of_uuid4)
+ register(dont_put_admin_tests_on_nonadmin_path)
+ register(unsupported_exception_attribute_PY3)
diff --git a/tempest/hacking/ignored_list_T110.txt b/tempest/hacking/ignored_list_T110.txt
deleted file mode 100644
index 8de3151..0000000
--- a/tempest/hacking/ignored_list_T110.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-./tempest/services/identity/v3/json/identity_client.py
-./tempest/services/messaging/json/messaging_client.py
-./tempest/services/object_storage/object_client.py
-./tempest/services/telemetry/json/alarming_client.py
-./tempest/services/telemetry/json/telemetry_client.py
-./tempest/services/volume/base/base_qos_client.py
-./tempest/services/volume/base/base_backups_client.py
-./tempest/services/baremetal/base.py
diff --git a/tempest/hacking/ignored_list_T111.txt b/tempest/hacking/ignored_list_T111.txt
deleted file mode 100644
index 8017e76..0000000
--- a/tempest/hacking/ignored_list_T111.txt
+++ /dev/null
@@ -1 +0,0 @@
-./tempest/services/baremetal/base.py
diff --git a/tempest/stress/__init__.py b/tempest/lib/__init__.py
similarity index 100%
rename from tempest/stress/__init__.py
rename to tempest/lib/__init__.py
diff --git a/tempest/api_schema/__init__.py b/tempest/lib/api_schema/__init__.py
similarity index 100%
rename from tempest/api_schema/__init__.py
rename to tempest/lib/api_schema/__init__.py
diff --git a/tempest/api_schema/response/__init__.py b/tempest/lib/api_schema/response/__init__.py
similarity index 100%
rename from tempest/api_schema/response/__init__.py
rename to tempest/lib/api_schema/response/__init__.py
diff --git a/tempest/api_schema/response/compute/__init__.py b/tempest/lib/api_schema/response/compute/__init__.py
similarity index 100%
rename from tempest/api_schema/response/compute/__init__.py
rename to tempest/lib/api_schema/response/compute/__init__.py
diff --git a/tempest/api_schema/response/compute/v2_1/__init__.py b/tempest/lib/api_schema/response/compute/v2_1/__init__.py
similarity index 100%
rename from tempest/api_schema/response/compute/v2_1/__init__.py
rename to tempest/lib/api_schema/response/compute/v2_1/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_1/agents.py b/tempest/lib/api_schema/response/compute/v2_1/agents.py
new file mode 100644
index 0000000..6f712b4
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_1/agents.py
@@ -0,0 +1,82 @@
+# Copyright 2014 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.
+
+common_agent_info = {
+ 'type': 'object',
+ 'properties': {
+ 'agent_id': {'type': ['integer', 'string']},
+ 'hypervisor': {'type': 'string'},
+ 'os': {'type': 'string'},
+ 'architecture': {'type': 'string'},
+ 'version': {'type': 'string'},
+ 'url': {'type': 'string', 'format': 'uri'},
+ 'md5hash': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ 'required': ['agent_id', 'hypervisor', 'os', 'architecture',
+ 'version', 'url', 'md5hash']
+}
+
+list_agents = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'agents': {
+ 'type': 'array',
+ 'items': common_agent_info
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['agents']
+ }
+}
+
+create_agent = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'agent': common_agent_info
+ },
+ 'additionalProperties': False,
+ 'required': ['agent']
+ }
+}
+
+update_agent = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'agent': {
+ 'type': 'object',
+ 'properties': {
+ 'agent_id': {'type': ['integer', 'string']},
+ 'version': {'type': 'string'},
+ 'url': {'type': 'string', 'format': 'uri'},
+ 'md5hash': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ 'required': ['agent_id', 'version', 'url', 'md5hash']
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['agent']
+ }
+}
+
+delete_agent = {
+ 'status_code': [200]
+}
diff --git a/tempest/lib/api_schema/response/compute/v2_1/aggregates.py b/tempest/lib/api_schema/response/compute/v2_1/aggregates.py
new file mode 100644
index 0000000..3289a34
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_1/aggregates.py
@@ -0,0 +1,92 @@
+# Copyright 2014 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.
+
+import copy
+
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+
+# create-aggregate api doesn't have 'hosts' and 'metadata' attributes.
+aggregate_for_create = {
+ 'type': 'object',
+ 'properties': {
+ 'availability_zone': {'type': ['string', 'null']},
+ 'created_at': parameter_types.date_time,
+ 'deleted': {'type': 'boolean'},
+ 'deleted_at': parameter_types.date_time_or_null,
+ 'id': {'type': 'integer'},
+ 'name': {'type': 'string'},
+ 'updated_at': parameter_types.date_time_or_null
+ },
+ 'additionalProperties': False,
+ 'required': ['availability_zone', 'created_at', 'deleted',
+ 'deleted_at', 'id', 'name', 'updated_at'],
+}
+
+common_aggregate_info = copy.deepcopy(aggregate_for_create)
+common_aggregate_info['properties'].update({
+ 'hosts': {'type': 'array'},
+ 'metadata': {'type': 'object'}
+})
+common_aggregate_info['required'].extend(['hosts', 'metadata'])
+
+list_aggregates = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'aggregates': {
+ 'type': 'array',
+ 'items': common_aggregate_info
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['aggregates'],
+ }
+}
+
+get_aggregate = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'aggregate': common_aggregate_info
+ },
+ 'additionalProperties': False,
+ 'required': ['aggregate'],
+ }
+}
+
+aggregate_set_metadata = get_aggregate
+# The 'updated_at' attribute of 'update_aggregate' can't be null.
+update_aggregate = copy.deepcopy(get_aggregate)
+update_aggregate['response_body']['properties']['aggregate']['properties'][
+ 'updated_at'] = parameter_types.date_time
+
+delete_aggregate = {
+ 'status_code': [200]
+}
+
+create_aggregate = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'aggregate': aggregate_for_create
+ },
+ 'additionalProperties': False,
+ 'required': ['aggregate'],
+ }
+}
+
+aggregate_add_remove_host = get_aggregate
diff --git a/tempest/lib/api_schema/response/compute/v2_1/availability_zone.py b/tempest/lib/api_schema/response/compute/v2_1/availability_zone.py
new file mode 100644
index 0000000..0dc28c3
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_1/availability_zone.py
@@ -0,0 +1,80 @@
+# Copyright 2014 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.
+
+import copy
+
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+
+
+base = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'availabilityZoneInfo': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'zoneName': {'type': 'string'},
+ 'zoneState': {
+ 'type': 'object',
+ 'properties': {
+ 'available': {'type': 'boolean'}
+ },
+ 'additionalProperties': False,
+ 'required': ['available']
+ },
+ # NOTE: Here is the difference between detail and
+ # non-detail.
+ 'hosts': {'type': 'null'}
+ },
+ 'additionalProperties': False,
+ 'required': ['zoneName', 'zoneState', 'hosts']
+ }
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['availabilityZoneInfo']
+ }
+}
+
+detail = {
+ 'type': ['object', 'null'],
+ 'patternProperties': {
+ # NOTE: Here is for a hostname
+ '^[a-zA-Z0-9-_.]+$': {
+ 'type': 'object',
+ 'patternProperties': {
+ # NOTE: Here is for a service name
+ '^.*$': {
+ 'type': 'object',
+ 'properties': {
+ 'available': {'type': 'boolean'},
+ 'active': {'type': 'boolean'},
+ 'updated_at': parameter_types.date_time_or_null
+ },
+ 'additionalProperties': False,
+ 'required': ['available', 'active', 'updated_at']
+ }
+ }
+ }
+ }
+}
+
+list_availability_zone_list = copy.deepcopy(base)
+
+list_availability_zone_list_detail = copy.deepcopy(base)
+list_availability_zone_list_detail['response_body']['properties'][
+ 'availabilityZoneInfo']['items']['properties']['hosts'] = detail
diff --git a/tempest/lib/api_schema/response/compute/v2_1/baremetal_nodes.py b/tempest/lib/api_schema/response/compute/v2_1/baremetal_nodes.py
new file mode 100644
index 0000000..d1ee877
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_1/baremetal_nodes.py
@@ -0,0 +1,63 @@
+# Copyright 2015 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.
+
+import copy
+
+node = {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'interfaces': {'type': 'array'},
+ 'host': {'type': 'string'},
+ 'task_state': {'type': ['string', 'null']},
+ 'cpus': {'type': ['integer', 'string']},
+ 'memory_mb': {'type': ['integer', 'string']},
+ 'disk_gb': {'type': ['integer', 'string']},
+ },
+ 'additionalProperties': False,
+ 'required': ['id', 'interfaces', 'host', 'task_state', 'cpus', 'memory_mb',
+ 'disk_gb']
+}
+
+list_baremetal_nodes = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'nodes': {
+ 'type': 'array',
+ 'items': node
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['nodes']
+ }
+}
+
+baremetal_node = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'node': node
+ },
+ 'additionalProperties': False,
+ 'required': ['node']
+ }
+}
+get_baremetal_node = copy.deepcopy(baremetal_node)
+get_baremetal_node['response_body']['properties']['node'][
+ 'properties'].update({'instance_uuid': {'type': ['string', 'null']}})
+get_baremetal_node['response_body']['properties']['node'][
+ 'required'].append('instance_uuid')
diff --git a/tempest/lib/api_schema/response/compute/v2_1/certificates.py b/tempest/lib/api_schema/response/compute/v2_1/certificates.py
new file mode 100644
index 0000000..4e7cbe4
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_1/certificates.py
@@ -0,0 +1,41 @@
+# Copyright 2014 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.
+
+import copy
+
+_common_schema = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'certificate': {
+ 'type': 'object',
+ 'properties': {
+ 'data': {'type': 'string'},
+ 'private_key': {'type': 'string'},
+ },
+ 'additionalProperties': False,
+ 'required': ['data', 'private_key']
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['certificate']
+ }
+}
+
+get_certificate = copy.deepcopy(_common_schema)
+get_certificate['response_body']['properties']['certificate'][
+ 'properties']['private_key'].update({'type': 'null'})
+
+create_certificate = copy.deepcopy(_common_schema)
diff --git a/tempest/lib/api_schema/response/compute/v2_1/extensions.py b/tempest/lib/api_schema/response/compute/v2_1/extensions.py
new file mode 100644
index 0000000..b5962d7
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_1/extensions.py
@@ -0,0 +1,46 @@
+# Copyright 2014 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
+
+list_extensions = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'extensions': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'updated': parameter_types.date_time,
+ 'name': {'type': 'string'},
+ 'links': {'type': 'array'},
+ 'namespace': {
+ 'type': 'string',
+ 'format': 'uri'
+ },
+ 'alias': {'type': 'string'},
+ 'description': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ 'required': ['updated', 'name', 'links', 'namespace',
+ 'alias', 'description']
+ }
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['extensions']
+ }
+}
diff --git a/tempest/lib/api_schema/response/compute/v2_1/fixed_ips.py b/tempest/lib/api_schema/response/compute/v2_1/fixed_ips.py
new file mode 100644
index 0000000..a653213
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_1/fixed_ips.py
@@ -0,0 +1,41 @@
+# Copyright 2014 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
+
+get_fixed_ip = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'fixed_ip': {
+ 'type': 'object',
+ 'properties': {
+ 'address': parameter_types.ip_address,
+ 'cidr': {'type': 'string'},
+ 'host': {'type': 'string'},
+ 'hostname': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ 'required': ['address', 'cidr', 'host', 'hostname']
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['fixed_ip']
+ }
+}
+
+reserve_unreserve_fixed_ip = {
+ 'status_code': [202]
+}
diff --git a/tempest/lib/api_schema/response/compute/v2_1/flavors.py b/tempest/lib/api_schema/response/compute/v2_1/flavors.py
new file mode 100644
index 0000000..af5e67f
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_1/flavors.py
@@ -0,0 +1,103 @@
+# Copyright 2014 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
+
+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'}
+ },
+ 'additionalProperties': False,
+ 'required': ['name', 'links', 'id']
+ }
+ },
+ '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'}
+ },
+ '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']
+}
+
+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']
+ }
+}
+
+unset_flavor_extra_specs = {
+ 'status_code': [200]
+}
+
+create_update_get_flavor_details = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'flavor': common_flavor_info
+ },
+ 'additionalProperties': False,
+ 'required': ['flavor']
+ }
+}
+
+delete_flavor = {
+ 'status_code': [202]
+}
diff --git a/tempest/lib/api_schema/response/compute/v2_1/flavors_access.py b/tempest/lib/api_schema/response/compute/v2_1/flavors_access.py
new file mode 100644
index 0000000..a4d6af0
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_1/flavors_access.py
@@ -0,0 +1,36 @@
+# Copyright 2014 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.
+
+add_remove_list_flavor_access = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'flavor_access': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'flavor_id': {'type': 'string'},
+ 'tenant_id': {'type': 'string'},
+ },
+ 'additionalProperties': False,
+ 'required': ['flavor_id', 'tenant_id'],
+ }
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['flavor_access']
+ }
+}
diff --git a/tempest/lib/api_schema/response/compute/v2_1/flavors_extra_specs.py b/tempest/lib/api_schema/response/compute/v2_1/flavors_extra_specs.py
new file mode 100644
index 0000000..a438d48
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_1/flavors_extra_specs.py
@@ -0,0 +1,40 @@
+# Copyright 2014 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.
+
+set_get_flavor_extra_specs = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'extra_specs': {
+ 'type': 'object',
+ 'patternProperties': {
+ '^[a-zA-Z0-9_\-\. :]+$': {'type': 'string'}
+ }
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['extra_specs']
+ }
+}
+
+set_get_flavor_extra_specs_key = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'patternProperties': {
+ '^[a-zA-Z0-9_\-\. :]+$': {'type': 'string'}
+ }
+ }
+}
diff --git a/tempest/lib/api_schema/response/compute/v2_1/floating_ips.py b/tempest/lib/api_schema/response/compute/v2_1/floating_ips.py
new file mode 100644
index 0000000..0c66590
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_1/floating_ips.py
@@ -0,0 +1,148 @@
+# Copyright 2014 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
+
+common_floating_ip_info = {
+ 'type': 'object',
+ 'properties': {
+ # NOTE: Now the type of 'id' is integer, but
+ # here allows 'string' also because we will be
+ # able to change it to 'uuid' in the future.
+ 'id': {'type': ['integer', 'string']},
+ 'pool': {'type': ['string', 'null']},
+ 'instance_id': {'type': ['string', 'null']},
+ 'ip': parameter_types.ip_address,
+ 'fixed_ip': parameter_types.ip_address
+ },
+ 'additionalProperties': False,
+ 'required': ['id', 'pool', 'instance_id',
+ 'ip', 'fixed_ip'],
+
+}
+list_floating_ips = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'floating_ips': {
+ 'type': 'array',
+ 'items': common_floating_ip_info
+ },
+ },
+ 'additionalProperties': False,
+ 'required': ['floating_ips'],
+ }
+}
+
+create_get_floating_ip = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'floating_ip': common_floating_ip_info
+ },
+ 'additionalProperties': False,
+ 'required': ['floating_ip'],
+ }
+}
+
+list_floating_ip_pools = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'floating_ip_pools': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'name': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ 'required': ['name'],
+ }
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['floating_ip_pools'],
+ }
+}
+
+add_remove_floating_ip = {
+ 'status_code': [202]
+}
+
+create_floating_ips_bulk = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'floating_ips_bulk_create': {
+ 'type': 'object',
+ 'properties': {
+ 'interface': {'type': ['string', 'null']},
+ 'ip_range': {'type': 'string'},
+ 'pool': {'type': ['string', 'null']},
+ },
+ 'additionalProperties': False,
+ 'required': ['interface', 'ip_range', 'pool'],
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['floating_ips_bulk_create'],
+ }
+}
+
+delete_floating_ips_bulk = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'floating_ips_bulk_delete': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ 'required': ['floating_ips_bulk_delete'],
+ }
+}
+
+list_floating_ips_bulk = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'floating_ip_info': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'address': parameter_types.ip_address,
+ 'instance_uuid': {'type': ['string', 'null']},
+ 'interface': {'type': ['string', 'null']},
+ 'pool': {'type': ['string', 'null']},
+ 'project_id': {'type': ['string', 'null']},
+ 'fixed_ip': parameter_types.ip_address
+ },
+ 'additionalProperties': False,
+ # NOTE: fixed_ip is introduced after JUNO release,
+ # So it is not defined as 'required'.
+ 'required': ['address', 'instance_uuid', 'interface',
+ 'pool', 'project_id'],
+ }
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['floating_ip_info'],
+ }
+}
diff --git a/tempest/lib/api_schema/response/compute/v2_1/hosts.py b/tempest/lib/api_schema/response/compute/v2_1/hosts.py
new file mode 100644
index 0000000..cae3435
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_1/hosts.py
@@ -0,0 +1,119 @@
+# Copyright 2014 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.
+
+import copy
+
+
+list_hosts = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'hosts': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'host_name': {'type': 'string'},
+ 'service': {'type': 'string'},
+ 'zone': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ 'required': ['host_name', 'service', 'zone']
+ }
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['hosts']
+ }
+}
+
+get_host_detail = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'host': {
+ 'type': 'array',
+ 'item': {
+ 'type': 'object',
+ 'properties': {
+ 'resource': {
+ 'type': 'object',
+ 'properties': {
+ 'cpu': {'type': 'integer'},
+ 'disk_gb': {'type': 'integer'},
+ 'host': {'type': 'string'},
+ 'memory_mb': {'type': 'integer'},
+ 'project': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ 'required': ['cpu', 'disk_gb', 'host',
+ 'memory_mb', 'project']
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['resource']
+ }
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['host']
+ }
+}
+
+startup_host = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'host': {'type': 'string'},
+ 'power_action': {'enum': ['startup']}
+ },
+ 'additionalProperties': False,
+ 'required': ['host', 'power_action']
+ }
+}
+
+# The 'power_action' attribute of 'shutdown_host' API is 'shutdown'
+shutdown_host = copy.deepcopy(startup_host)
+
+shutdown_host['response_body']['properties']['power_action'] = {
+ 'enum': ['shutdown']
+}
+
+# The 'power_action' attribute of 'reboot_host' API is 'reboot'
+reboot_host = copy.deepcopy(startup_host)
+
+reboot_host['response_body']['properties']['power_action'] = {
+ 'enum': ['reboot']
+}
+
+update_host = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'host': {'type': 'string'},
+ 'maintenance_mode': {'enum': ['on_maintenance',
+ 'off_maintenance']},
+ 'status': {'enum': ['enabled', 'disabled']}
+ },
+ 'additionalProperties': False,
+ 'anyOf': [
+ {'required': ['host', 'status']},
+ {'required': ['host', 'maintenance_mode']}
+ ]
+ }
+}
diff --git a/tempest/lib/api_schema/response/compute/v2_1/hypervisors.py b/tempest/lib/api_schema/response/compute/v2_1/hypervisors.py
new file mode 100644
index 0000000..d15b4f6
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_1/hypervisors.py
@@ -0,0 +1,195 @@
+# Copyright 2014 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.
+
+import copy
+
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+
+get_hypervisor_statistics = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'hypervisor_statistics': {
+ 'type': 'object',
+ 'properties': {
+ 'count': {'type': 'integer'},
+ 'current_workload': {'type': 'integer'},
+ 'disk_available_least': {'type': ['integer', 'null']},
+ 'free_disk_gb': {'type': 'integer'},
+ 'free_ram_mb': {'type': 'integer'},
+ 'local_gb': {'type': 'integer'},
+ 'local_gb_used': {'type': 'integer'},
+ 'memory_mb': {'type': 'integer'},
+ 'memory_mb_used': {'type': 'integer'},
+ 'running_vms': {'type': 'integer'},
+ 'vcpus': {'type': 'integer'},
+ 'vcpus_used': {'type': 'integer'}
+ },
+ 'additionalProperties': False,
+ 'required': ['count', 'current_workload',
+ 'disk_available_least', 'free_disk_gb',
+ 'free_ram_mb', 'local_gb', 'local_gb_used',
+ 'memory_mb', 'memory_mb_used', 'running_vms',
+ 'vcpus', 'vcpus_used']
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['hypervisor_statistics']
+ }
+}
+
+
+hypervisor_detail = {
+ 'type': 'object',
+ 'properties': {
+ 'status': {'type': 'string'},
+ 'state': {'type': 'string'},
+ 'cpu_info': {'type': 'string'},
+ 'current_workload': {'type': 'integer'},
+ 'disk_available_least': {'type': ['integer', 'null']},
+ 'host_ip': parameter_types.ip_address,
+ 'free_disk_gb': {'type': 'integer'},
+ 'free_ram_mb': {'type': 'integer'},
+ 'hypervisor_hostname': {'type': 'string'},
+ 'hypervisor_type': {'type': 'string'},
+ 'hypervisor_version': {'type': 'integer'},
+ 'id': {'type': ['integer', 'string']},
+ 'local_gb': {'type': 'integer'},
+ 'local_gb_used': {'type': 'integer'},
+ 'memory_mb': {'type': 'integer'},
+ 'memory_mb_used': {'type': 'integer'},
+ 'running_vms': {'type': 'integer'},
+ 'service': {
+ 'type': 'object',
+ 'properties': {
+ 'host': {'type': 'string'},
+ 'id': {'type': ['integer', 'string']},
+ 'disabled_reason': {'type': ['string', 'null']}
+ },
+ 'additionalProperties': False,
+ 'required': ['host', 'id']
+ },
+ 'vcpus': {'type': 'integer'},
+ 'vcpus_used': {'type': 'integer'}
+ },
+ 'additionalProperties': False,
+ # NOTE: When loading os-hypervisor-status extension,
+ # a response contains status and state. So these params
+ # should not be required.
+ 'required': ['cpu_info', 'current_workload',
+ 'disk_available_least', 'host_ip',
+ 'free_disk_gb', 'free_ram_mb',
+ 'hypervisor_hostname', 'hypervisor_type',
+ 'hypervisor_version', 'id', 'local_gb',
+ 'local_gb_used', 'memory_mb', 'memory_mb_used',
+ 'running_vms', 'service', 'vcpus', 'vcpus_used']
+}
+
+list_hypervisors_detail = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'hypervisors': {
+ 'type': 'array',
+ 'items': hypervisor_detail
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['hypervisors']
+ }
+}
+
+get_hypervisor = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'hypervisor': hypervisor_detail
+ },
+ 'additionalProperties': False,
+ 'required': ['hypervisor']
+ }
+}
+
+list_search_hypervisors = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'hypervisors': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'status': {'type': 'string'},
+ 'state': {'type': 'string'},
+ 'id': {'type': ['integer', 'string']},
+ 'hypervisor_hostname': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ # NOTE: When loading os-hypervisor-status extension,
+ # a response contains status and state. So these params
+ # should not be required.
+ 'required': ['id', 'hypervisor_hostname']
+ }
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['hypervisors']
+ }
+}
+
+get_hypervisor_uptime = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'hypervisor': {
+ 'type': 'object',
+ 'properties': {
+ 'status': {'type': 'string'},
+ 'state': {'type': 'string'},
+ 'id': {'type': ['integer', 'string']},
+ 'hypervisor_hostname': {'type': 'string'},
+ 'uptime': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ # NOTE: When loading os-hypervisor-status extension,
+ # a response contains status and state. So these params
+ # should not be required.
+ 'required': ['id', 'hypervisor_hostname', 'uptime']
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['hypervisor']
+ }
+}
+
+get_hypervisors_servers = copy.deepcopy(list_search_hypervisors)
+get_hypervisors_servers['response_body']['properties']['hypervisors']['items'][
+ 'properties']['servers'] = {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'uuid': {'type': 'string'},
+ 'name': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ }
+ }
+# In V2 API, if there is no servers (VM) on the Hypervisor host then 'servers'
+# attribute will not be present in response body So it is not 'required'.
diff --git a/tempest/lib/api_schema/response/compute/v2_1/images.py b/tempest/lib/api_schema/response/compute/v2_1/images.py
new file mode 100644
index 0000000..156ff4a
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_1/images.py
@@ -0,0 +1,156 @@
+# Copyright 2014 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.
+
+import copy
+
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+
+image_links = copy.deepcopy(parameter_types.links)
+image_links['items']['properties'].update({'type': {'type': 'string'}})
+
+image_status_enums = ['ACTIVE', 'SAVING', 'DELETED', 'ERROR', 'UNKNOWN']
+
+common_image_schema = {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'status': {'enum': image_status_enums},
+ 'updated': parameter_types.date_time,
+ 'links': image_links,
+ 'name': {'type': ['string', 'null']},
+ 'created': parameter_types.date_time,
+ 'minDisk': {'type': 'integer'},
+ 'minRam': {'type': 'integer'},
+ 'progress': {'type': 'integer'},
+ 'metadata': {'type': 'object'},
+ 'server': {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'links': parameter_types.links
+ },
+ 'additionalProperties': False,
+ 'required': ['id', 'links']
+ },
+ 'OS-EXT-IMG-SIZE:size': {'type': ['integer', 'null']},
+ 'OS-DCF:diskConfig': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ # 'server' attributes only comes in response body if image is
+ # associated with any server. 'OS-EXT-IMG-SIZE:size' & 'OS-DCF:diskConfig'
+ # are API extension, So those are not defined as 'required'.
+ 'required': ['id', 'status', 'updated', 'links', 'name',
+ 'created', 'minDisk', 'minRam', 'progress',
+ 'metadata']
+}
+
+get_image = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'image': common_image_schema
+ },
+ 'additionalProperties': False,
+ 'required': ['image']
+ }
+}
+
+list_images = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'images': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'links': image_links,
+ 'name': {'type': ['string', 'null']}
+ },
+ 'additionalProperties': False,
+ 'required': ['id', 'links', 'name']
+ }
+ },
+ 'images_links': parameter_types.links
+ },
+ 'additionalProperties': False,
+ # NOTE(gmann): images_links attribute is not necessary to be
+ # present always So it is not 'required'.
+ 'required': ['images']
+ }
+}
+
+create_image = {
+ 'status_code': [202],
+ 'response_header': {
+ 'type': 'object',
+ 'properties': parameter_types.response_header
+ }
+}
+create_image['response_header']['properties'].update(
+ {'location': {
+ 'type': 'string',
+ 'format': 'uri'}
+ }
+)
+create_image['response_header']['required'] = ['location']
+
+delete = {
+ 'status_code': [204]
+}
+
+image_metadata = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'metadata': {'type': 'object'}
+ },
+ 'additionalProperties': False,
+ 'required': ['metadata']
+ }
+}
+
+image_meta_item = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'meta': {'type': 'object'}
+ },
+ 'additionalProperties': False,
+ 'required': ['meta']
+ }
+}
+
+list_images_details = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'images': {
+ 'type': 'array',
+ 'items': common_image_schema
+ },
+ 'images_links': parameter_types.links
+ },
+ 'additionalProperties': False,
+ # NOTE(gmann): images_links attribute is not necessary to be
+ # present always So it is not 'required'.
+ 'required': ['images']
+ }
+}
diff --git a/tempest/lib/api_schema/response/compute/v2_1/instance_usage_audit_logs.py b/tempest/lib/api_schema/response/compute/v2_1/instance_usage_audit_logs.py
new file mode 100644
index 0000000..15224c5
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_1/instance_usage_audit_logs.py
@@ -0,0 +1,78 @@
+# Copyright 2014 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.
+
+common_instance_usage_audit_log = {
+ 'type': 'object',
+ 'properties': {
+ 'hosts_not_run': {
+ 'type': 'array',
+ 'items': {'type': 'string'}
+ },
+ 'log': {
+ 'type': 'object',
+ 'patternProperties': {
+ # NOTE: Here is a host name.
+ '^.+$': {
+ 'type': 'object',
+ 'properties': {
+ 'state': {'type': 'string'},
+ 'instances': {'type': 'integer'},
+ 'errors': {'type': 'integer'},
+ 'message': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ 'required': ['state', 'instances', 'errors', 'message']
+ }
+ }
+ },
+ 'num_hosts': {'type': 'integer'},
+ 'num_hosts_done': {'type': 'integer'},
+ 'num_hosts_not_run': {'type': 'integer'},
+ 'num_hosts_running': {'type': 'integer'},
+ 'overall_status': {'type': 'string'},
+ 'period_beginning': {'type': 'string'},
+ 'period_ending': {'type': 'string'},
+ 'total_errors': {'type': 'integer'},
+ 'total_instances': {'type': 'integer'}
+ },
+ 'additionalProperties': False,
+ 'required': ['hosts_not_run', 'log', 'num_hosts', 'num_hosts_done',
+ 'num_hosts_not_run', 'num_hosts_running', 'overall_status',
+ 'period_beginning', 'period_ending', 'total_errors',
+ 'total_instances']
+}
+
+get_instance_usage_audit_log = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'instance_usage_audit_log': common_instance_usage_audit_log
+ },
+ 'additionalProperties': False,
+ 'required': ['instance_usage_audit_log']
+ }
+}
+
+list_instance_usage_audit_log = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'instance_usage_audit_logs': common_instance_usage_audit_log
+ },
+ 'additionalProperties': False,
+ 'required': ['instance_usage_audit_logs']
+ }
+}
diff --git a/tempest/lib/api_schema/response/compute/v2_1/interfaces.py b/tempest/lib/api_schema/response/compute/v2_1/interfaces.py
new file mode 100644
index 0000000..9984750
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_1/interfaces.py
@@ -0,0 +1,73 @@
+# Copyright 2014 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
+
+interface_common_info = {
+ 'type': 'object',
+ 'properties': {
+ 'port_state': {'type': 'string'},
+ 'fixed_ips': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'subnet_id': {
+ 'type': 'string',
+ 'format': 'uuid'
+ },
+ 'ip_address': parameter_types.ip_address
+ },
+ 'additionalProperties': False,
+ 'required': ['subnet_id', 'ip_address']
+ }
+ },
+ 'port_id': {'type': 'string', 'format': 'uuid'},
+ 'net_id': {'type': 'string', 'format': 'uuid'},
+ 'mac_addr': parameter_types.mac_address
+ },
+ 'additionalProperties': False,
+ 'required': ['port_state', 'fixed_ips', 'port_id', 'net_id', 'mac_addr']
+}
+
+get_create_interfaces = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'interfaceAttachment': interface_common_info
+ },
+ 'additionalProperties': False,
+ 'required': ['interfaceAttachment']
+ }
+}
+
+list_interfaces = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'interfaceAttachments': {
+ 'type': 'array',
+ 'items': interface_common_info
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['interfaceAttachments']
+ }
+}
+
+delete_interface = {
+ 'status_code': [202]
+}
diff --git a/tempest/lib/api_schema/response/compute/v2_1/keypairs.py b/tempest/lib/api_schema/response/compute/v2_1/keypairs.py
new file mode 100644
index 0000000..e7dcf79
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_1/keypairs.py
@@ -0,0 +1,106 @@
+# Copyright 2014 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
+
+get_keypair = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'keypair': {
+ 'type': 'object',
+ 'properties': {
+ 'public_key': {'type': 'string'},
+ 'name': {'type': 'string'},
+ 'fingerprint': {'type': 'string'},
+ 'user_id': {'type': 'string'},
+ 'deleted': {'type': 'boolean'},
+ 'created_at': parameter_types.date_time,
+ 'updated_at': parameter_types.date_time_or_null,
+ 'deleted_at': parameter_types.date_time_or_null,
+ 'id': {'type': 'integer'}
+
+ },
+ 'additionalProperties': False,
+ 'required': ['public_key', 'name', 'fingerprint', 'user_id',
+ 'deleted', 'created_at', 'updated_at',
+ 'deleted_at', 'id']
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['keypair']
+ }
+}
+
+create_keypair = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'keypair': {
+ 'type': 'object',
+ 'properties': {
+ 'fingerprint': {'type': 'string'},
+ 'name': {'type': 'string'},
+ 'public_key': {'type': 'string'},
+ 'user_id': {'type': 'string'},
+ 'private_key': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ # When create keypair API is being called with 'Public key'
+ # (Importing keypair) then, response body does not contain
+ # 'private_key' So it is not defined as 'required'
+ 'required': ['fingerprint', 'name', 'public_key', 'user_id']
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['keypair']
+ }
+}
+
+delete_keypair = {
+ 'status_code': [202],
+}
+
+list_keypairs = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'keypairs': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'keypair': {
+ 'type': 'object',
+ 'properties': {
+ 'public_key': {'type': 'string'},
+ 'name': {'type': 'string'},
+ 'fingerprint': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ 'required': ['public_key', 'name', 'fingerprint']
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['keypair']
+ }
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['keypairs']
+ }
+}
diff --git a/tempest/lib/api_schema/response/compute/v2_1/limits.py b/tempest/lib/api_schema/response/compute/v2_1/limits.py
new file mode 100644
index 0000000..81f175f
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_1/limits.py
@@ -0,0 +1,106 @@
+# Copyright 2014 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.
+
+get_limit = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'limits': {
+ 'type': 'object',
+ 'properties': {
+ 'absolute': {
+ 'type': 'object',
+ 'properties': {
+ 'maxTotalRAMSize': {'type': 'integer'},
+ 'totalCoresUsed': {'type': 'integer'},
+ 'maxTotalInstances': {'type': 'integer'},
+ 'maxTotalFloatingIps': {'type': 'integer'},
+ 'totalSecurityGroupsUsed': {'type': 'integer'},
+ 'maxTotalCores': {'type': 'integer'},
+ 'totalFloatingIpsUsed': {'type': 'integer'},
+ 'maxSecurityGroups': {'type': 'integer'},
+ 'maxServerMeta': {'type': 'integer'},
+ 'maxPersonality': {'type': 'integer'},
+ 'maxImageMeta': {'type': 'integer'},
+ 'maxPersonalitySize': {'type': 'integer'},
+ 'maxSecurityGroupRules': {'type': 'integer'},
+ 'maxTotalKeypairs': {'type': 'integer'},
+ 'totalRAMUsed': {'type': 'integer'},
+ 'totalInstancesUsed': {'type': 'integer'},
+ 'maxServerGroupMembers': {'type': 'integer'},
+ 'maxServerGroups': {'type': 'integer'},
+ 'totalServerGroupsUsed': {'type': 'integer'}
+ },
+ 'additionalProperties': False,
+ # NOTE(gmann): maxServerGroupMembers, maxServerGroups
+ # and totalServerGroupsUsed are API extension,
+ # and some environments return a response without these
+ # attributes.So they are not 'required'.
+ 'required': ['maxImageMeta',
+ 'maxPersonality',
+ 'maxPersonalitySize',
+ 'maxSecurityGroupRules',
+ 'maxSecurityGroups',
+ 'maxServerMeta',
+ 'maxTotalCores',
+ 'maxTotalFloatingIps',
+ 'maxTotalInstances',
+ 'maxTotalKeypairs',
+ 'maxTotalRAMSize',
+ 'totalCoresUsed',
+ 'totalFloatingIpsUsed',
+ 'totalInstancesUsed',
+ 'totalRAMUsed',
+ 'totalSecurityGroupsUsed']
+ },
+ 'rate': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'limit': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'next-available':
+ {'type': 'string'},
+ 'remaining':
+ {'type': 'integer'},
+ 'unit':
+ {'type': 'string'},
+ 'value':
+ {'type': 'integer'},
+ 'verb':
+ {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ }
+ },
+ 'regex': {'type': 'string'},
+ 'uri': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ }
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['absolute', 'rate']
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['limits']
+ }
+}
diff --git a/tempest/lib/api_schema/response/compute/v2_1/migrations.py b/tempest/lib/api_schema/response/compute/v2_1/migrations.py
new file mode 100644
index 0000000..c50286d
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_1/migrations.py
@@ -0,0 +1,53 @@
+# Copyright 2014 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
+
+list_migrations = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'migrations': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'integer'},
+ 'status': {'type': ['string', 'null']},
+ 'instance_uuid': {'type': ['string', 'null']},
+ 'source_node': {'type': ['string', 'null']},
+ 'source_compute': {'type': ['string', 'null']},
+ 'dest_node': {'type': ['string', 'null']},
+ 'dest_compute': {'type': ['string', 'null']},
+ 'dest_host': {'type': ['string', 'null']},
+ 'old_instance_type_id': {'type': ['integer', 'null']},
+ 'new_instance_type_id': {'type': ['integer', 'null']},
+ 'created_at': parameter_types.date_time,
+ 'updated_at': parameter_types.date_time_or_null
+ },
+ 'additionalProperties': False,
+ 'required': [
+ 'id', 'status', 'instance_uuid', 'source_node',
+ 'source_compute', 'dest_node', 'dest_compute',
+ 'dest_host', 'old_instance_type_id',
+ 'new_instance_type_id', 'created_at', 'updated_at'
+ ]
+ }
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['migrations']
+ }
+}
diff --git a/tempest/lib/api_schema/response/compute/v2_1/parameter_types.py b/tempest/lib/api_schema/response/compute/v2_1/parameter_types.py
new file mode 100644
index 0000000..28ed816
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_1/parameter_types.py
@@ -0,0 +1,122 @@
+# Copyright 2014 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.
+
+links = {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'href': {
+ 'type': 'string',
+ 'format': 'uri'
+ },
+ 'rel': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ 'required': ['href', 'rel']
+ }
+}
+
+mac_address = {
+ 'type': 'string',
+ 'pattern': '(?:[a-f0-9]{2}:){5}[a-f0-9]{2}'
+}
+
+ip_address = {
+ 'oneOf': [
+ {
+ 'type': 'string',
+ 'oneOf': [
+ {'format': 'ipv4'},
+ {'format': 'ipv6'}
+ ]
+ },
+ {'type': 'null'}
+ ]
+}
+
+access_ip_v4 = {
+ 'type': 'string',
+ 'oneOf': [{'format': 'ipv4'}, {'enum': ['']}]
+}
+
+access_ip_v6 = {
+ 'type': 'string',
+ 'oneOf': [{'format': 'ipv6'}, {'enum': ['']}]
+}
+
+addresses = {
+ 'type': 'object',
+ 'patternProperties': {
+ # NOTE: Here is for 'private' or something.
+ '^[a-zA-Z0-9-_.]+$': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'version': {'enum': [4, 6]},
+ 'addr': {
+ 'type': 'string',
+ 'oneOf': [
+ {'format': 'ipv4'},
+ {'format': 'ipv6'}
+ ]
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['version', 'addr']
+ }
+ }
+ }
+}
+
+date_time = {
+ 'type': 'string',
+ 'format': 'iso8601-date-time'
+}
+
+date_time_or_null = {
+ 'type': ['string', 'null'],
+ 'format': 'iso8601-date-time'
+}
+
+response_header = {
+ 'connection': {'type': 'string'},
+ 'content-length': {'type': 'string'},
+ 'content-type': {'type': 'string'},
+ 'status': {'type': 'string'},
+ 'x-compute-request-id': {'type': 'string'},
+ 'vary': {'type': 'string'},
+ 'x-openstack-nova-api-version': {'type': 'string'},
+ # NOTE(gmann): Validating this as string only as this
+ # date in header is returned in different format than
+ # ISO 8601 date time format which is not consistent with
+ # other date-time format in nova.
+ # This API is already deprecated so not worth to fix
+ # on nova side.
+ 'date': {
+ 'type': 'string'
+ }
+}
+
+power_state = {
+ 'type': 'integer',
+ # 0: NOSTATE
+ # 1: RUNNING
+ # 3: PAUSED
+ # 4: SHUTDOWN
+ # 6: CRASHED
+ # 7: SUSPENDED
+ 'enum': [0, 1, 3, 4, 6, 7]
+}
diff --git a/tempest/lib/api_schema/response/compute/v2_1/quota_classes.py b/tempest/lib/api_schema/response/compute/v2_1/quota_classes.py
new file mode 100644
index 0000000..03d7f12
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_1/quota_classes.py
@@ -0,0 +1,31 @@
+# Copyright 2014 IBM 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 copy
+
+from tempest.lib.api_schema.response.compute.v2_1 import quotas
+
+# NOTE(mriedem): os-quota-class-sets responses are the same as os-quota-sets
+# except for the key in the response body is quota_class_set instead of
+# quota_set, so update this copy of the schema from os-quota-sets.
+get_quota_class_set = copy.deepcopy(quotas.get_quota_set)
+get_quota_class_set['response_body']['properties']['quota_class_set'] = (
+ get_quota_class_set['response_body']['properties'].pop('quota_set'))
+get_quota_class_set['response_body']['required'] = ['quota_class_set']
+
+update_quota_class_set = copy.deepcopy(quotas.update_quota_set)
+update_quota_class_set['response_body']['properties']['quota_class_set'] = (
+ update_quota_class_set['response_body']['properties'].pop('quota_set'))
+update_quota_class_set['response_body']['required'] = ['quota_class_set']
diff --git a/tempest/lib/api_schema/response/compute/v2_1/quotas.py b/tempest/lib/api_schema/response/compute/v2_1/quotas.py
new file mode 100644
index 0000000..44f5bdf
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_1/quotas.py
@@ -0,0 +1,183 @@
+# Copyright 2014 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.
+
+import copy
+
+update_quota_set = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'quota_set': {
+ 'type': 'object',
+ 'properties': {
+ 'instances': {'type': 'integer'},
+ 'cores': {'type': 'integer'},
+ 'ram': {'type': 'integer'},
+ 'floating_ips': {'type': 'integer'},
+ 'fixed_ips': {'type': 'integer'},
+ 'metadata_items': {'type': 'integer'},
+ 'key_pairs': {'type': 'integer'},
+ 'security_groups': {'type': 'integer'},
+ 'security_group_rules': {'type': 'integer'},
+ 'server_group_members': {'type': 'integer'},
+ 'server_groups': {'type': 'integer'},
+ 'injected_files': {'type': 'integer'},
+ 'injected_file_content_bytes': {'type': 'integer'},
+ 'injected_file_path_bytes': {'type': 'integer'}
+ },
+ 'additionalProperties': False,
+ # NOTE: server_group_members and server_groups are represented
+ # when enabling quota_server_group extension. So they should
+ # not be required.
+ 'required': ['instances', 'cores', 'ram',
+ 'floating_ips', 'fixed_ips',
+ 'metadata_items', 'key_pairs',
+ 'security_groups', 'security_group_rules',
+ 'injected_files', 'injected_file_content_bytes',
+ 'injected_file_path_bytes']
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['quota_set']
+ }
+}
+
+get_quota_set = copy.deepcopy(update_quota_set)
+get_quota_set['response_body']['properties']['quota_set']['properties'][
+ 'id'] = {'type': 'string'}
+get_quota_set['response_body']['properties']['quota_set']['required'].extend([
+ 'id'])
+
+get_quota_set_details = copy.deepcopy(get_quota_set)
+get_quota_set_details['response_body']['properties']['quota_set'][
+ 'properties'] = {
+ 'id': {'type': 'string'},
+ 'instances': {
+ 'type': 'object',
+ 'properties': {
+ 'reserved': {'type': 'integer'},
+ 'limit': {'type': 'integer'},
+ 'in_use': {'type': 'integer'}
+ }
+ },
+ 'cores': {
+ 'type': 'object',
+ 'properties': {
+ 'reserved': {'type': 'integer'},
+ 'limit': {'type': 'integer'},
+ 'in_use': {'type': 'integer'}
+ }
+ },
+ 'ram': {
+ 'type': 'object',
+ 'properties': {
+ 'reserved': {'type': 'integer'},
+ 'limit': {'type': 'integer'},
+ 'in_use': {'type': 'integer'}
+ }
+ },
+ 'floating_ips': {
+ 'type': 'object',
+ 'properties': {
+ 'reserved': {'type': 'integer'},
+ 'limit': {'type': 'integer'},
+ 'in_use': {'type': 'integer'}
+ }
+ },
+ 'fixed_ips': {
+ 'type': 'object',
+ 'properties': {
+ 'reserved': {'type': 'integer'},
+ 'limit': {'type': 'integer'},
+ 'in_use': {'type': 'integer'}
+ }
+ },
+ 'metadata_items': {
+ 'type': 'object',
+ 'properties': {
+ 'reserved': {'type': 'integer'},
+ 'limit': {'type': 'integer'},
+ 'in_use': {'type': 'integer'}
+ }
+ },
+ 'key_pairs': {
+ 'type': 'object',
+ 'properties': {
+ 'reserved': {'type': 'integer'},
+ 'limit': {'type': 'integer'},
+ 'in_use': {'type': 'integer'}
+ }
+ },
+ 'security_groups': {
+ 'type': 'object',
+ 'properties': {
+ 'reserved': {'type': 'integer'},
+ 'limit': {'type': 'integer'},
+ 'in_use': {'type': 'integer'}
+ }
+ },
+ 'security_group_rules': {
+ 'type': 'object',
+ 'properties': {
+ 'reserved': {'type': 'integer'},
+ 'limit': {'type': 'integer'},
+ 'in_use': {'type': 'integer'}
+ }
+ },
+ 'server_group_members': {
+ 'type': 'object',
+ 'properties': {
+ 'reserved': {'type': 'integer'},
+ 'limit': {'type': 'integer'},
+ 'in_use': {'type': 'integer'}
+ }
+ },
+ 'server_groups': {
+ 'type': 'object',
+ 'properties': {
+ 'reserved': {'type': 'integer'},
+ 'limit': {'type': 'integer'},
+ 'in_use': {'type': 'integer'}
+ }
+ },
+ 'injected_files': {
+ 'type': 'object',
+ 'properties': {
+ 'reserved': {'type': 'integer'},
+ 'limit': {'type': 'integer'},
+ 'in_use': {'type': 'integer'}
+ }
+ },
+ 'injected_file_content_bytes': {
+ 'type': 'object',
+ 'properties': {
+ 'reserved': {'type': 'integer'},
+ 'limit': {'type': 'integer'},
+ 'in_use': {'type': 'integer'}
+ }
+ },
+ 'injected_file_path_bytes': {
+ 'type': 'object',
+ 'properties': {
+ 'reserved': {'type': 'integer'},
+ 'limit': {'type': 'integer'},
+ 'in_use': {'type': 'integer'}
+ }
+ }
+}
+
+delete_quota = {
+ 'status_code': [202]
+}
diff --git a/tempest/lib/api_schema/response/compute/v2_1/security_group_default_rule.py b/tempest/lib/api_schema/response/compute/v2_1/security_group_default_rule.py
new file mode 100644
index 0000000..2ec2826
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_1/security_group_default_rule.py
@@ -0,0 +1,65 @@
+# Copyright 2014 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.
+
+common_security_group_default_rule_info = {
+ 'type': 'object',
+ 'properties': {
+ 'from_port': {'type': 'integer'},
+ 'id': {'type': 'integer'},
+ 'ip_protocol': {'type': 'string'},
+ 'ip_range': {
+ 'type': 'object',
+ 'properties': {
+ 'cidr': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ 'required': ['cidr'],
+ },
+ 'to_port': {'type': 'integer'},
+ },
+ 'additionalProperties': False,
+ 'required': ['from_port', 'id', 'ip_protocol', 'ip_range', 'to_port'],
+}
+
+create_get_security_group_default_rule = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'security_group_default_rule':
+ common_security_group_default_rule_info
+ },
+ 'additionalProperties': False,
+ 'required': ['security_group_default_rule']
+ }
+}
+
+delete_security_group_default_rule = {
+ 'status_code': [204]
+}
+
+list_security_group_default_rules = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'security_group_default_rules': {
+ 'type': 'array',
+ 'items': common_security_group_default_rule_info
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['security_group_default_rules']
+ }
+}
diff --git a/tempest/lib/api_schema/response/compute/v2_1/security_groups.py b/tempest/lib/api_schema/response/compute/v2_1/security_groups.py
new file mode 100644
index 0000000..5ed5a5c
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_1/security_groups.py
@@ -0,0 +1,113 @@
+# Copyright 2014 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.
+
+common_security_group_rule = {
+ 'from_port': {'type': ['integer', 'null']},
+ 'to_port': {'type': ['integer', 'null']},
+ 'group': {
+ 'type': 'object',
+ 'properties': {
+ 'tenant_id': {'type': 'string'},
+ 'name': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ },
+ 'ip_protocol': {'type': ['string', 'null']},
+ # 'parent_group_id' can be UUID so defining it as 'string' also.
+ 'parent_group_id': {'type': ['string', 'integer', 'null']},
+ 'ip_range': {
+ 'type': 'object',
+ 'properties': {
+ 'cidr': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ # When optional argument is provided in request body
+ # like 'group_id' then, attribute 'cidr' does not
+ # comes in response body. So it is not 'required'.
+ },
+ 'id': {'type': ['string', 'integer']}
+}
+
+common_security_group = {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': ['integer', 'string']},
+ 'name': {'type': 'string'},
+ 'tenant_id': {'type': 'string'},
+ 'rules': {
+ 'type': 'array',
+ 'items': {
+ 'type': ['object', 'null'],
+ 'properties': common_security_group_rule,
+ 'additionalProperties': False,
+ }
+ },
+ 'description': {'type': 'string'},
+ },
+ 'additionalProperties': False,
+ 'required': ['id', 'name', 'tenant_id', 'rules', 'description'],
+}
+
+list_security_groups = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'security_groups': {
+ 'type': 'array',
+ 'items': common_security_group
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['security_groups']
+ }
+}
+
+get_security_group = create_security_group = update_security_group = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'security_group': common_security_group
+ },
+ 'additionalProperties': False,
+ 'required': ['security_group']
+ }
+}
+
+delete_security_group = {
+ 'status_code': [202]
+}
+
+create_security_group_rule = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'security_group_rule': {
+ 'type': 'object',
+ 'properties': common_security_group_rule,
+ 'additionalProperties': False,
+ 'required': ['from_port', 'to_port', 'group', 'ip_protocol',
+ 'parent_group_id', 'id', 'ip_range']
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['security_group_rule']
+ }
+}
+
+delete_security_group_rule = {
+ 'status_code': [202]
+}
diff --git a/tempest/lib/api_schema/response/compute/v2_1/servers.py b/tempest/lib/api_schema/response/compute/v2_1/servers.py
new file mode 100644
index 0000000..2954de0
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_1/servers.py
@@ -0,0 +1,596 @@
+# Copyright 2014 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.
+
+import copy
+
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+
+create_server = {
+ 'status_code': [202],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'server': {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'security_groups': {'type': 'array'},
+ 'links': parameter_types.links,
+ 'OS-DCF:diskConfig': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ # NOTE: OS-DCF:diskConfig & security_groups are API extension,
+ # and some environments return a response without these
+ # attributes.So they are not 'required'.
+ 'required': ['id', 'links']
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['server']
+ }
+}
+
+create_server_with_admin_pass = copy.deepcopy(create_server)
+create_server_with_admin_pass['response_body']['properties']['server'][
+ 'properties'].update({'adminPass': {'type': 'string'}})
+create_server_with_admin_pass['response_body']['properties']['server'][
+ 'required'].append('adminPass')
+
+list_servers = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'servers': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'links': parameter_types.links,
+ 'name': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ 'required': ['id', 'links', 'name']
+ }
+ },
+ 'servers_links': parameter_types.links
+ },
+ 'additionalProperties': False,
+ # NOTE(gmann): servers_links attribute is not necessary to be
+ # present always So it is not 'required'.
+ 'required': ['servers']
+ }
+}
+
+delete_server = {
+ 'status_code': [204],
+}
+
+common_show_server = {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'name': {'type': 'string'},
+ 'status': {'type': 'string'},
+ 'image': {'oneOf': [
+ {'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'links': parameter_types.links
+ },
+ 'additionalProperties': False,
+ 'required': ['id', 'links']},
+ {'type': ['string', 'null']}
+ ]},
+ 'flavor': {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'links': parameter_types.links
+ },
+ # NOTE(gmann): This will be empty object if there is no
+ # flavor info present in DB. This can happen when flavor info is
+ # deleted after server creation.
+ 'additionalProperties': False
+ },
+ 'fault': {
+ 'type': 'object',
+ 'properties': {
+ 'code': {'type': 'integer'},
+ 'created': {'type': 'string'},
+ 'message': {'type': 'string'},
+ 'details': {'type': 'string'},
+ },
+ 'additionalProperties': False,
+ # NOTE(gmann): 'details' is not necessary to be present
+ # in the 'fault'. So it is not defined as 'required'.
+ 'required': ['code', 'created', 'message']
+ },
+ 'user_id': {'type': 'string'},
+ 'tenant_id': {'type': 'string'},
+ 'created': parameter_types.date_time,
+ 'updated': parameter_types.date_time,
+ 'progress': {'type': 'integer'},
+ 'metadata': {'type': 'object'},
+ 'links': parameter_types.links,
+ 'addresses': parameter_types.addresses,
+ 'hostId': {'type': 'string'},
+ 'OS-DCF:diskConfig': {'type': 'string'},
+ 'accessIPv4': parameter_types.access_ip_v4,
+ 'accessIPv6': parameter_types.access_ip_v6
+ },
+ 'additionalProperties': False,
+ # NOTE(GMann): 'progress' attribute is present in the response
+ # only when server's status is one of the progress statuses
+ # ("ACTIVE","BUILD", "REBUILD", "RESIZE","VERIFY_RESIZE")
+ # 'fault' attribute is present in the response
+ # only when server's status is one of the "ERROR", "DELETED".
+ # OS-DCF:diskConfig and accessIPv4/v6 are API
+ # extensions, and some environments return a response
+ # without these attributes.So these are not defined as 'required'.
+ 'required': ['id', 'name', 'status', 'image', 'flavor',
+ 'user_id', 'tenant_id', 'created', 'updated',
+ 'metadata', 'links', 'addresses', 'hostId']
+}
+
+update_server = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'server': common_show_server
+ },
+ 'additionalProperties': False,
+ 'required': ['server']
+ }
+}
+
+server_detail = copy.deepcopy(common_show_server)
+server_detail['properties'].update({
+ 'key_name': {'type': ['string', 'null']},
+ 'security_groups': {'type': 'array'},
+
+ # NOTE: Non-admin users also can see "OS-SRV-USG" and "OS-EXT-AZ"
+ # attributes.
+ 'OS-SRV-USG:launched_at': {'type': ['string', 'null']},
+ 'OS-SRV-USG:terminated_at': {'type': ['string', 'null']},
+ 'OS-EXT-AZ:availability_zone': {'type': 'string'},
+
+ # NOTE: Admin users only can see "OS-EXT-STS" and "OS-EXT-SRV-ATTR"
+ # attributes.
+ 'OS-EXT-STS:task_state': {'type': ['string', 'null']},
+ 'OS-EXT-STS:vm_state': {'type': 'string'},
+ 'OS-EXT-STS:power_state': parameter_types.power_state,
+ 'OS-EXT-SRV-ATTR:host': {'type': ['string', 'null']},
+ 'OS-EXT-SRV-ATTR:instance_name': {'type': 'string'},
+ 'OS-EXT-SRV-ATTR:hypervisor_hostname': {'type': ['string', 'null']},
+ 'os-extended-volumes:volumes_attached': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ },
+ },
+ 'config_drive': {'type': 'string'}
+})
+server_detail['properties']['addresses']['patternProperties'][
+ '^[a-zA-Z0-9-_.]+$']['items']['properties'].update({
+ 'OS-EXT-IPS:type': {'type': 'string'},
+ 'OS-EXT-IPS-MAC:mac_addr': parameter_types.mac_address})
+# NOTE(gmann): Update OS-EXT-IPS:type and OS-EXT-IPS-MAC:mac_addr
+# attributes in server address. Those are API extension,
+# and some environments return a response without
+# these attributes. So they are not 'required'.
+
+get_server = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'server': server_detail
+ },
+ 'additionalProperties': False,
+ 'required': ['server']
+ }
+}
+
+list_servers_detail = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'servers': {
+ 'type': 'array',
+ 'items': server_detail
+ },
+ 'servers_links': parameter_types.links
+ },
+ 'additionalProperties': False,
+ # NOTE(gmann): servers_links attribute is not necessary to be
+ # present always So it is not 'required'.
+ 'required': ['servers']
+ }
+}
+
+rebuild_server = copy.deepcopy(update_server)
+rebuild_server['status_code'] = [202]
+
+rebuild_server_with_admin_pass = copy.deepcopy(rebuild_server)
+rebuild_server_with_admin_pass['response_body']['properties']['server'][
+ 'properties'].update({'adminPass': {'type': 'string'}})
+rebuild_server_with_admin_pass['response_body']['properties']['server'][
+ 'required'].append('adminPass')
+
+rescue_server = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'additionalProperties': False,
+ }
+}
+
+rescue_server_with_admin_pass = copy.deepcopy(rescue_server)
+rescue_server_with_admin_pass['response_body'].update(
+ {'properties': {'adminPass': {'type': 'string'}}})
+rescue_server_with_admin_pass['response_body'].update(
+ {'required': ['adminPass']})
+
+
+list_virtual_interfaces = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'virtual_interfaces': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'mac_address': parameter_types.mac_address,
+ 'OS-EXT-VIF-NET:net_id': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ # 'OS-EXT-VIF-NET:net_id' is API extension So it is
+ # not defined as 'required'
+ 'required': ['id', 'mac_address']
+ }
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['virtual_interfaces']
+ }
+}
+
+common_attach_volume_info = {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'device': {'type': ['string', 'null']},
+ 'volumeId': {'type': 'string'},
+ 'serverId': {'type': ['integer', 'string']}
+ },
+ 'additionalProperties': False,
+ # 'device' is optional in response.
+ 'required': ['id', 'volumeId', 'serverId']
+}
+
+attach_volume = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'volumeAttachment': common_attach_volume_info
+ },
+ 'additionalProperties': False,
+ 'required': ['volumeAttachment']
+ }
+}
+
+detach_volume = {
+ 'status_code': [202]
+}
+
+show_volume_attachment = copy.deepcopy(attach_volume)
+show_volume_attachment['response_body']['properties'][
+ 'volumeAttachment']['properties'].update({'serverId': {'type': 'string'}})
+
+list_volume_attachments = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'volumeAttachments': {
+ 'type': 'array',
+ 'items': common_attach_volume_info
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['volumeAttachments']
+ }
+}
+list_volume_attachments['response_body']['properties'][
+ 'volumeAttachments']['items']['properties'].update(
+ {'serverId': {'type': 'string'}})
+
+list_addresses_by_network = {
+ 'status_code': [200],
+ 'response_body': parameter_types.addresses
+}
+
+list_addresses = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'addresses': parameter_types.addresses
+ },
+ 'additionalProperties': False,
+ 'required': ['addresses']
+ }
+}
+
+common_server_group = {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'name': {'type': 'string'},
+ 'policies': {
+ 'type': 'array',
+ 'items': {'type': 'string'}
+ },
+ # 'members' attribute contains the array of instance's UUID of
+ # instances present in server group
+ 'members': {
+ 'type': 'array',
+ 'items': {'type': 'string'}
+ },
+ 'metadata': {'type': 'object'}
+ },
+ 'additionalProperties': False,
+ 'required': ['id', 'name', 'policies', 'members', 'metadata']
+}
+
+create_show_server_group = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'server_group': common_server_group
+ },
+ 'additionalProperties': False,
+ 'required': ['server_group']
+ }
+}
+
+delete_server_group = {
+ 'status_code': [204]
+}
+
+list_server_groups = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'server_groups': {
+ 'type': 'array',
+ 'items': common_server_group
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['server_groups']
+ }
+}
+
+instance_actions = {
+ 'type': 'object',
+ 'properties': {
+ 'action': {'type': 'string'},
+ 'request_id': {'type': 'string'},
+ 'user_id': {'type': 'string'},
+ 'project_id': {'type': 'string'},
+ 'start_time': parameter_types.date_time,
+ 'message': {'type': ['string', 'null']},
+ 'instance_uuid': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ 'required': ['action', 'request_id', 'user_id', 'project_id',
+ 'start_time', 'message', 'instance_uuid']
+}
+
+instance_action_events = {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'event': {'type': 'string'},
+ 'start_time': parameter_types.date_time,
+ # The finish_time, result and optionally traceback are all
+ # possibly None (null) until the event is actually finished.
+ # The traceback would only be set if there was an error, but
+ # when the event is complete both finish_time and result will
+ # be set.
+ 'finish_time': parameter_types.date_time_or_null,
+ 'result': {'type': ['string', 'null']},
+ 'traceback': {'type': ['string', 'null']}
+ },
+ 'additionalProperties': False,
+ 'required': ['event', 'start_time', 'finish_time', 'result',
+ 'traceback']
+ }
+}
+
+list_instance_actions = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'instanceActions': {
+ 'type': 'array',
+ 'items': instance_actions
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['instanceActions']
+ }
+}
+
+instance_actions_with_events = copy.deepcopy(instance_actions)
+instance_actions_with_events['properties'].update({
+ 'events': instance_action_events})
+# 'events' does not come in response body always so it is not
+# defined as 'required'
+
+show_instance_action = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'instanceAction': instance_actions_with_events
+ },
+ 'additionalProperties': False,
+ 'required': ['instanceAction']
+ }
+}
+
+show_password = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'password': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ 'required': ['password']
+ }
+}
+
+get_vnc_console = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'console': {
+ 'type': 'object',
+ 'properties': {
+ 'type': {'type': 'string'},
+ 'url': {
+ 'type': 'string',
+ 'format': 'uri'
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['type', 'url']
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['console']
+ }
+}
+
+get_console_output = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'output': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ 'required': ['output']
+ }
+}
+
+set_server_metadata = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'metadata': {
+ 'type': 'object',
+ 'patternProperties': {
+ '^.+$': {'type': 'string'}
+ }
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['metadata']
+ }
+}
+
+list_server_metadata = copy.deepcopy(set_server_metadata)
+
+update_server_metadata = copy.deepcopy(set_server_metadata)
+
+delete_server_metadata_item = {
+ 'status_code': [204]
+}
+
+set_show_server_metadata_item = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'meta': {
+ 'type': 'object',
+ 'patternProperties': {
+ '^.+$': {'type': 'string'}
+ }
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['meta']
+ }
+}
+
+server_actions_common_schema = {
+ 'status_code': [202]
+}
+
+server_actions_delete_password = {
+ 'status_code': [204]
+}
+
+server_actions_confirm_resize = copy.deepcopy(
+ server_actions_delete_password)
+
+update_attached_volume = {
+ 'status_code': [202]
+}
+
+evacuate_server = {
+ 'status_code': [200]
+}
+
+evacuate_server_with_admin_pass = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'adminPass': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ 'required': ['adminPass']
+ }
+}
+
+show_server_diagnostics = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object'
+ }
+}
diff --git a/tempest/lib/api_schema/response/compute/v2_1/services.py b/tempest/lib/api_schema/response/compute/v2_1/services.py
new file mode 100644
index 0000000..3b58ece
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_1/services.py
@@ -0,0 +1,89 @@
+# Copyright 2014 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
+
+list_services = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'services': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': ['integer', 'string'],
+ 'pattern': '^[a-zA-Z!]*@[0-9]+$'},
+ 'zone': {'type': 'string'},
+ 'host': {'type': 'string'},
+ 'state': {'type': 'string'},
+ 'binary': {'type': 'string'},
+ 'status': {'type': 'string'},
+ 'updated_at': parameter_types.date_time_or_null,
+ 'disabled_reason': {'type': ['string', 'null']}
+ },
+ 'additionalProperties': False,
+ 'required': ['id', 'zone', 'host', 'state', 'binary',
+ 'status', 'updated_at', 'disabled_reason']
+ }
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['services']
+ }
+}
+
+enable_disable_service = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'service': {
+ 'type': 'object',
+ 'properties': {
+ 'status': {'type': 'string'},
+ 'binary': {'type': 'string'},
+ 'host': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ 'required': ['status', 'binary', 'host']
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['service']
+ }
+}
+
+disable_log_reason = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'service': {
+ 'type': 'object',
+ 'properties': {
+ 'disabled_reason': {'type': 'string'},
+ 'binary': {'type': 'string'},
+ 'host': {'type': 'string'},
+ 'status': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ 'required': ['disabled_reason', 'binary', 'host', 'status']
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['service']
+ }
+}
diff --git a/tempest/lib/api_schema/response/compute/v2_1/snapshots.py b/tempest/lib/api_schema/response/compute/v2_1/snapshots.py
new file mode 100644
index 0000000..826f854
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_1/snapshots.py
@@ -0,0 +1,63 @@
+# Copyright 2015 Fujitsu(fnst) 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
+
+common_snapshot_info = {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'volumeId': {'type': 'string'},
+ 'status': {'type': 'string'},
+ 'size': {'type': 'integer'},
+ 'createdAt': parameter_types.date_time,
+ 'displayName': {'type': ['string', 'null']},
+ 'displayDescription': {'type': ['string', 'null']}
+ },
+ 'additionalProperties': False,
+ 'required': ['id', 'volumeId', 'status', 'size',
+ 'createdAt', 'displayName', 'displayDescription']
+}
+
+create_get_snapshot = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'snapshot': common_snapshot_info
+ },
+ 'additionalProperties': False,
+ 'required': ['snapshot']
+ }
+}
+
+list_snapshots = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'snapshots': {
+ 'type': 'array',
+ 'items': common_snapshot_info
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['snapshots']
+ }
+}
+
+delete_snapshot = {
+ 'status_code': [202]
+}
diff --git a/tempest/lib/api_schema/response/compute/v2_1/tenant_networks.py b/tempest/lib/api_schema/response/compute/v2_1/tenant_networks.py
new file mode 100644
index 0000000..ddfab96
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_1/tenant_networks.py
@@ -0,0 +1,53 @@
+# Copyright 2015 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.
+
+param_network = {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'cidr': {'type': ['string', 'null']},
+ 'label': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ 'required': ['id', 'cidr', 'label']
+}
+
+
+list_tenant_networks = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'networks': {
+ 'type': 'array',
+ 'items': param_network
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['networks']
+ }
+}
+
+
+get_tenant_network = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'network': param_network
+ },
+ 'additionalProperties': False,
+ 'required': ['network']
+ }
+}
diff --git a/tempest/lib/api_schema/response/compute/v2_1/tenant_usages.py b/tempest/lib/api_schema/response/compute/v2_1/tenant_usages.py
new file mode 100644
index 0000000..b531d2e
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_1/tenant_usages.py
@@ -0,0 +1,89 @@
+# Copyright 2014 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.
+
+import copy
+
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+
+_server_usages = {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'ended_at': parameter_types.date_time_or_null,
+ 'flavor': {'type': 'string'},
+ 'hours': {'type': 'number'},
+ 'instance_id': {'type': 'string'},
+ 'local_gb': {'type': 'integer'},
+ 'memory_mb': {'type': 'integer'},
+ 'name': {'type': 'string'},
+ 'started_at': parameter_types.date_time,
+ 'state': {'type': 'string'},
+ 'tenant_id': {'type': 'string'},
+ 'uptime': {'type': 'integer'},
+ 'vcpus': {'type': 'integer'},
+ },
+ 'required': ['ended_at', 'flavor', 'hours', 'instance_id', 'local_gb',
+ 'memory_mb', 'name', 'started_at', 'state', 'tenant_id',
+ 'uptime', 'vcpus']
+ }
+}
+
+_tenant_usage_list = {
+ 'type': 'object',
+ 'properties': {
+ 'server_usages': _server_usages,
+ 'start': parameter_types.date_time,
+ 'stop': parameter_types.date_time,
+ 'tenant_id': {'type': 'string'},
+ 'total_hours': {'type': 'number'},
+ 'total_local_gb_usage': {'type': 'number'},
+ 'total_memory_mb_usage': {'type': 'number'},
+ 'total_vcpus_usage': {'type': 'number'},
+ },
+ 'required': ['start', 'stop', 'tenant_id',
+ 'total_hours', 'total_local_gb_usage',
+ 'total_memory_mb_usage', 'total_vcpus_usage']
+}
+
+# 'required' of get_tenant is different from list_tenant's.
+_tenant_usage_get = copy.deepcopy(_tenant_usage_list)
+_tenant_usage_get['required'] = ['server_usages', 'start', 'stop', 'tenant_id',
+ 'total_hours', 'total_local_gb_usage',
+ 'total_memory_mb_usage', 'total_vcpus_usage']
+
+list_tenant_usage = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'tenant_usages': {
+ 'type': 'array',
+ 'items': _tenant_usage_list
+ }
+ },
+ 'required': ['tenant_usages']
+ }
+}
+
+get_tenant_usage = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'tenant_usage': _tenant_usage_get
+ },
+ 'required': ['tenant_usage']
+ }
+}
diff --git a/tempest/lib/api_schema/response/compute/v2_1/versions.py b/tempest/lib/api_schema/response/compute/v2_1/versions.py
new file mode 100644
index 0000000..7f56239
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_1/versions.py
@@ -0,0 +1,112 @@
+# Copyright 2015 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.
+
+import copy
+
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+
+
+_version = {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'links': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'href': {'type': 'string', 'format': 'uri'},
+ 'rel': {'type': 'string'},
+ 'type': {'type': 'string'},
+ },
+ 'required': ['href', 'rel'],
+ 'additionalProperties': False
+ }
+ },
+ 'status': {'type': 'string'},
+ 'updated': parameter_types.date_time,
+ 'version': {'type': 'string'},
+ 'min_version': {'type': 'string'},
+ 'media-types': {
+ 'type': 'array',
+ 'properties': {
+ 'base': {'type': 'string'},
+ 'type': {'type': 'string'},
+ }
+ },
+ },
+ # NOTE: version and min_version have been added since Kilo,
+ # so they should not be required.
+ # NOTE(sdague): media-types only shows up in single version requests.
+ 'required': ['id', 'links', 'status', 'updated'],
+ 'additionalProperties': False
+}
+
+list_versions = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'versions': {
+ 'type': 'array',
+ 'items': _version
+ }
+ },
+ 'required': ['versions'],
+ 'additionalProperties': False
+ }
+}
+
+
+_detail_get_version = copy.deepcopy(_version)
+_detail_get_version['properties'].pop('min_version')
+_detail_get_version['properties'].pop('version')
+_detail_get_version['properties'].pop('updated')
+_detail_get_version['properties']['media-types'] = {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'base': {'type': 'string'},
+ 'type': {'type': 'string'}
+ }
+ }
+}
+_detail_get_version['required'] = ['id', 'links', 'status', 'media-types']
+
+get_version = {
+ 'status_code': [300],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'choices': {
+ 'type': 'array',
+ 'items': _detail_get_version
+ }
+ },
+ 'required': ['choices'],
+ 'additionalProperties': False
+ }
+}
+
+get_one_version = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'version': _version
+ },
+ 'additionalProperties': False
+ }
+}
diff --git a/tempest/lib/api_schema/response/compute/v2_1/volumes.py b/tempest/lib/api_schema/response/compute/v2_1/volumes.py
new file mode 100644
index 0000000..c35dae9
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_1/volumes.py
@@ -0,0 +1,122 @@
+# Copyright 2014 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
+
+create_get_volume = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'volume': {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'status': {'type': 'string'},
+ 'displayName': {'type': ['string', 'null']},
+ 'availabilityZone': {'type': 'string'},
+ 'createdAt': parameter_types.date_time,
+ 'displayDescription': {'type': ['string', 'null']},
+ 'volumeType': {'type': ['string', 'null']},
+ 'snapshotId': {'type': ['string', 'null']},
+ 'metadata': {'type': 'object'},
+ 'size': {'type': 'integer'},
+ 'attachments': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'device': {'type': 'string'},
+ 'volumeId': {'type': 'string'},
+ 'serverId': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ # NOTE- If volume is not attached to any server
+ # then, 'attachments' attributes comes as array
+ # with empty objects "[{}]" due to that elements
+ # of 'attachments' cannot defined as 'required'.
+ # If it would come as empty array "[]" then,
+ # those elements can be defined as 'required'.
+ }
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['id', 'status', 'displayName', 'availabilityZone',
+ 'createdAt', 'displayDescription', 'volumeType',
+ 'snapshotId', 'metadata', 'size', 'attachments']
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['volume']
+ }
+}
+
+list_volumes = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'volumes': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'status': {'type': 'string'},
+ 'displayName': {'type': ['string', 'null']},
+ 'availabilityZone': {'type': 'string'},
+ 'createdAt': parameter_types.date_time,
+ 'displayDescription': {'type': ['string', 'null']},
+ 'volumeType': {'type': ['string', 'null']},
+ 'snapshotId': {'type': ['string', 'null']},
+ 'metadata': {'type': 'object'},
+ 'size': {'type': 'integer'},
+ 'attachments': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'device': {'type': 'string'},
+ 'volumeId': {'type': 'string'},
+ 'serverId': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ # NOTE- If volume is not attached to any server
+ # then, 'attachments' attributes comes as array
+ # with empty object "[{}]" due to that elements
+ # of 'attachments' cannot defined as 'required'
+ # If it would come as empty array "[]" then,
+ # those elements can be defined as 'required'.
+ }
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['id', 'status', 'displayName',
+ 'availabilityZone', 'createdAt',
+ 'displayDescription', 'volumeType',
+ 'snapshotId', 'metadata', 'size',
+ 'attachments']
+ }
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['volumes']
+ }
+}
+
+delete_volume = {
+ 'status_code': [202]
+}
diff --git a/tempest/api_schema/response/compute/v2_1/__init__.py b/tempest/lib/api_schema/response/compute/v2_11/__init__.py
similarity index 100%
copy from tempest/api_schema/response/compute/v2_1/__init__.py
copy to tempest/lib/api_schema/response/compute/v2_11/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_11/services.py b/tempest/lib/api_schema/response/compute/v2_11/services.py
new file mode 100644
index 0000000..18b833b
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_11/services.py
@@ -0,0 +1,46 @@
+# Copyright 2017 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 copy
+
+from tempest.lib.api_schema.response.compute.v2_1 import services
+
+
+list_services = copy.deepcopy(services.list_services)
+list_services['response_body']['properties']['services']['items'][
+ 'properties']['forced_down'] = {'type': 'boolean'}
+list_services['response_body']['properties']['services']['items'][
+ 'required'].append('forced_down')
+
+update_forced_down = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'service': {
+ 'type': 'object',
+ 'properties': {
+ 'binary': {'type': 'string'},
+ 'host': {'type': 'string'},
+ 'forced_down': {'type': 'boolean'}
+ },
+ 'additionalProperties': False,
+ 'required': ['binary', 'host', 'forced_down']
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['service']
+ }
+}
diff --git a/tempest/services/identity/v3/__init__.py b/tempest/lib/api_schema/response/compute/v2_13/__init__.py
similarity index 100%
copy from tempest/services/identity/v3/__init__.py
copy to tempest/lib/api_schema/response/compute/v2_13/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_13/servers.py b/tempest/lib/api_schema/response/compute/v2_13/servers.py
new file mode 100644
index 0000000..a90f3e4
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_13/servers.py
@@ -0,0 +1,34 @@
+# Copyright 2017 NTT 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 copy
+
+from tempest.lib.api_schema.response.compute.v2_1 import servers
+
+
+common_server_group = copy.deepcopy(servers.common_server_group)
+common_server_group['properties']['project_id'] = {'type': 'string'}
+common_server_group['properties']['user_id'] = {'type': 'string'}
+common_server_group['required'].append('project_id')
+common_server_group['required'].append('user_id')
+
+create_show_server_group = copy.deepcopy(servers.create_show_server_group)
+create_show_server_group['response_body']['properties'][
+ 'server_group'] = common_server_group
+
+delete_server_group = copy.deepcopy(servers.delete_server_group)
+
+list_server_groups = copy.deepcopy(servers.list_server_groups)
+list_server_groups['response_body']['properties']['server_groups'][
+ 'items'] = common_server_group
diff --git a/tempest/api/baremetal/__init__.py b/tempest/lib/api_schema/response/compute/v2_16/__init__.py
similarity index 100%
copy from tempest/api/baremetal/__init__.py
copy to tempest/lib/api_schema/response/compute/v2_16/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_16/servers.py b/tempest/lib/api_schema/response/compute/v2_16/servers.py
new file mode 100644
index 0000000..3eb658f
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_16/servers.py
@@ -0,0 +1,160 @@
+# Copyright 2014 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.
+import copy
+
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+from tempest.lib.api_schema.response.compute.v2_9 import servers
+
+# Compute microversion 2.16:
+# 1. New attributes in 'server' dict.
+# 'host_status'
+
+server_detail = {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'name': {'type': 'string'},
+ 'status': {'type': 'string'},
+ 'image': {'oneOf': [
+ {'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'links': parameter_types.links
+ },
+ 'additionalProperties': False,
+ 'required': ['id', 'links']},
+ {'type': ['string', 'null']}
+ ]},
+ 'flavor': {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'links': parameter_types.links
+ },
+ 'additionalProperties': False,
+ 'required': ['id', 'links']
+ },
+ 'fault': {
+ 'type': 'object',
+ 'properties': {
+ 'code': {'type': 'integer'},
+ 'created': {'type': 'string'},
+ 'message': {'type': 'string'},
+ 'details': {'type': 'string'},
+ },
+ 'additionalProperties': False,
+ # NOTE(gmann): 'details' is not necessary to be present
+ # in the 'fault'. So it is not defined as 'required'.
+ 'required': ['code', 'created', 'message']
+ },
+ 'user_id': {'type': 'string'},
+ 'tenant_id': {'type': 'string'},
+ 'created': {'type': 'string'},
+ 'updated': {'type': 'string'},
+ 'progress': {'type': 'integer'},
+ 'metadata': {'type': 'object'},
+ 'links': parameter_types.links,
+ 'addresses': parameter_types.addresses,
+ 'hostId': {'type': 'string'},
+ 'OS-DCF:diskConfig': {'type': 'string'},
+ 'accessIPv4': parameter_types.access_ip_v4,
+ 'accessIPv6': parameter_types.access_ip_v6,
+ 'key_name': {'type': ['string', 'null']},
+ 'security_groups': {'type': 'array'},
+ 'OS-SRV-USG:launched_at': {'type': ['string', 'null']},
+ 'OS-SRV-USG:terminated_at': {'type': ['string', 'null']},
+ 'OS-EXT-AZ:availability_zone': {'type': 'string'},
+ 'OS-EXT-STS:task_state': {'type': ['string', 'null']},
+ 'OS-EXT-STS:vm_state': {'type': 'string'},
+ 'OS-EXT-STS:power_state': parameter_types.power_state,
+ 'OS-EXT-SRV-ATTR:host': {'type': ['string', 'null']},
+ 'OS-EXT-SRV-ATTR:instance_name': {'type': 'string'},
+ 'OS-EXT-SRV-ATTR:hypervisor_hostname': {'type': ['string', 'null']},
+ 'config_drive': {'type': 'string'},
+ 'os-extended-volumes:volumes_attached': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'delete_on_termination': {'type': 'boolean'}
+ },
+ 'additionalProperties': False,
+ },
+ },
+ 'OS-EXT-SRV-ATTR:reservation_id': {'type': ['string', 'null']},
+ 'OS-EXT-SRV-ATTR:launch_index': {'type': 'integer'},
+ 'OS-EXT-SRV-ATTR:kernel_id': {'type': ['string', 'null']},
+ 'OS-EXT-SRV-ATTR:ramdisk_id': {'type': ['string', 'null']},
+ 'OS-EXT-SRV-ATTR:hostname': {'type': 'string'},
+ 'OS-EXT-SRV-ATTR:root_device_name': {'type': ['string', 'null']},
+ 'OS-EXT-SRV-ATTR:user_data': {'type': ['string', 'null']},
+ 'locked': {'type': 'boolean'},
+ # NOTE(gmann): new attributes in version 2.16
+ 'host_status': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ # NOTE(gmann): 'progress' attribute is present in the response
+ # only when server's status is one of the progress statuses
+ # ("ACTIVE","BUILD", "REBUILD", "RESIZE","VERIFY_RESIZE")
+ # 'fault' attribute is present in the response
+ # only when server's status is one of the "ERROR", "DELETED".
+ # OS-DCF:diskConfig and accessIPv4/v6 are API
+ # extensions, and some environments return a response
+ # without these attributes.So these are not defined as 'required'.
+ 'required': ['id', 'name', 'status', 'image', 'flavor',
+ 'user_id', 'tenant_id', 'created', 'updated',
+ 'metadata', 'links', 'addresses', 'hostId']
+}
+
+server_detail['properties']['addresses']['patternProperties'][
+ '^[a-zA-Z0-9-_.]+$']['items']['properties'].update({
+ 'OS-EXT-IPS:type': {'type': 'string'},
+ 'OS-EXT-IPS-MAC:mac_addr': parameter_types.mac_address})
+# NOTE(gmann)dd: Update OS-EXT-IPS:type and OS-EXT-IPS-MAC:mac_addr
+# attributes in server address. Those are API extension,
+# and some environments return a response without
+# these attributes. So they are not 'required'.
+
+get_server = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'server': server_detail
+ },
+ 'additionalProperties': False,
+ 'required': ['server']
+ }
+}
+
+list_servers_detail = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'servers': {
+ 'type': 'array',
+ 'items': server_detail
+ },
+ 'servers_links': parameter_types.links
+ },
+ 'additionalProperties': False,
+ # NOTE(gmann): servers_links attribute is not necessary to be
+ # present always So it is not 'required'.
+ 'required': ['servers']
+ }
+}
+
+list_servers = copy.deepcopy(servers.list_servers)
diff --git a/tempest/api/baremetal/__init__.py b/tempest/lib/api_schema/response/compute/v2_19/__init__.py
similarity index 100%
copy from tempest/api/baremetal/__init__.py
copy to tempest/lib/api_schema/response/compute/v2_19/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_19/servers.py b/tempest/lib/api_schema/response/compute/v2_19/servers.py
new file mode 100644
index 0000000..fd9e933
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_19/servers.py
@@ -0,0 +1,52 @@
+# Copyright 2016 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.
+
+import copy
+
+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)
+
+get_server = copy.deepcopy(serversv216.get_server)
+get_server['response_body']['properties']['server'][
+ 'properties'].update({'description': {'type': ['string', 'null']}})
+get_server['response_body']['properties']['server'][
+ 'required'].append('description')
+
+list_servers_detail = copy.deepcopy(serversv216.list_servers_detail)
+list_servers_detail['response_body']['properties']['servers']['items'][
+ 'properties'].update({'description': {'type': ['string', 'null']}})
+list_servers_detail['response_body']['properties']['servers']['items'][
+ 'required'].append('description')
+
+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(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(
+ 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'][
+ 'required'].append('description')
diff --git a/tempest/api/image/admin/v2/__init__.py b/tempest/lib/api_schema/response/compute/v2_2/__init__.py
similarity index 100%
rename from tempest/api/image/admin/v2/__init__.py
rename to tempest/lib/api_schema/response/compute/v2_2/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_2/keypairs.py b/tempest/lib/api_schema/response/compute/v2_2/keypairs.py
new file mode 100644
index 0000000..0bb7771
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_2/keypairs.py
@@ -0,0 +1,41 @@
+# Copyright 2016 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.
+
+import copy
+
+from tempest.lib.api_schema.response.compute.v2_1 import keypairs
+
+get_keypair = copy.deepcopy(keypairs.get_keypair)
+get_keypair['response_body']['properties']['keypair'][
+ 'properties'].update({'type': {'type': 'string'}})
+get_keypair['response_body']['properties']['keypair'][
+ 'required'].append('type')
+
+create_keypair = copy.deepcopy(keypairs.create_keypair)
+create_keypair['status_code'] = [201]
+create_keypair['response_body']['properties']['keypair'][
+ 'properties'].update({'type': {'type': 'string'}})
+create_keypair['response_body']['properties']['keypair'][
+ 'required'].append('type')
+
+delete_keypair = {
+ 'status_code': [204],
+}
+
+list_keypairs = copy.deepcopy(keypairs.list_keypairs)
+list_keypairs['response_body']['properties']['keypairs'][
+ 'items']['properties']['keypair'][
+ 'properties'].update({'type': {'type': 'string'}})
+list_keypairs['response_body']['properties']['keypairs'][
+ 'items']['properties']['keypair']['required'].append('type')
diff --git a/tempest/services/identity/v3/__init__.py b/tempest/lib/api_schema/response/compute/v2_23/__init__.py
similarity index 100%
copy from tempest/services/identity/v3/__init__.py
copy to tempest/lib/api_schema/response/compute/v2_23/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_23/migrations.py b/tempest/lib/api_schema/response/compute/v2_23/migrations.py
new file mode 100644
index 0000000..3cd0f6e
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_23/migrations.py
@@ -0,0 +1,62 @@
+# Copyright 2014 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
+
+# Compute microversion 2.23:
+# New attributes in 'migrations' list.
+# 'migration_type'
+# 'links'
+
+list_migrations = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'migrations': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'integer'},
+ 'status': {'type': ['string', 'null']},
+ 'instance_uuid': {'type': ['string', 'null']},
+ 'source_node': {'type': ['string', 'null']},
+ 'source_compute': {'type': ['string', 'null']},
+ 'dest_node': {'type': ['string', 'null']},
+ 'dest_compute': {'type': ['string', 'null']},
+ 'dest_host': {'type': ['string', 'null']},
+ 'old_instance_type_id': {'type': ['integer', 'null']},
+ 'new_instance_type_id': {'type': ['integer', 'null']},
+ 'created_at': {'type': 'string'},
+ 'updated_at': {'type': ['string', 'null']},
+ # New attributes in version 2.23
+ 'migration_type': {'type': ['string', 'null']},
+ 'links': parameter_types.links
+ },
+ 'additionalProperties': False,
+ 'required': [
+ 'id', 'status', 'instance_uuid', 'source_node',
+ 'source_compute', 'dest_node', 'dest_compute',
+ 'dest_host', 'old_instance_type_id',
+ 'new_instance_type_id', 'created_at', 'updated_at',
+ 'migration_type'
+ ]
+ }
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['migrations']
+ }
+}
diff --git a/tempest/api/baremetal/__init__.py b/tempest/lib/api_schema/response/compute/v2_26/__init__.py
similarity index 100%
copy from tempest/api/baremetal/__init__.py
copy to tempest/lib/api_schema/response/compute/v2_26/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_26/servers.py b/tempest/lib/api_schema/response/compute/v2_26/servers.py
new file mode 100644
index 0000000..5c35eab
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_26/servers.py
@@ -0,0 +1,100 @@
+# Copyright 2016 IBM Corp.
+# Copyright 2017 AT&T Corp.
+#
+# 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_1 import servers as servers21
+from tempest.lib.api_schema.response.compute.v2_19 import servers as servers219
+
+# The 2.26 microversion changes the server GET and (detailed) LIST responses to
+# include the server 'tags' which is just a list of strings.
+
+tag_items = {
+ 'type': 'array',
+ 'maxItems': 50,
+ 'items': {
+ 'type': 'string',
+ 'pattern': '^[^,/]*$',
+ 'maxLength': 60
+ }
+}
+
+get_server = copy.deepcopy(servers219.get_server)
+get_server['response_body']['properties']['server'][
+ 'properties'].update({'tags': tag_items})
+get_server['response_body']['properties']['server'][
+ 'required'].append('tags')
+
+list_servers_detail = copy.deepcopy(servers219.list_servers_detail)
+list_servers_detail['response_body']['properties']['servers']['items'][
+ 'properties'].update({'tags': tag_items})
+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)
+
+list_tags = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'tags': tag_items,
+ },
+ 'additionalProperties': False,
+ 'required': ['tags']
+ }
+}
+
+update_all_tags = copy.deepcopy(list_tags)
+
+delete_all_tags = {'status_code': [204]}
+
+check_tag_existence = {'status_code': [204]}
+
+update_tag = {
+ 'status_code': [201, 204],
+ 'response_header': {
+ 'type': 'object',
+ 'properties': {
+ 'location': {
+ 'type': 'string'
+ }
+ },
+ 'required': ['location']
+ }
+}
+
+delete_tag = {'status_code': [204]}
diff --git a/tempest/services/identity/v3/__init__.py b/tempest/lib/api_schema/response/compute/v2_3/__init__.py
similarity index 100%
copy from tempest/services/identity/v3/__init__.py
copy to tempest/lib/api_schema/response/compute/v2_3/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_3/servers.py b/tempest/lib/api_schema/response/compute/v2_3/servers.py
new file mode 100644
index 0000000..f24103e
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_3/servers.py
@@ -0,0 +1,166 @@
+# Copyright 2014 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.
+import copy
+
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+from tempest.lib.api_schema.response.compute.v2_1 import servers
+
+# Compute microversion 2.3:
+# 1. New attributes in 'os-extended-volumes:volumes_attached' dict.
+# 'delete_on_termination'
+# 2. New attributes in 'server' dict.
+# 'OS-EXT-SRV-ATTR:reservation_id'
+# 'OS-EXT-SRV-ATTR:launch_index'
+# 'OS-EXT-SRV-ATTR:kernel_id'
+# 'OS-EXT-SRV-ATTR:ramdisk_id'
+# 'OS-EXT-SRV-ATTR:hostname'
+# 'OS-EXT-SRV-ATTR:root_device_name'
+# 'OS-EXT-SRV-ATTR:user_data'
+
+server_detail = {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'name': {'type': 'string'},
+ 'status': {'type': 'string'},
+ 'image': {'oneOf': [
+ {'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'links': parameter_types.links
+ },
+ 'additionalProperties': False,
+ 'required': ['id', 'links']},
+ {'type': ['string', 'null']}
+ ]},
+ 'flavor': {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'links': parameter_types.links
+ },
+ 'additionalProperties': False,
+ 'required': ['id', 'links']
+ },
+ 'fault': {
+ 'type': 'object',
+ 'properties': {
+ 'code': {'type': 'integer'},
+ 'created': {'type': 'string'},
+ 'message': {'type': 'string'},
+ 'details': {'type': 'string'},
+ },
+ 'additionalProperties': False,
+ # NOTE(gmann): 'details' is not necessary to be present
+ # in the 'fault'. So it is not defined as 'required'.
+ 'required': ['code', 'created', 'message']
+ },
+ 'user_id': {'type': 'string'},
+ 'tenant_id': {'type': 'string'},
+ 'created': {'type': 'string'},
+ 'updated': {'type': 'string'},
+ 'progress': {'type': 'integer'},
+ 'metadata': {'type': 'object'},
+ 'links': parameter_types.links,
+ 'addresses': parameter_types.addresses,
+ 'hostId': {'type': 'string'},
+ 'OS-DCF:diskConfig': {'type': 'string'},
+ 'accessIPv4': parameter_types.access_ip_v4,
+ 'accessIPv6': parameter_types.access_ip_v6,
+ 'key_name': {'type': ['string', 'null']},
+ 'security_groups': {'type': 'array'},
+ 'OS-SRV-USG:launched_at': {'type': ['string', 'null']},
+ 'OS-SRV-USG:terminated_at': {'type': ['string', 'null']},
+ 'OS-EXT-AZ:availability_zone': {'type': 'string'},
+ 'OS-EXT-STS:task_state': {'type': ['string', 'null']},
+ 'OS-EXT-STS:vm_state': {'type': 'string'},
+ 'OS-EXT-STS:power_state': parameter_types.power_state,
+ 'OS-EXT-SRV-ATTR:host': {'type': ['string', 'null']},
+ 'OS-EXT-SRV-ATTR:instance_name': {'type': 'string'},
+ 'OS-EXT-SRV-ATTR:hypervisor_hostname': {'type': ['string', 'null']},
+ 'config_drive': {'type': 'string'},
+ # NOTE(gmann): new attributes in version 2.3
+ 'os-extended-volumes:volumes_attached': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'delete_on_termination': {'type': 'boolean'}
+ },
+ 'additionalProperties': False,
+ },
+ },
+ 'OS-EXT-SRV-ATTR:reservation_id': {'type': ['string', 'null']},
+ 'OS-EXT-SRV-ATTR:launch_index': {'type': 'integer'},
+ 'OS-EXT-SRV-ATTR:kernel_id': {'type': ['string', 'null']},
+ 'OS-EXT-SRV-ATTR:ramdisk_id': {'type': ['string', 'null']},
+ 'OS-EXT-SRV-ATTR:hostname': {'type': 'string'},
+ 'OS-EXT-SRV-ATTR:root_device_name': {'type': ['string', 'null']},
+ 'OS-EXT-SRV-ATTR:user_data': {'type': ['string', 'null']},
+ },
+ 'additionalProperties': False,
+ # NOTE(gmann): 'progress' attribute is present in the response
+ # only when server's status is one of the progress statuses
+ # ("ACTIVE","BUILD", "REBUILD", "RESIZE","VERIFY_RESIZE")
+ # 'fault' attribute is present in the response
+ # only when server's status is one of the "ERROR", "DELETED".
+ # OS-DCF:diskConfig and accessIPv4/v6 are API
+ # extensions, and some environments return a response
+ # without these attributes.So these are not defined as 'required'.
+ 'required': ['id', 'name', 'status', 'image', 'flavor',
+ 'user_id', 'tenant_id', 'created', 'updated',
+ 'metadata', 'links', 'addresses', 'hostId']
+}
+
+server_detail['properties']['addresses']['patternProperties'][
+ '^[a-zA-Z0-9-_.]+$']['items']['properties'].update({
+ 'OS-EXT-IPS:type': {'type': 'string'},
+ 'OS-EXT-IPS-MAC:mac_addr': parameter_types.mac_address})
+# NOTE(gmann)dd: Update OS-EXT-IPS:type and OS-EXT-IPS-MAC:mac_addr
+# attributes in server address. Those are API extension,
+# and some environments return a response without
+# these attributes. So they are not 'required'.
+
+get_server = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'server': server_detail
+ },
+ 'additionalProperties': False,
+ 'required': ['server']
+ }
+}
+
+list_servers_detail = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'servers': {
+ 'type': 'array',
+ 'items': server_detail
+ },
+ 'servers_links': parameter_types.links
+ },
+ 'additionalProperties': False,
+ # NOTE(gmann): servers_links attribute is not necessary to be
+ # present always So it is not 'required'.
+ 'required': ['servers']
+ }
+}
+
+list_servers = copy.deepcopy(servers.list_servers)
diff --git a/tempest/api/baremetal/__init__.py b/tempest/lib/api_schema/response/compute/v2_45/__init__.py
similarity index 100%
copy from tempest/api/baremetal/__init__.py
copy to tempest/lib/api_schema/response/compute/v2_45/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_45/images.py b/tempest/lib/api_schema/response/compute/v2_45/images.py
new file mode 100644
index 0000000..8a48f36
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_45/images.py
@@ -0,0 +1,32 @@
+# 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.
+
+# The 2.45 microversion removes the "location" header and adds "image_id"
+# to the response body.
+create_image = {
+ 'status_code': [202],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'image_id': {'type': 'string'}
+ },
+ 'additionalProperties': False,
+ 'required': ['image_id']
+ }
+}
+
+# NOTE(mriedem): The compute proxy APIs for showing/listing and deleting
+# images were deprecated in microversion 2.35, and the compute proxy APIs for
+# working with image metadata were deprecated in microversion 2.39. Therefore,
+# client-side code shouldn't rely on those APIs in the compute images client
+# past those microversions and should instead use the Glance images client
+# directly.
diff --git a/tempest/api/baremetal/__init__.py b/tempest/lib/api_schema/response/compute/v2_47/__init__.py
similarity index 100%
copy from tempest/api/baremetal/__init__.py
copy to tempest/lib/api_schema/response/compute/v2_47/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_47/servers.py b/tempest/lib/api_schema/response/compute/v2_47/servers.py
new file mode 100644
index 0000000..935be70
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_47/servers.py
@@ -0,0 +1,55 @@
+# 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_26 import servers as servers226
+
+flavor = {
+ 'type': 'object',
+ 'properties': {
+ 'original_name': {'type': 'string'},
+ 'disk': {'type': 'integer'},
+ 'ephemeral': {'type': 'integer'},
+ 'ram': {'type': 'integer'},
+ 'swap': {'type': 'integer'},
+ 'vcpus': {'type': 'integer'},
+ 'extra_specs': {
+ 'type': 'object',
+ 'patternProperties': {
+ '^[a-zA-Z0-9_\-\. :]+$': {'type': 'string'}
+ }
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['original_name', 'disk', 'ephemeral', 'ram', 'swap', 'vcpus']
+}
+
+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/api/baremetal/__init__.py b/tempest/lib/api_schema/response/compute/v2_48/__init__.py
similarity index 100%
copy from tempest/api/baremetal/__init__.py
copy to tempest/lib/api_schema/response/compute/v2_48/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_48/servers.py b/tempest/lib/api_schema/response/compute/v2_48/servers.py
new file mode 100644
index 0000000..5904758
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_48/servers.py
@@ -0,0 +1,115 @@
+# Copyright 2017 Mirantis Inc.
+#
+# 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_1 import parameter_types
+from tempest.lib.api_schema.response.compute.v2_47 import servers as servers247
+
+
+show_server_diagnostics = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'state': {
+ 'type': 'string', 'enum': [
+ 'pending', 'running', 'paused', 'shutdown', 'crashed',
+ 'suspended']
+ },
+ 'driver': {
+ 'type': 'string', 'enum': [
+ 'libvirt', 'xenapi', 'vmwareapi', 'ironic', 'hyperv']
+ },
+ 'hypervisor': {'type': ['string', 'null']},
+ 'hypervisor_os': {'type': ['string', 'null']},
+ 'uptime': {'type': ['integer', 'null']},
+ 'config_drive': {'type': 'boolean'},
+ 'num_cpus': {'type': 'integer'},
+ 'num_nics': {'type': 'integer'},
+ 'num_disks': {'type': 'integer'},
+ 'memory_details': {
+ 'type': 'object',
+ 'properties': {
+ 'maximum': {'type': ['integer', 'null']},
+ 'used': {'type': ['integer', 'null']}
+ },
+ 'additionalProperties': False,
+ 'required': ['maximum', 'used']
+ },
+ 'cpu_details': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': ['integer', 'null']},
+ 'time': {'type': ['integer', 'null']},
+ 'utilisation': {'type': ['integer', 'null']}
+ },
+ 'additionalProperties': False,
+ 'required': ['id', 'time', 'utilisation']
+ }
+ },
+ 'nic_details': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'mac_address': {'oneOf': [parameter_types.mac_address,
+ {'type': 'null'}]},
+ 'rx_octets': {'type': ['integer', 'null']},
+ 'rx_errors': {'type': ['integer', 'null']},
+ 'rx_drop': {'type': ['integer', 'null']},
+ 'rx_packets': {'type': ['integer', 'null']},
+ 'rx_rate': {'type': ['integer', 'null']},
+ 'tx_octets': {'type': ['integer', 'null']},
+ 'tx_errors': {'type': ['integer', 'null']},
+ 'tx_drop': {'type': ['integer', 'null']},
+ 'tx_packets': {'type': ['integer', 'null']},
+ 'tx_rate': {'type': ['integer', 'null']}
+ },
+ 'additionalProperties': False,
+ 'required': ['mac_address', 'rx_octets', 'rx_errors',
+ 'rx_drop',
+ 'rx_packets', 'rx_rate', 'tx_octets',
+ 'tx_errors',
+ 'tx_drop', 'tx_packets', 'tx_rate']
+ }
+ },
+ 'disk_details': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'read_bytes': {'type': ['integer', 'null']},
+ 'read_requests': {'type': ['integer', 'null']},
+ 'write_bytes': {'type': ['integer', 'null']},
+ 'write_requests': {'type': ['integer', 'null']},
+ 'errors_count': {'type': ['integer', 'null']}
+ },
+ 'additionalProperties': False,
+ 'required': ['read_bytes', 'read_requests', 'write_bytes',
+ 'write_requests', 'errors_count']
+ }
+ }
+ },
+ 'additionalProperties': False,
+ 'required': [
+ 'state', 'driver', 'hypervisor', 'hypervisor_os', 'uptime',
+ 'config_drive', 'num_cpus', 'num_nics', 'num_disks',
+ 'memory_details', 'cpu_details', 'nic_details', 'disk_details'],
+ }
+}
+
+get_server = copy.deepcopy(servers247.get_server)
diff --git a/tempest/api/baremetal/__init__.py b/tempest/lib/api_schema/response/compute/v2_54/__init__.py
similarity index 100%
copy from tempest/api/baremetal/__init__.py
copy to 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/api/baremetal/__init__.py b/tempest/lib/api_schema/response/compute/v2_55/__init__.py
similarity index 100%
copy from tempest/api/baremetal/__init__.py
copy to 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/api/baremetal/__init__.py b/tempest/lib/api_schema/response/compute/v2_57/__init__.py
similarity index 100%
copy from tempest/api/baremetal/__init__.py
copy to tempest/lib/api_schema/response/compute/v2_57/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_57/servers.py b/tempest/lib/api_schema/response/compute/v2_57/servers.py
new file mode 100644
index 0000000..ed1ca7d
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_57/servers.py
@@ -0,0 +1,53 @@
+# 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_54 import servers as servers254
+# ****** Schemas changed in microversion 2.57 *****************
+
+# Note(gmann): This is schema for microversion 2.57 which includes the
+# 'user_data' in the Response body of the following APIs:
+# - ``POST '/servers/{server_id}/action (rebuild)``
+
+user_data = {
+ 'oneOf': [
+ {
+ 'type': 'string',
+ 'format': 'base64',
+ 'maxLength': 65535
+ },
+ {'type': 'null'}
+ ]
+}
+
+rebuild_server = copy.deepcopy(servers254.rebuild_server)
+rebuild_server['response_body']['properties']['server'][
+ 'properties'].update({'user_data': user_data})
+rebuild_server['response_body']['properties']['server'][
+ 'required'].append('user_data')
+
+rebuild_server_with_admin_pass = copy.deepcopy(
+ servers254.rebuild_server_with_admin_pass)
+rebuild_server_with_admin_pass['response_body']['properties']['server'][
+ 'properties'].update({'user_data': user_data})
+rebuild_server_with_admin_pass['response_body']['properties']['server'][
+ 'required'].append('user_data')
+
+# ****** Schemas unchanged in microversion 2.57 since microversion 2.54 ***
+
+# NOTE(gmann): Below are the unchanged schema in this microversion. We need
+# to keeo 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(servers254.get_server)
+list_servers_detail = copy.deepcopy(servers254.list_servers_detail)
+update_server = copy.deepcopy(servers254.update_server)
diff --git a/tempest/api/baremetal/__init__.py b/tempest/lib/api_schema/response/compute/v2_6/__init__.py
similarity index 100%
copy from tempest/api/baremetal/__init__.py
copy to tempest/lib/api_schema/response/compute/v2_6/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_6/servers.py b/tempest/lib/api_schema/response/compute/v2_6/servers.py
new file mode 100644
index 0000000..29b3e86
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_6/servers.py
@@ -0,0 +1,48 @@
+# Copyright 2016 IBM Corp.
+#
+# 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_3 import servers
+
+list_servers = copy.deepcopy(servers.list_servers)
+get_server = copy.deepcopy(servers.get_server)
+list_servers_detail = copy.deepcopy(servers.list_servers_detail)
+
+# NOTE: The consolidated remote console API got introduced with v2.6
+# with bp/consolidate-console-api. See Nova commit 578bafeda
+get_remote_consoles = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'remote_console': {
+ 'type': 'object',
+ 'properties': {
+ 'protocol': {'enum': ['vnc', 'rdp', 'serial', 'spice']},
+ 'type': {'enum': ['novnc', 'xpvnc', 'rdp-html5',
+ 'spice-html5', 'serial']},
+ 'url': {
+ 'type': 'string',
+ 'format': 'uri'
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['protocol', 'type', 'url']
+ }
+ },
+ 'additionalProperties': False,
+ 'required': ['remote_console']
+ }
+}
diff --git a/tempest/services/identity/v3/__init__.py b/tempest/lib/api_schema/response/compute/v2_63/__init__.py
similarity index 100%
copy from tempest/services/identity/v3/__init__.py
copy to tempest/lib/api_schema/response/compute/v2_63/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_63/servers.py b/tempest/lib/api_schema/response/compute/v2_63/servers.py
new file mode 100644
index 0000000..5cdaf54
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_63/servers.py
@@ -0,0 +1,65 @@
+# 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_26 import servers as servers226
+from tempest.lib.api_schema.response.compute.v2_54 import servers as servers254
+from tempest.lib.api_schema.response.compute.v2_57 import servers as servers257
+
+# Nova microversion 2.63 adds 'trusted_image_certificates' (a list of
+# certificate IDs) to the server rebuild and servers details responses.
+
+
+trusted_certs = {
+ 'type': ['array', 'null'],
+ 'minItems': 1,
+ 'maxItems': 50,
+ 'uniqueItems': True,
+ 'items': {
+ 'type': 'string',
+ 'minLength': 1
+ }
+}
+# list response schema wasn't changed for v2.63 so use v2.26
+list_servers = copy.deepcopy(servers226.list_servers)
+
+list_servers_detail = copy.deepcopy(servers254.list_servers_detail)
+list_servers_detail['response_body']['properties']['servers']['items'][
+ 'properties'].update({'trusted_image_certificates': trusted_certs})
+list_servers_detail['response_body']['properties']['servers']['items'][
+ 'required'].append('trusted_image_certificates')
+
+rebuild_server = copy.deepcopy(servers257.rebuild_server)
+rebuild_server['response_body']['properties']['server'][
+ 'properties'].update({'trusted_image_certificates': trusted_certs})
+rebuild_server['response_body']['properties']['server'][
+ 'required'].append('trusted_image_certificates')
+
+rebuild_server_with_admin_pass = copy.deepcopy(
+ servers257.rebuild_server_with_admin_pass)
+rebuild_server_with_admin_pass['response_body']['properties']['server'][
+ 'properties'].update({'trusted_image_certificates': trusted_certs})
+rebuild_server_with_admin_pass['response_body']['properties']['server'][
+ 'required'].append('trusted_image_certificates')
+
+update_server = copy.deepcopy(servers254.update_server)
+update_server['response_body']['properties']['server'][
+ 'properties'].update({'trusted_image_certificates': trusted_certs})
+update_server['response_body']['properties']['server'][
+ 'required'].append('trusted_image_certificates')
+
+get_server = copy.deepcopy(servers254.get_server)
+get_server['response_body']['properties']['server'][
+ 'properties'].update({'trusted_image_certificates': trusted_certs})
+get_server['response_body']['properties']['server'][
+ 'required'].append('trusted_image_certificates')
diff --git a/tempest/api/baremetal/__init__.py b/tempest/lib/api_schema/response/compute/v2_9/__init__.py
similarity index 100%
copy from tempest/api/baremetal/__init__.py
copy to tempest/lib/api_schema/response/compute/v2_9/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_9/servers.py b/tempest/lib/api_schema/response/compute/v2_9/servers.py
new file mode 100644
index 0000000..7df02d5
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_9/servers.py
@@ -0,0 +1,51 @@
+# Copyright 2016 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.
+
+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)
+
+get_server = copy.deepcopy(servers.get_server)
+get_server['response_body']['properties']['server'][
+ 'properties'].update({'locked': {'type': 'boolean'}})
+get_server['response_body']['properties']['server'][
+ 'required'].append('locked')
+
+list_servers_detail = copy.deepcopy(servers.list_servers_detail)
+list_servers_detail['response_body']['properties']['servers']['items'][
+ '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/services/volume/__init__.py b/tempest/lib/api_schema/response/volume/__init__.py
similarity index 100%
copy from tempest/services/volume/__init__.py
copy to tempest/lib/api_schema/response/volume/__init__.py
diff --git a/tempest/lib/api_schema/response/volume/versions.py b/tempest/lib/api_schema/response/volume/versions.py
new file mode 100644
index 0000000..2391a8c
--- /dev/null
+++ b/tempest/lib/api_schema/response/volume/versions.py
@@ -0,0 +1,60 @@
+# Copyright 2015 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.
+
+
+list_versions = {
+ 'status_code': [300],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'versions': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'status': {'type': 'string'},
+ 'updated': {'type': 'string'},
+ 'id': {'type': 'string'},
+ 'links': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'href': {'type': 'string',
+ 'format': 'uri'},
+ 'rel': {'type': 'string'},
+ 'type': {'type': 'string'},
+ },
+ 'required': ['href', 'rel']
+ }
+ },
+ 'min_version': {'type': 'string'},
+ 'version': {'type': 'string'},
+ 'media-types': {
+ 'type': 'array',
+ 'properties': {
+ 'base': {'type': 'string'},
+ 'type': {'type': 'string'}
+ },
+ 'required': ['base', 'type']
+ }
+ },
+ 'required': ['status', 'updated', 'id', 'links',
+ 'min_version', 'version', 'media-types']
+ }
+ }
+ },
+ 'required': ['versions'],
+ }
+}
diff --git a/tempest/lib/auth.py b/tempest/lib/auth.py
new file mode 100644
index 0000000..2dd9d00
--- /dev/null
+++ b/tempest/lib/auth.py
@@ -0,0 +1,854 @@
+# Copyright 2014 Hewlett-Packard Development Company, L.P.
+# Copyright 2016 Rackspace Inc.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import abc
+import copy
+import datetime
+import re
+
+from oslo_log import log as logging
+import six
+from six.moves.urllib import parse as urlparse
+
+from tempest.lib import exceptions
+from tempest.lib.services.identity.v2 import token_client as json_v2id
+from tempest.lib.services.identity.v3 import token_client as json_v3id
+
+ISO8601_FLOAT_SECONDS = '%Y-%m-%dT%H:%M:%S.%fZ'
+ISO8601_INT_SECONDS = '%Y-%m-%dT%H:%M:%SZ'
+LOG = logging.getLogger(__name__)
+
+
+def replace_version(url, new_version):
+ parts = urlparse.urlparse(url)
+ version_path = '/%s' % new_version
+ path, subs = re.subn(r'(^|/)+v\d+(?:\.\d+)?',
+ version_path,
+ parts.path,
+ count=1)
+ if not subs:
+ path = '%s%s' % (parts.path.rstrip('/'), version_path)
+ url = urlparse.urlunparse((parts.scheme,
+ parts.netloc,
+ path,
+ parts.params,
+ parts.query,
+ parts.fragment))
+ return url
+
+
+def apply_url_filters(url, filters):
+ if filters.get('api_version', None) is not None:
+ url = replace_version(url, filters['api_version'])
+ parts = urlparse.urlparse(url)
+ if filters.get('skip_path', None) is not None and parts.path != '':
+ url = urlparse.urlunparse((parts.scheme,
+ parts.netloc,
+ '/',
+ parts.params,
+ parts.query,
+ parts.fragment))
+
+ return url
+
+
+@six.add_metaclass(abc.ABCMeta)
+class AuthProvider(object):
+ """Provide authentication"""
+
+ SCOPES = set(['project'])
+
+ def __init__(self, credentials, scope='project'):
+ """Auth provider __init__
+
+ :param credentials: credentials for authentication
+ :param scope: the default scope to be used by the credential providers
+ when requesting a token. Valid values depend on the
+ AuthProvider class implementation, and are defined in
+ the set SCOPES. Default value is 'project'.
+ """
+ if self.check_credentials(credentials):
+ self.credentials = credentials
+ else:
+ if isinstance(credentials, Credentials):
+ password = credentials.get('password')
+ message = "Credentials are: " + str(credentials)
+ if password is None:
+ message += " Password is not defined."
+ else:
+ message += " Password is defined."
+ raise exceptions.InvalidCredentials(message)
+ else:
+ raise TypeError("credentials object is of type %s, which is"
+ " not a valid Credentials object type." %
+ credentials.__class__.__name__)
+ self._scope = None
+ self.scope = scope
+ self.cache = None
+ self.alt_auth_data = None
+ self.alt_part = None
+
+ def __str__(self):
+ return "Creds :{creds}, cached auth data: {cache}".format(
+ creds=self.credentials, cache=self.cache)
+
+ @abc.abstractmethod
+ def _decorate_request(self, filters, method, url, headers=None, body=None,
+ auth_data=None):
+ """Decorate request with authentication data"""
+ return
+
+ @abc.abstractmethod
+ def _get_auth(self):
+ return
+
+ @abc.abstractmethod
+ def _fill_credentials(self, auth_data_body):
+ return
+
+ def fill_credentials(self):
+ """Fill credentials object with data from auth"""
+ auth_data = self.get_auth()
+ self._fill_credentials(auth_data[1])
+ return self.credentials
+
+ @classmethod
+ def check_credentials(cls, credentials):
+ """Verify credentials are valid."""
+ return isinstance(credentials, Credentials) and credentials.is_valid()
+
+ @property
+ def auth_data(self):
+ """Auth data for set scope"""
+ return self.get_auth()
+
+ @property
+ def scope(self):
+ """Scope used in auth requests"""
+ return self._scope
+
+ @auth_data.deleter
+ def auth_data(self):
+ self.clear_auth()
+
+ def get_auth(self):
+ """Returns auth from cache if available, else auth first"""
+ if self.cache is None or self.is_expired(self.cache):
+ self.set_auth()
+ return self.cache
+
+ def set_auth(self):
+ """Forces setting auth.
+
+ Forces setting auth, ignores cache if it exists.
+ Refills credentials.
+ """
+ self.cache = self._get_auth()
+ self._fill_credentials(self.cache[1])
+
+ def clear_auth(self):
+ """Clear access cache
+
+ Can be called to clear the access cache so that next request
+ will fetch a new token and base_url.
+ """
+ self.cache = None
+ self.credentials.reset()
+
+ @abc.abstractmethod
+ def is_expired(self, auth_data):
+ return
+
+ def auth_request(self, method, url, headers=None, body=None, filters=None):
+ """Obtains auth data and decorates a request with that.
+
+ :param method: HTTP method of the request
+ :param url: relative URL of the request (path)
+ :param headers: HTTP headers of the request
+ :param body: HTTP body in case of POST / PUT
+ :param filters: select a base URL out of the catalog
+ :return: a Tuple (url, headers, body)
+ """
+ orig_req = dict(url=url, headers=headers, body=body)
+
+ auth_url, auth_headers, auth_body = self._decorate_request(
+ filters, method, url, headers, body)
+ auth_req = dict(url=auth_url, headers=auth_headers, body=auth_body)
+
+ # Overwrite part if the request if it has been requested
+ if self.alt_part is not None:
+ if self.alt_auth_data is not None:
+ alt_url, alt_headers, alt_body = self._decorate_request(
+ filters, method, url, headers, body,
+ auth_data=self.alt_auth_data)
+ alt_auth_req = dict(url=alt_url, headers=alt_headers,
+ body=alt_body)
+ if auth_req[self.alt_part] == alt_auth_req[self.alt_part]:
+ raise exceptions.BadAltAuth(part=self.alt_part)
+ auth_req[self.alt_part] = alt_auth_req[self.alt_part]
+
+ else:
+ # If the requested part is not affected by auth, we are
+ # not altering auth as expected, raise an exception
+ if auth_req[self.alt_part] == orig_req[self.alt_part]:
+ raise exceptions.BadAltAuth(part=self.alt_part)
+ # If alt auth data is None, skip auth in the requested part
+ auth_req[self.alt_part] = orig_req[self.alt_part]
+
+ # Next auth request will be normal, unless otherwise requested
+ self.reset_alt_auth_data()
+
+ return auth_req['url'], auth_req['headers'], auth_req['body']
+
+ def reset_alt_auth_data(self):
+ """Configure auth provider to provide valid authentication data"""
+ self.alt_part = None
+ self.alt_auth_data = None
+
+ def set_alt_auth_data(self, request_part, auth_data):
+ """Alternate auth data on next request
+
+ Configure auth provider to provide alt authentication data
+ on a part of the *next* auth_request. If credentials are None,
+ set invalid data.
+
+ :param request_part: request part to contain invalid auth: url,
+ headers, body
+ :param auth_data: alternative auth_data from which to get the
+ invalid data to be injected
+ """
+ self.alt_part = request_part
+ self.alt_auth_data = auth_data
+
+ @abc.abstractmethod
+ def base_url(self, filters, auth_data=None):
+ """Extracts the base_url based on provided filters"""
+ return
+
+ @scope.setter
+ def scope(self, value):
+ """Set the scope to be used in token requests
+
+ :param scope: scope to be used. If the scope is different, clear caches
+ """
+ if value not in self.SCOPES:
+ raise exceptions.InvalidScope(
+ scope=value, auth_provider=self.__class__.__name__)
+ if value != self.scope:
+ self.clear_auth()
+ self._scope = value
+
+
+class KeystoneAuthProvider(AuthProvider):
+
+ EXPIRY_DATE_FORMATS = (ISO8601_FLOAT_SECONDS, ISO8601_INT_SECONDS)
+
+ token_expiry_threshold = datetime.timedelta(seconds=60)
+
+ def __init__(self, credentials, auth_url,
+ disable_ssl_certificate_validation=None,
+ ca_certs=None, trace_requests=None, scope='project',
+ http_timeout=None, proxy_url=None):
+ super(KeystoneAuthProvider, self).__init__(credentials, scope)
+ self.dscv = disable_ssl_certificate_validation
+ self.ca_certs = ca_certs
+ self.trace_requests = trace_requests
+ self.http_timeout = http_timeout
+ self.proxy_url = proxy_url
+ self.auth_url = auth_url
+ self.auth_client = self._auth_client(auth_url)
+
+ def _decorate_request(self, filters, method, url, headers=None, body=None,
+ auth_data=None):
+ if auth_data is None:
+ auth_data = self.get_auth()
+ token, _ = auth_data
+ base_url = self.base_url(filters=filters, auth_data=auth_data)
+ # build authenticated request
+ # returns new request, it does not touch the original values
+ _headers = copy.deepcopy(headers) if headers is not None else {}
+ _headers['X-Auth-Token'] = str(token)
+ if url is None or url == "":
+ _url = base_url
+ else:
+ # Join base URL and url, and remove multiple contiguous slashes
+ _url = "/".join([base_url, url])
+ parts = [x for x in urlparse.urlparse(_url)]
+ parts[2] = re.sub("/{2,}", "/", parts[2])
+ _url = urlparse.urlunparse(parts)
+ # no change to method or body
+ return str(_url), _headers, body
+
+ @abc.abstractmethod
+ def _auth_client(self):
+ return
+
+ @abc.abstractmethod
+ def _auth_params(self):
+ """Auth parameters to be passed to the token request
+
+ By default all fields available in Credentials are passed to the
+ token request. Scope may affect this.
+ """
+ return
+
+ def _get_auth(self):
+ # Bypasses the cache
+ auth_func = getattr(self.auth_client, 'get_token')
+ auth_params = self._auth_params()
+
+ # returns token, auth_data
+ token, auth_data = auth_func(**auth_params)
+ return token, auth_data
+
+ def _parse_expiry_time(self, expiry_string):
+ expiry = None
+ for date_format in self.EXPIRY_DATE_FORMATS:
+ try:
+ expiry = datetime.datetime.strptime(
+ expiry_string, date_format)
+ except ValueError:
+ pass
+ if expiry is None:
+ raise ValueError(
+ "time data '{data}' does not match any of the"
+ "expected formats: {formats}".format(
+ data=expiry_string, formats=self.EXPIRY_DATE_FORMATS))
+ return expiry
+
+ def get_token(self):
+ return self.get_auth()[0]
+
+
+class KeystoneV2AuthProvider(KeystoneAuthProvider):
+ """Provides authentication based on the Identity V2 API
+
+ The Keystone Identity V2 API defines both unscoped and project scoped
+ tokens. This auth provider only implements 'project'.
+ """
+
+ SCOPES = set(['project'])
+
+ def _auth_client(self, auth_url):
+ return json_v2id.TokenClient(
+ auth_url, disable_ssl_certificate_validation=self.dscv,
+ ca_certs=self.ca_certs, trace_requests=self.trace_requests,
+ http_timeout=self.http_timeout, proxy_url=self.proxy_url)
+
+ def _auth_params(self):
+ """Auth parameters to be passed to the token request
+
+ All fields available in Credentials are passed to the token request.
+ """
+ return dict(
+ user=self.credentials.username,
+ password=self.credentials.password,
+ tenant=self.credentials.tenant_name,
+ auth_data=True)
+
+ def _fill_credentials(self, auth_data_body):
+ tenant = auth_data_body['token']['tenant']
+ user = auth_data_body['user']
+ if self.credentials.tenant_name is None:
+ self.credentials.tenant_name = tenant['name']
+ if self.credentials.tenant_id is None:
+ self.credentials.tenant_id = tenant['id']
+ if self.credentials.username is None:
+ self.credentials.username = user['name']
+ if self.credentials.user_id is None:
+ self.credentials.user_id = user['id']
+
+ def base_url(self, filters, auth_data=None):
+ """Base URL from catalog
+
+ :param filters: Used to filter results
+
+ Filters can be:
+
+ - service: service type name such as compute, image, etc.
+ - region: service region name
+ - name: service name, only if service exists
+ - endpoint_type: type of endpoint such as
+ adminURL, publicURL, internalURL
+ - api_version: the version of api used to replace catalog version
+ - skip_path: skips the suffix path of the url and uses base URL
+
+ :rtype: string
+ :return: url with filters applied
+ """
+ if auth_data is None:
+ auth_data = self.get_auth()
+ token, _auth_data = auth_data
+ service = filters.get('service')
+ region = filters.get('region')
+ name = filters.get('name')
+ endpoint_type = filters.get('endpoint_type', 'publicURL')
+
+ if service is None:
+ raise exceptions.EndpointNotFound("No service provided")
+
+ _base_url = None
+ for ep in _auth_data['serviceCatalog']:
+ if ep["type"] == service:
+ if name is not None and ep["name"] != name:
+ continue
+ for _ep in ep['endpoints']:
+ if region is not None and _ep['region'] == region:
+ _base_url = _ep.get(endpoint_type)
+ if not _base_url:
+ # No region or name matching, use the first
+ _base_url = ep['endpoints'][0].get(endpoint_type)
+ break
+ if _base_url is None:
+ raise exceptions.EndpointNotFound(
+ "service: %s, region: %s, endpoint_type: %s, name: %s" %
+ (service, region, endpoint_type, name))
+ return apply_url_filters(_base_url, filters)
+
+ def is_expired(self, auth_data):
+ _, access = auth_data
+ expiry = self._parse_expiry_time(access['token']['expires'])
+ return (expiry - self.token_expiry_threshold <=
+ datetime.datetime.utcnow())
+
+
+class KeystoneV3AuthProvider(KeystoneAuthProvider):
+ """Provides authentication based on the Identity V3 API"""
+
+ SCOPES = set(['project', 'domain', 'unscoped', None])
+
+ def _auth_client(self, auth_url):
+ return json_v3id.V3TokenClient(
+ auth_url, disable_ssl_certificate_validation=self.dscv,
+ ca_certs=self.ca_certs, trace_requests=self.trace_requests,
+ http_timeout=self.http_timeout, proxy_url=self.proxy_url)
+
+ def _auth_params(self):
+ """Auth parameters to be passed to the token request
+
+ Fields available in Credentials are passed to the token request,
+ depending on the value of scope. Valid values for scope are: "project",
+ "domain". Any other string (e.g. "unscoped") or None will lead to an
+ unscoped token request.
+ """
+
+ auth_params = dict(
+ user_id=self.credentials.user_id,
+ username=self.credentials.username,
+ user_domain_id=self.credentials.user_domain_id,
+ user_domain_name=self.credentials.user_domain_name,
+ password=self.credentials.password,
+ auth_data=True)
+
+ if self.scope == 'project':
+ auth_params.update(
+ project_domain_id=self.credentials.project_domain_id,
+ project_domain_name=self.credentials.project_domain_name,
+ project_id=self.credentials.project_id,
+ project_name=self.credentials.project_name)
+
+ if self.scope == 'domain':
+ auth_params.update(
+ domain_id=self.credentials.domain_id,
+ domain_name=self.credentials.domain_name)
+
+ return auth_params
+
+ def _fill_credentials(self, auth_data_body):
+ # project or domain, depending on the scope
+ project = auth_data_body.get('project', None)
+ domain = auth_data_body.get('domain', None)
+ # user is always there
+ user = auth_data_body['user']
+ # Set project fields
+ if project is not None:
+ if self.credentials.project_name is None:
+ self.credentials.project_name = project['name']
+ if self.credentials.project_id is None:
+ self.credentials.project_id = project['id']
+ if self.credentials.project_domain_id is None:
+ self.credentials.project_domain_id = project['domain']['id']
+ if self.credentials.project_domain_name is None:
+ self.credentials.project_domain_name = (
+ project['domain']['name'])
+ # Set domain fields
+ if domain is not None:
+ if self.credentials.domain_id is None:
+ self.credentials.domain_id = domain['id']
+ if self.credentials.domain_name is None:
+ self.credentials.domain_name = domain['name']
+ # Set user fields
+ if self.credentials.username is None:
+ self.credentials.username = user['name']
+ if self.credentials.user_id is None:
+ self.credentials.user_id = user['id']
+ if self.credentials.user_domain_id is None:
+ self.credentials.user_domain_id = user['domain']['id']
+ if self.credentials.user_domain_name is None:
+ self.credentials.user_domain_name = user['domain']['name']
+
+ def base_url(self, filters, auth_data=None):
+ """Base URL from catalog
+
+ If scope is not 'project', it may be that there is not catalog in
+ the auth_data. In such case, as long as the requested service is
+ 'identity', we can use the original auth URL to build the base_url.
+
+ :param filters: Used to filter results
+
+ Filters can be:
+
+ - service: service type name such as compute, image, etc.
+ - region: service region name
+ - name: service name, only if service exists
+ - endpoint_type: type of endpoint such as
+ adminURL, publicURL, internalURL
+ - api_version: the version of api used to replace catalog version
+ - skip_path: skips the suffix path of the url and uses base URL
+
+ :rtype: string
+ :return: url with filters applied
+ """
+ if auth_data is None:
+ auth_data = self.get_auth()
+ token, _auth_data = auth_data
+ service = filters.get('service')
+ region = filters.get('region')
+ name = filters.get('name')
+ endpoint_type = filters.get('endpoint_type', 'public')
+
+ if service is None:
+ raise exceptions.EndpointNotFound("No service provided")
+
+ if 'URL' in endpoint_type:
+ endpoint_type = endpoint_type.replace('URL', '')
+ _base_url = None
+ catalog = _auth_data.get('catalog', [])
+
+ # Select entries with matching service type
+ service_catalog = [ep for ep in catalog if ep['type'] == service]
+ if service_catalog:
+ if name is not None:
+ service_catalog = (
+ [ep for ep in service_catalog if ep['name'] == name])
+ if service_catalog:
+ service_catalog = service_catalog[0]['endpoints']
+ else:
+ raise exceptions.EndpointNotFound(name)
+ else:
+ service_catalog = service_catalog[0]['endpoints']
+ else:
+ if not catalog and service == 'identity':
+ # NOTE(andreaf) If there's no catalog at all and the service
+ # is identity, it's a valid use case. Having a non-empty
+ # catalog with no identity in it is not valid instead.
+ msg = ('Got an empty catalog. Scope: %s. '
+ 'Falling back to configured URL for %s: %s')
+ LOG.debug(msg, self.scope, service, self.auth_url)
+ return apply_url_filters(self.auth_url, filters)
+ else:
+ # No matching service
+ msg = ('No matching service found in the catalog.\n'
+ 'Scope: %s, Credentials: %s\n'
+ 'Auth data: %s\n'
+ 'Service: %s, Region: %s, endpoint_type: %s\n'
+ 'Catalog: %s')
+ raise exceptions.EndpointNotFound(msg % (
+ self.scope, self.credentials, _auth_data, service, region,
+ endpoint_type, catalog))
+ # Filter by endpoint type (interface)
+ filtered_catalog = [ep for ep in service_catalog if
+ ep['interface'] == endpoint_type]
+ if not filtered_catalog:
+ # No matching type, keep all and try matching by region at least
+ filtered_catalog = service_catalog
+ # Filter by region
+ filtered_catalog = [ep for ep in filtered_catalog if
+ ep['region'] == region]
+ if not filtered_catalog:
+ # No matching region (or name), take the first endpoint
+ filtered_catalog = [service_catalog[0]]
+ # There should be only one match. If not take the first.
+ _base_url = filtered_catalog[0].get('url', None)
+ if _base_url is None:
+ raise exceptions.EndpointNotFound(service)
+ return apply_url_filters(_base_url, filters)
+
+ def is_expired(self, auth_data):
+ _, access = auth_data
+ expiry = self._parse_expiry_time(access['expires_at'])
+ return (expiry - self.token_expiry_threshold <=
+ datetime.datetime.utcnow())
+
+
+def is_identity_version_supported(identity_version):
+ return identity_version in IDENTITY_VERSION
+
+
+def get_credentials(auth_url, fill_in=True, identity_version='v2',
+ disable_ssl_certificate_validation=None, ca_certs=None,
+ trace_requests=None, http_timeout=None, proxy_url=None,
+ **kwargs):
+ """Builds a credentials object based on the configured auth_version
+
+ :param auth_url (string): Full URI of the OpenStack Identity API(Keystone)
+ which is used to fetch the token from Identity service.
+ :param fill_in (boolean): obtain a token and fill in all credential
+ details provided by the identity service. When fill_in is not
+ specified, credentials are not validated. Validation can be invoked
+ by invoking ``is_valid()``
+ :param identity_version (string): identity API version is used to
+ select the matching auth provider and credentials class
+ :param disable_ssl_certificate_validation: whether to enforce SSL
+ certificate validation in SSL API requests to the auth system
+ :param ca_certs: CA certificate bundle for validation of certificates
+ in SSL API requests to the auth system
+ :param trace_requests: trace in log API requests to the auth system
+ :param http_timeout: timeout in seconds to wait for the http request to
+ return
+ :param proxy_url: URL of HTTP(s) proxy used when fill_in is True
+ :param kwargs (dict): Dict of credential key/value pairs
+
+ Examples:
+
+ Returns credentials from the provided parameters:
+ >>> get_credentials(username='foo', password='bar')
+
+ Returns credentials including IDs:
+ >>> get_credentials(username='foo', password='bar', fill_in=True)
+ """
+ if not is_identity_version_supported(identity_version):
+ raise exceptions.InvalidIdentityVersion(
+ identity_version=identity_version)
+
+ credential_class, auth_provider_class = IDENTITY_VERSION.get(
+ identity_version)
+
+ creds = credential_class(**kwargs)
+ # Fill in the credentials fields that were not specified
+ if fill_in:
+ dscv = disable_ssl_certificate_validation
+ auth_provider = auth_provider_class(
+ creds, auth_url, disable_ssl_certificate_validation=dscv,
+ ca_certs=ca_certs, trace_requests=trace_requests,
+ http_timeout=http_timeout, proxy_url=proxy_url)
+ creds = auth_provider.fill_credentials()
+ return creds
+
+
+class Credentials(object):
+ """Set of credentials for accessing OpenStack services
+
+ ATTRIBUTES: list of valid class attributes representing credentials.
+ """
+
+ ATTRIBUTES = []
+ COLLISIONS = []
+
+ def __init__(self, **kwargs):
+ """Enforce the available attributes at init time (only).
+
+ Additional attributes can still be set afterwards if tests need
+ to do so.
+ """
+ self._initial = kwargs
+ self._apply_credentials(kwargs)
+
+ def _apply_credentials(self, attr):
+ for (key1, key2) in self.COLLISIONS:
+ val1 = attr.get(key1)
+ val2 = attr.get(key2)
+ if val1 and val2 and val1 != val2:
+ msg = ('Cannot have conflicting values for %s and %s' %
+ (key1, key2))
+ raise exceptions.InvalidCredentials(msg)
+ for key in attr:
+ if key in self.ATTRIBUTES:
+ setattr(self, key, attr[key])
+ else:
+ msg = '%s is not a valid attr for %s' % (key, self.__class__)
+ raise exceptions.InvalidCredentials(msg)
+
+ def __str__(self):
+ """Represent only attributes included in self.ATTRIBUTES"""
+ attrs = [attr for attr in self.ATTRIBUTES if attr is not 'password']
+ _repr = dict((k, getattr(self, k)) for k in attrs)
+ return str(_repr)
+
+ def __eq__(self, other):
+ """Credentials are equal if attributes in self.ATTRIBUTES are equal"""
+ return str(self) == str(other)
+
+ def __ne__(self, other):
+ """Contrary to the __eq__"""
+ return not self.__eq__(other)
+
+ def __getattr__(self, key):
+ # If an attribute is set, __getattr__ is not invoked
+ # If an attribute is not set, and it is a known one, return None
+ if key in self.ATTRIBUTES:
+ return None
+ else:
+ raise AttributeError
+
+ def __delitem__(self, key):
+ # For backwards compatibility, support dict behaviour
+ if key in self.ATTRIBUTES:
+ delattr(self, key)
+ else:
+ raise AttributeError
+
+ def get(self, item, default=None):
+ # In this patch act as dict for backward compatibility
+ try:
+ return getattr(self, item)
+ except AttributeError:
+ return default
+
+ def get_init_attributes(self):
+ return self._initial.keys()
+
+ def is_valid(self):
+ raise NotImplementedError
+
+ def reset(self):
+ # First delete all known attributes
+ for key in self.ATTRIBUTES:
+ if getattr(self, key) is not None:
+ delattr(self, key)
+ # Then re-apply initial setup
+ self._apply_credentials(self._initial)
+
+
+class KeystoneV2Credentials(Credentials):
+
+ ATTRIBUTES = ['username', 'password', 'tenant_name', 'user_id',
+ 'tenant_id', 'project_id', 'project_name']
+ COLLISIONS = [('project_name', 'tenant_name'), ('project_id', 'tenant_id')]
+
+ def __str__(self):
+ """Represent only attributes included in self.ATTRIBUTES"""
+ attrs = [attr for attr in self.ATTRIBUTES if attr is not 'password']
+ _repr = dict((k, getattr(self, k)) for k in attrs)
+ return str(_repr)
+
+ def __setattr__(self, key, value):
+ # NOTE(andreaf) In order to ease the migration towards 'project' we
+ # support v2 credentials configured with 'project' and translate it
+ # to tenant on the fly. The original kwargs are stored for clients
+ # that may rely on them. We also set project when tenant is defined
+ # so clients can rely on project being part of credentials.
+ parent = super(KeystoneV2Credentials, self)
+ # for project_* set tenant only
+ if key == 'project_id':
+ parent.__setattr__('tenant_id', value)
+ elif key == 'project_name':
+ parent.__setattr__('tenant_name', value)
+ if key == 'tenant_id':
+ parent.__setattr__('project_id', value)
+ elif key == 'tenant_name':
+ parent.__setattr__('project_name', value)
+ # trigger default behaviour for all attributes
+ parent.__setattr__(key, value)
+
+ def is_valid(self):
+ """Check of credentials (no API call)
+
+ Minimum set of valid credentials, are username and password.
+ Tenant is optional.
+ """
+ return None not in (self.username, self.password)
+
+
+class KeystoneV3Credentials(Credentials):
+ """Credentials suitable for the Keystone Identity V3 API"""
+
+ ATTRIBUTES = ['domain_id', 'domain_name', 'password', 'username',
+ 'project_domain_id', 'project_domain_name', 'project_id',
+ 'project_name', 'tenant_id', 'tenant_name', 'user_domain_id',
+ 'user_domain_name', 'user_id']
+ COLLISIONS = [('project_name', 'tenant_name'), ('project_id', 'tenant_id')]
+
+ def __setattr__(self, key, value):
+ parent = super(KeystoneV3Credentials, self)
+ # for tenant_* set both project and tenant
+ if key == 'tenant_id':
+ parent.__setattr__('project_id', value)
+ elif key == 'tenant_name':
+ parent.__setattr__('project_name', value)
+ # for project_* set both project and tenant
+ if key == 'project_id':
+ parent.__setattr__('tenant_id', value)
+ elif key == 'project_name':
+ parent.__setattr__('tenant_name', value)
+ # for *_domain_* set both user and project if not set yet
+ if key == 'user_domain_id':
+ if self.project_domain_id is None:
+ parent.__setattr__('project_domain_id', value)
+ if key == 'project_domain_id':
+ if self.user_domain_id is None:
+ parent.__setattr__('user_domain_id', value)
+ if key == 'user_domain_name':
+ if self.project_domain_name is None:
+ parent.__setattr__('project_domain_name', value)
+ if key == 'project_domain_name':
+ if self.user_domain_name is None:
+ parent.__setattr__('user_domain_name', value)
+ # support domain_name coming from config
+ if key == 'domain_name':
+ if self.user_domain_name is None:
+ parent.__setattr__('user_domain_name', value)
+ if self.project_domain_name is None:
+ parent.__setattr__('project_domain_name', value)
+ # finally trigger default behaviour for all attributes
+ parent.__setattr__(key, value)
+
+ def is_valid(self):
+ """Check of credentials (no API call)
+
+ Valid combinations of v3 credentials (excluding token)
+ - User id, password (optional domain)
+ - User name, password and its domain id/name
+ For the scope, valid combinations are:
+ - None
+ - Project id (optional domain)
+ - Project name and its domain id/name
+ - Domain id
+ - Domain name
+ """
+ valid_user_domain = any(
+ [self.user_domain_id is not None,
+ self.user_domain_name is not None])
+ valid_project_domain = any(
+ [self.project_domain_id is not None,
+ self.project_domain_name is not None])
+ valid_user = any(
+ [self.user_id is not None,
+ self.username is not None and valid_user_domain])
+ valid_project_scope = any(
+ [self.project_name is None and self.project_id is None,
+ self.project_id is not None,
+ self.project_name is not None and valid_project_domain])
+ valid_domain_scope = any(
+ [self.domain_id is None and self.domain_name is None,
+ self.domain_id or self.domain_name])
+ return all([self.password is not None,
+ valid_user,
+ valid_project_scope and valid_domain_scope])
+
+
+IDENTITY_VERSION = {'v2': (KeystoneV2Credentials, KeystoneV2AuthProvider),
+ 'v3': (KeystoneV3Credentials, KeystoneV3AuthProvider)}
diff --git a/tempest/lib/base.py b/tempest/lib/base.py
new file mode 100644
index 0000000..3be55c0
--- /dev/null
+++ b/tempest/lib/base.py
@@ -0,0 +1,67 @@
+# 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.
+
+import os
+
+import fixtures
+import testtools
+
+
+class BaseTestCase(testtools.testcase.WithAttributes, testtools.TestCase):
+ setUpClassCalled = False
+
+ # NOTE(sdague): log_format is defined inline here instead of using the oslo
+ # default because going through the config path recouples config to the
+ # stress tests too early, and depending on testr order will fail unit tests
+ log_format = ('%(asctime)s %(process)d %(levelname)-8s '
+ '[%(name)s] %(message)s')
+
+ @classmethod
+ def setUpClass(cls):
+ if hasattr(super(BaseTestCase, cls), 'setUpClass'):
+ super(BaseTestCase, cls).setUpClass()
+ cls.setUpClassCalled = True
+
+ @classmethod
+ def tearDownClass(cls):
+ if hasattr(super(BaseTestCase, cls), 'tearDownClass'):
+ super(BaseTestCase, cls).tearDownClass()
+
+ def setUp(self):
+ super(BaseTestCase, self).setUp()
+ if not self.setUpClassCalled:
+ raise RuntimeError("setUpClass does not calls the super's "
+ "setUpClass in {!r}".format(type(self)))
+ test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
+ try:
+ test_timeout = int(test_timeout)
+ except ValueError:
+ test_timeout = 0
+ if test_timeout > 0:
+ self.useFixture(fixtures.Timeout(test_timeout, gentle=True))
+
+ if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
+ os.environ.get('OS_STDOUT_CAPTURE') == '1'):
+ stdout = self.useFixture(fixtures.StringStream('stdout')).stream
+ self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
+ if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
+ os.environ.get('OS_STDERR_CAPTURE') == '1'):
+ 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'):
+ self.useFixture(fixtures.LoggerFixture(nuke_handlers=False,
+ format=self.log_format,
+ level=None))
diff --git a/tempest/stress/__init__.py b/tempest/lib/cli/__init__.py
similarity index 100%
copy from tempest/stress/__init__.py
copy to tempest/lib/cli/__init__.py
diff --git a/tempest/lib/cli/base.py b/tempest/lib/cli/base.py
new file mode 100644
index 0000000..3fb56ec
--- /dev/null
+++ b/tempest/lib/cli/base.py
@@ -0,0 +1,443 @@
+# Copyright 2013 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.
+
+import os
+import shlex
+import subprocess
+
+from oslo_log import log as logging
+import six
+
+from tempest.lib import base
+import tempest.lib.cli.output_parser
+from tempest.lib import exceptions
+
+
+LOG = logging.getLogger(__name__)
+
+
+def execute(cmd, action, flags='', params='', fail_ok=False,
+ merge_stderr=False, cli_dir='/usr/bin', prefix=''):
+ """Executes specified command for the given action.
+
+ :param cmd: command to be executed
+ :type cmd: string
+ :param action: string of the cli command to run
+ :type action: string
+ :param flags: any optional cli flags to use
+ :type flags: string
+ :param params: string of any optional positional args to use
+ :type params: string
+ :param fail_ok: boolean if True an exception is not raised when the
+ cli return code is non-zero
+ :type fail_ok: boolean
+ :param merge_stderr: boolean if True the stderr buffer is merged into
+ stdout
+ :type merge_stderr: boolean
+ :param cli_dir: The path where the cmd can be executed
+ :type cli_dir: string
+ :param prefix: prefix to insert before command
+ :type prefix: string
+ """
+ cmd = ' '.join([prefix, os.path.join(cli_dir, cmd),
+ flags, action, params])
+ cmd = cmd.strip()
+ LOG.info("running: '%s'", cmd)
+ if six.PY2:
+ cmd = cmd.encode('utf-8')
+ cmd = shlex.split(cmd)
+ stdout = subprocess.PIPE
+ stderr = subprocess.STDOUT if merge_stderr else subprocess.PIPE
+ proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
+ result, result_err = proc.communicate()
+ if not fail_ok and proc.returncode != 0:
+ raise exceptions.CommandFailed(proc.returncode,
+ cmd,
+ result,
+ result_err)
+ if six.PY2:
+ return result
+ else:
+ return os.fsdecode(result)
+
+
+class CLIClient(object):
+ """Class to use OpenStack official python client CLI's with auth
+
+ :param username: The username to authenticate with
+ :type username: string
+ :param password: The password to authenticate with
+ :type password: string
+ :param tenant_name: The name of the tenant to use with the client calls
+ :type tenant_name: string
+ :param uri: The auth uri for the OpenStack Deployment
+ :type uri: string
+ :param cli_dir: The path where the python client binaries are installed.
+ defaults to /usr/bin
+ :type cli_dir: string
+ :param insecure: if True, --insecure is passed to python client binaries.
+ :type insecure: boolean
+ :param prefix: prefix to insert before commands
+ :type prefix: string
+ :param user_domain_name: User's domain name
+ :type user_domain_name: string
+ :param user_domain_id: User's domain ID
+ :type user_domain_id: string
+ :param project_domain_name: Project's domain name
+ :type project_domain_name: string
+ :param project_domain_id: Project's domain ID
+ :type project_domain_id: string
+ :param identity_api_version: Version of the Identity API
+ :type identity_api_version: string
+ """
+
+ def __init__(self, username='', password='', tenant_name='', uri='',
+ cli_dir='', insecure=False, prefix='', user_domain_name=None,
+ user_domain_id=None, project_domain_name=None,
+ project_domain_id=None, identity_api_version=None, *args,
+ **kwargs):
+ """Initialize a new CLIClient object."""
+ super(CLIClient, self).__init__()
+ self.cli_dir = cli_dir if cli_dir else '/usr/bin'
+ self.username = username
+ self.tenant_name = tenant_name
+ self.password = password
+ self.uri = uri
+ self.insecure = insecure
+ self.prefix = prefix
+ self.user_domain_name = user_domain_name
+ self.user_domain_id = user_domain_id
+ self.project_domain_name = project_domain_name
+ self.project_domain_id = project_domain_id
+ self.identity_api_version = identity_api_version
+
+ def nova(self, action, flags='', params='', fail_ok=False,
+ endpoint_type='publicURL', merge_stderr=False):
+ """Executes nova command for the given action.
+
+ :param action: the cli command to run using nova
+ :type action: string
+ :param flags: any optional cli flags to use
+ :type flags: string
+ :param params: any optional positional args to use
+ :type params: string
+ :param fail_ok: if True an exception is not raised when the
+ cli return code is non-zero
+ :type fail_ok: boolean
+ :param endpoint_type: the type of endpoint for the service
+ :type endpoint_type: string
+ :param merge_stderr: if True the stderr buffer is merged into stdout
+ :type merge_stderr: boolean
+ """
+ flags += ' --os-endpoint-type %s' % endpoint_type
+ return self.cmd_with_auth(
+ 'nova', action, flags, params, fail_ok, merge_stderr)
+
+ def nova_manage(self, action, flags='', params='', fail_ok=False,
+ merge_stderr=False):
+ """Executes nova-manage command for the given action.
+
+ :param action: the cli command to run using nova-manage
+ :type action: string
+ :param flags: any optional cli flags to use
+ :type flags: string
+ :param params: any optional positional args to use
+ :type params: string
+ :param fail_ok: if True an exception is not raised when the
+ cli return code is non-zero
+ :type fail_ok: boolean
+ :param merge_stderr: if True the stderr buffer is merged into stdout
+ :type merge_stderr: boolean
+ """
+ return execute(
+ 'nova-manage', action, flags, params, fail_ok, merge_stderr,
+ self.cli_dir)
+
+ def keystone(self, action, flags='', params='', fail_ok=False,
+ merge_stderr=False):
+ """Executes keystone command for the given action.
+
+ :param action: the cli command to run using keystone
+ :type action: string
+ :param flags: any optional cli flags to use
+ :type flags: string
+ :param params: any optional positional args to use
+ :type params: string
+ :param fail_ok: if True an exception is not raised when the
+ cli return code is non-zero
+ :type fail_ok: boolean
+ :param merge_stderr: if True the stderr buffer is merged into stdout
+ :type merge_stderr: boolean
+ """
+ return self.cmd_with_auth(
+ 'keystone', action, flags, params, fail_ok, merge_stderr)
+
+ def glance(self, action, flags='', params='', fail_ok=False,
+ endpoint_type='publicURL', merge_stderr=False):
+ """Executes glance command for the given action.
+
+ :param action: the cli command to run using glance
+ :type action: string
+ :param flags: any optional cli flags to use
+ :type flags: string
+ :param params: any optional positional args to use
+ :type params: string
+ :param fail_ok: if True an exception is not raised when the
+ cli return code is non-zero
+ :type fail_ok: boolean
+ :param endpoint_type: the type of endpoint for the service
+ :type endpoint_type: string
+ :param merge_stderr: if True the stderr buffer is merged into stdout
+ :type merge_stderr: boolean
+ """
+ flags += ' --os-endpoint-type %s' % endpoint_type
+ return self.cmd_with_auth(
+ 'glance', action, flags, params, fail_ok, merge_stderr)
+
+ def ceilometer(self, action, flags='', params='',
+ fail_ok=False, endpoint_type='publicURL',
+ merge_stderr=False):
+ """Executes ceilometer command for the given action.
+
+ :param action: the cli command to run using ceilometer
+ :type action: string
+ :param flags: any optional cli flags to use
+ :type flags: string
+ :param params: any optional positional args to use
+ :type params: string
+ :param fail_ok: if True an exception is not raised when the
+ cli return code is non-zero
+ :type fail_ok: boolean
+ :param endpoint_type: the type of endpoint for the service
+ :type endpoint_type: string
+ :param merge_stderr: if True the stderr buffer is merged into stdout
+ :type merge_stderr: boolean
+ """
+ flags += ' --os-endpoint-type %s' % endpoint_type
+ return self.cmd_with_auth(
+ 'ceilometer', action, flags, params, fail_ok, merge_stderr)
+
+ def heat(self, action, flags='', params='',
+ fail_ok=False, endpoint_type='publicURL', merge_stderr=False):
+ """Executes heat command for the given action.
+
+ :param action: the cli command to run using heat
+ :type action: string
+ :param flags: any optional cli flags to use
+ :type flags: string
+ :param params: any optional positional args to use
+ :type params: string
+ :param fail_ok: if True an exception is not raised when the
+ cli return code is non-zero
+ :type fail_ok: boolean
+ :param endpoint_type: the type of endpoint for the service
+ :type endpoint_type: string
+ :param merge_stderr: if True the stderr buffer is merged into stdout
+ :type merge_stderr: boolean
+ """
+ flags += ' --os-endpoint-type %s' % endpoint_type
+ return self.cmd_with_auth(
+ 'heat', action, flags, params, fail_ok, merge_stderr)
+
+ def cinder(self, action, flags='', params='', fail_ok=False,
+ endpoint_type='publicURL', merge_stderr=False):
+ """Executes cinder command for the given action.
+
+ :param action: the cli command to run using cinder
+ :type action: string
+ :param flags: any optional cli flags to use
+ :type flags: string
+ :param params: any optional positional args to use
+ :type params: string
+ :param fail_ok: if True an exception is not raised when the
+ cli return code is non-zero
+ :type fail_ok: boolean
+ :param endpoint_type: the type of endpoint for the service
+ :type endpoint_type: string
+ :param merge_stderr: if True the stderr buffer is merged into stdout
+ :type merge_stderr: boolean
+ """
+ flags += ' --endpoint-type %s' % endpoint_type
+ return self.cmd_with_auth(
+ 'cinder', action, flags, params, fail_ok, merge_stderr)
+
+ def swift(self, action, flags='', params='', fail_ok=False,
+ endpoint_type='publicURL', merge_stderr=False):
+ """Executes swift command for the given action.
+
+ :param action: the cli command to run using swift
+ :type action: string
+ :param flags: any optional cli flags to use
+ :type flags: string
+ :param params: any optional positional args to use
+ :type params: string
+ :param fail_ok: if True an exception is not raised when the
+ cli return code is non-zero
+ :type fail_ok: boolean
+ :param endpoint_type: the type of endpoint for the service
+ :type endpoint_type: string
+ :param merge_stderr: if True the stderr buffer is merged into stdout
+ :type merge_stderr: boolean
+ """
+ flags += ' --os-endpoint-type %s' % endpoint_type
+ return self.cmd_with_auth(
+ 'swift', action, flags, params, fail_ok, merge_stderr)
+
+ def neutron(self, action, flags='', params='', fail_ok=False,
+ endpoint_type='publicURL', merge_stderr=False):
+ """Executes neutron command for the given action.
+
+ :param action: the cli command to run using neutron
+ :type action: string
+ :param flags: any optional cli flags to use
+ :type flags: string
+ :param params: any optional positional args to use
+ :type params: string
+ :param fail_ok: if True an exception is not raised when the
+ cli return code is non-zero
+ :type fail_ok: boolean
+ :param endpoint_type: the type of endpoint for the service
+ :type endpoint_type: string
+ :param merge_stderr: if True the stderr buffer is merged into stdout
+ :type merge_stderr: boolean
+ """
+ flags += ' --endpoint-type %s' % endpoint_type
+ return self.cmd_with_auth(
+ 'neutron', action, flags, params, fail_ok, merge_stderr)
+
+ def sahara(self, action, flags='', params='',
+ fail_ok=False, endpoint_type='publicURL', merge_stderr=True):
+ """Executes sahara command for the given action.
+
+ :param action: the cli command to run using sahara
+ :type action: string
+ :param flags: any optional cli flags to use
+ :type flags: string
+ :param params: any optional positional args to use
+ :type params: string
+ :param fail_ok: if True an exception is not raised when the
+ cli return code is non-zero
+ :type fail_ok: boolean
+ :param endpoint_type: the type of endpoint for the service
+ :type endpoint_type: string
+ :param merge_stderr: if True the stderr buffer is merged into stdout
+ :type merge_stderr: boolean
+ """
+ flags += ' --endpoint-type %s' % endpoint_type
+ return self.cmd_with_auth(
+ 'sahara', action, flags, params, fail_ok, merge_stderr)
+
+ def openstack(self, action, flags='', params='', fail_ok=False,
+ merge_stderr=False):
+ """Executes openstack command for the given action.
+
+ :param action: the cli command to run using openstack
+ :type action: string
+ :param flags: any optional cli flags to use
+ :type flags: string
+ :param params: any optional positional args to use
+ :type params: string
+ :param fail_ok: if True an exception is not raised when the
+ cli return code is non-zero
+ :type fail_ok: boolean
+ :param merge_stderr: if True the stderr buffer is merged into stdout
+ :type merge_stderr: boolean
+ """
+ return self.cmd_with_auth(
+ 'openstack', action, flags, params, fail_ok, merge_stderr)
+
+ def cmd_with_auth(self, cmd, action, flags='', params='',
+ fail_ok=False, merge_stderr=False):
+ """Executes given command with auth attributes appended.
+
+ :param cmd: command to be executed
+ :type cmd: string
+ :param action: command on cli to run
+ :type action: string
+ :param flags: optional cli flags to use
+ :type flags: string
+ :param params: optional positional args to use
+ :type params: string
+ :param fail_ok: if True an exception is not raised when the cli return
+ code is non-zero
+ :type fail_ok: boolean
+ :param merge_stderr: if True the stderr buffer is merged into stdout
+ :type merge_stderr: boolean
+ """
+ creds = ('--os-username %s --os-project-name %s --os-password %s '
+ '--os-auth-url %s' %
+ (self.username,
+ self.tenant_name,
+ self.password,
+ self.uri))
+ if self.identity_api_version:
+ creds += ' --os-identity-api-version %s' % (
+ self.identity_api_version)
+ if self.user_domain_name is not None:
+ creds += ' --os-user-domain-name %s' % self.user_domain_name
+ if self.user_domain_id is not None:
+ creds += ' --os-user-domain-id %s' % self.user_domain_id
+ if self.project_domain_name is not None:
+ creds += ' --os-project-domain-name %s' % self.project_domain_name
+ if self.project_domain_id is not None:
+ creds += ' --os-project-domain-id %s' % self.project_domain_id
+ if self.insecure:
+ flags = creds + ' --insecure ' + flags
+ else:
+ flags = creds + ' ' + flags
+ return execute(cmd, action, flags, params, fail_ok, merge_stderr,
+ self.cli_dir, prefix=self.prefix)
+
+
+class ClientTestBase(base.BaseTestCase):
+ """Base test class for testing the OpenStack client CLI interfaces."""
+
+ def setUp(self):
+ super(ClientTestBase, self).setUp()
+ self.clients = self._get_clients()
+ self.parser = tempest.lib.cli.output_parser
+
+ def _get_clients(self):
+ """Abstract method to initialize CLIClient object.
+
+ This method must be overloaded in child test classes. It should be
+ used to initialize the CLIClient object with the appropriate
+ credentials during the setUp() phase of tests.
+ """
+ raise NotImplementedError
+
+ def assertTableStruct(self, items, field_names):
+ """Verify that all items has keys listed in field_names.
+
+ :param items: items to assert are field names in the output table
+ :type items: list
+ :param field_names: field names from the output table of the cmd
+ :type field_names: list
+ """
+ for item in items:
+ for field in field_names:
+ self.assertIn(field, item)
+
+ def assertFirstLineStartsWith(self, lines, beginning):
+ """Verify that the first line starts with a string
+
+ :param lines: strings for each line of output
+ :type lines: list
+ :param beginning: verify this is at the beginning of the first line
+ :type beginning: string
+ """
+ self.assertTrue(lines[0].startswith(beginning),
+ msg=('Beginning of first line has invalid content: %s'
+ % lines[:3]))
diff --git a/tempest/lib/cli/output_parser.py b/tempest/lib/cli/output_parser.py
new file mode 100644
index 0000000..a7d5e49
--- /dev/null
+++ b/tempest/lib/cli/output_parser.py
@@ -0,0 +1,171 @@
+# Copyright 2013 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.
+
+"""Collection of utilities for parsing CLI clients output."""
+
+import re
+
+from oslo_log import log as logging
+
+from tempest.lib import exceptions
+
+
+LOG = logging.getLogger(__name__)
+
+
+delimiter_line = re.compile('^\+\-[\+\-]+\-\+$')
+
+
+def details_multiple(output_lines, with_label=False):
+ """Return list of dicts with item details from cli output tables.
+
+ If with_label is True, key '__label' is added to each items dict.
+ For more about 'label' see OutputParser.tables().
+ """
+ items = []
+ tables_ = tables(output_lines)
+ for table_ in tables_:
+ if ('Property' not in table_['headers'] or
+ 'Value' not in table_['headers']):
+ raise exceptions.InvalidStructure()
+ item = {}
+ for value in table_['values']:
+ item[value[0]] = value[1]
+ if with_label:
+ item['__label'] = table_['label']
+ items.append(item)
+ return items
+
+
+def details(output_lines, with_label=False):
+ """Return dict with details of first item (table) found in output."""
+ items = details_multiple(output_lines, with_label)
+ return items[0]
+
+
+def listing(output_lines):
+ """Return list of dicts with basic item info parsed from cli output."""
+
+ items = []
+ table_ = table(output_lines)
+ for row in table_['values']:
+ item = {}
+ for col_idx, col_key in enumerate(table_['headers']):
+ item[col_key] = row[col_idx]
+ items.append(item)
+ return items
+
+
+def tables(output_lines):
+ """Find all ascii-tables in output and parse them.
+
+ Return list of tables parsed from cli output as dicts.
+ (see OutputParser.table())
+
+ And, if found, label key (separated line preceding the table)
+ is added to each tables dict.
+ """
+ tables_ = []
+
+ table_ = []
+ label = None
+
+ start = False
+ header = False
+
+ if not isinstance(output_lines, list):
+ output_lines = output_lines.split('\n')
+
+ for line in output_lines:
+ if delimiter_line.match(line):
+ if not start:
+ start = True
+ elif not header:
+ # we are after head area
+ header = True
+ else:
+ # table ends here
+ start = header = None
+ table_.append(line)
+
+ parsed = table(table_)
+ parsed['label'] = label
+ tables_.append(parsed)
+
+ table_ = []
+ label = None
+ continue
+ if start:
+ table_.append(line)
+ else:
+ if label is None:
+ label = line
+ else:
+ LOG.warning('Invalid line between tables: %s', line)
+ if table_:
+ LOG.warning('Missing end of table')
+
+ return tables_
+
+
+def table(output_lines):
+ """Parse single table from cli output.
+
+ Return dict with list of column names in 'headers' key and
+ rows in 'values' key.
+ """
+ table_ = {'headers': [], 'values': []}
+ columns = None
+
+ if not isinstance(output_lines, list):
+ output_lines = output_lines.split('\n')
+
+ if not output_lines[-1]:
+ # skip last line if empty (just newline at the end)
+ output_lines = output_lines[:-1]
+
+ for line in output_lines:
+ if delimiter_line.match(line):
+ columns = _table_columns(line)
+ continue
+ if '|' not in line:
+ LOG.warning('skipping invalid table line: %s', line)
+ continue
+ row = []
+ for col in columns:
+ row.append(line[col[0]:col[1]].strip())
+ if table_['headers']:
+ table_['values'].append(row)
+ else:
+ table_['headers'] = row
+
+ return table_
+
+
+def _table_columns(first_table_row):
+ """Find column ranges in output line.
+
+ Return list of tuples (start,end) for each column
+ detected by plus (+) characters in delimiter line.
+ """
+ positions = []
+ start = 1 # there is '+' at 0
+ while start < len(first_table_row):
+ end = first_table_row.find('+', start)
+ if end == -1:
+ break
+ positions.append((start, end))
+ start = end + 1
+ return positions
diff --git a/tempest/stress/__init__.py b/tempest/lib/cmd/__init__.py
similarity index 100%
copy from tempest/stress/__init__.py
copy to tempest/lib/cmd/__init__.py
diff --git a/tempest/lib/cmd/check_uuid.py b/tempest/lib/cmd/check_uuid.py
new file mode 100755
index 0000000..d1f0888
--- /dev/null
+++ b/tempest/lib/cmd/check_uuid.py
@@ -0,0 +1,362 @@
+#!/usr/bin/env python
+
+# Copyright 2014 Mirantis, Inc.
+#
+# 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 argparse
+import ast
+import importlib
+import inspect
+import os
+import sys
+import unittest
+import uuid
+
+from oslo_utils import uuidutils
+import six.moves.urllib.parse as urlparse
+
+DECORATOR_MODULE = 'decorators'
+DECORATOR_NAME = 'idempotent_id'
+DECORATOR_IMPORT = 'tempest.%s' % DECORATOR_MODULE
+IMPORT_LINE = 'from tempest.lib import %s' % DECORATOR_MODULE
+DECORATOR_TEMPLATE = "@%s.%s('%%s')" % (DECORATOR_MODULE,
+ DECORATOR_NAME)
+UNIT_TESTS_EXCLUDE = 'tempest.tests'
+
+
+class SourcePatcher(object):
+
+ """"Lazy patcher for python source files"""
+
+ def __init__(self):
+ self.source_files = None
+ self.patches = None
+ self.clear()
+
+ def clear(self):
+ """Clear inner state"""
+ self.source_files = {}
+ self.patches = {}
+
+ @staticmethod
+ def _quote(s):
+ return urlparse.quote(s)
+
+ @staticmethod
+ def _unquote(s):
+ return urlparse.unquote(s)
+
+ def add_patch(self, filename, patch, line_no):
+ """Add lazy patch"""
+ if filename not in self.source_files:
+ with open(filename) as f:
+ self.source_files[filename] = self._quote(f.read())
+ patch_id = uuidutils.generate_uuid()
+ if not patch.endswith('\n'):
+ patch += '\n'
+ self.patches[patch_id] = self._quote(patch)
+ lines = self.source_files[filename].split(self._quote('\n'))
+ lines[line_no - 1] = ''.join(('{%s:s}' % patch_id, lines[line_no - 1]))
+ self.source_files[filename] = self._quote('\n').join(lines)
+
+ @staticmethod
+ def _save_changes(filename, source):
+ print('%s fixed' % filename)
+ with open(filename, 'w') as f:
+ f.write(source)
+
+ def apply_patches(self):
+ """Apply all patches"""
+ for filename in self.source_files:
+ patched_source = self._unquote(
+ self.source_files[filename].format(**self.patches)
+ )
+ self._save_changes(filename, patched_source)
+ self.clear()
+
+
+class TestChecker(object):
+
+ def __init__(self, package):
+ self.package = package
+ self.base_path = os.path.abspath(os.path.dirname(package.__file__))
+
+ def _path_to_package(self, path):
+ relative_path = path[len(self.base_path) + 1:]
+ if relative_path:
+ return '.'.join((self.package.__name__,) +
+ tuple(relative_path.split('/')))
+ else:
+ return self.package.__name__
+
+ def _modules_search(self):
+ """Recursive search for python modules in base package"""
+ modules = []
+ 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)
+ for item in files:
+ if item.endswith('.py'):
+ module_name = '.'.join((root_package,
+ os.path.splitext(item)[0]))
+ if not module_name.startswith(UNIT_TESTS_EXCLUDE):
+ modules.append(module_name)
+ return modules
+
+ @staticmethod
+ def _get_idempotent_id(test_node):
+ "Return key-value dict with metadata from @decorators.idempotent_id"
+ 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):
+ for arg in decorator.args:
+ idempotent_id = ast.literal_eval(arg)
+ return idempotent_id
+
+ @staticmethod
+ def _is_decorator(line):
+ return line.strip().startswith('@')
+
+ @staticmethod
+ def _is_def(line):
+ return line.strip().startswith('def ')
+
+ def _add_uuid_to_test(self, patcher, test_node, source_path):
+ with open(source_path) as src:
+ src_lines = src.read().split('\n')
+ lineno = test_node.lineno
+ insert_position = lineno
+ while True:
+ if (self._is_def(src_lines[lineno - 1]) or
+ (self._is_decorator(src_lines[lineno - 1]) and
+ (DECORATOR_TEMPLATE.split('(')[0] <=
+ src_lines[lineno - 1].strip().split('(')[0]))):
+ insert_position = lineno
+ break
+ lineno += 1
+ patcher.add_patch(
+ source_path,
+ ' ' * test_node.col_offset + DECORATOR_TEMPLATE % uuid.uuid4(),
+ insert_position
+ )
+
+ @staticmethod
+ def _is_test_case(module, node):
+ if (node.__class__ is ast.ClassDef and
+ hasattr(module, node.name) and
+ inspect.isclass(getattr(module, node.name))):
+ return issubclass(getattr(module, node.name), unittest.TestCase)
+
+ @staticmethod
+ def _is_test_method(node):
+ return (node.__class__ is ast.FunctionDef and
+ node.name.startswith('test_'))
+
+ @staticmethod
+ def _next_node(body, node):
+ if body.index(node) < len(body):
+ return body[body.index(node) + 1]
+
+ @staticmethod
+ def _import_name(node):
+ if isinstance(node, ast.Import):
+ return node.names[0].name
+ elif isinstance(node, ast.ImportFrom):
+ return '%s.%s' % (node.module, node.names[0].name)
+
+ def _add_import_for_test_uuid(self, patcher, src_parsed, source_path):
+ with open(source_path) as f:
+ src_lines = f.read().split('\n')
+ line_no = 0
+ tempest_imports = [node for node in src_parsed.body
+ if self._import_name(node) and
+ 'tempest.' in self._import_name(node)]
+ if not tempest_imports:
+ import_snippet = '\n'.join(('', IMPORT_LINE, ''))
+ else:
+ for node in tempest_imports:
+ if self._import_name(node) < DECORATOR_IMPORT:
+ continue
+ else:
+ line_no = node.lineno
+ import_snippet = IMPORT_LINE
+ break
+ else:
+ line_no = tempest_imports[-1].lineno
+ while True:
+ if (not src_lines[line_no - 1] or
+ getattr(self._next_node(src_parsed.body,
+ tempest_imports[-1]),
+ 'lineno') == line_no or
+ line_no == len(src_lines)):
+ break
+ line_no += 1
+ import_snippet = '\n'.join((IMPORT_LINE, ''))
+ patcher.add_patch(source_path, import_snippet, line_no)
+
+ def get_tests(self):
+ """Get test methods with sources from base package with metadata"""
+ tests = {}
+ for module_name in self._modules_search():
+ tests[module_name] = {}
+ module = importlib.import_module(module_name)
+ source_path = '.'.join(
+ (os.path.splitext(module.__file__)[0], 'py')
+ )
+ with open(source_path, 'r') as f:
+ source = f.read()
+ tests[module_name]['source_path'] = source_path
+ tests[module_name]['tests'] = {}
+ source_parsed = ast.parse(source)
+ tests[module_name]['ast'] = source_parsed
+ tests[module_name]['import_valid'] = (
+ hasattr(module, DECORATOR_MODULE) and
+ inspect.ismodule(getattr(module, DECORATOR_MODULE))
+ )
+ test_cases = (node for node in source_parsed.body
+ if self._is_test_case(module, node))
+ for node in test_cases:
+ for subnode in filter(self._is_test_method, node.body):
+ test_name = '%s.%s' % (node.name, subnode.name)
+ tests[module_name]['tests'][test_name] = subnode
+ return tests
+
+ @staticmethod
+ def _filter_tests(function, tests):
+ """Filter tests with condition 'function(test_node) == True'"""
+ result = {}
+ for module_name in tests:
+ for test_name in tests[module_name]['tests']:
+ if function(module_name, test_name, tests):
+ if module_name not in result:
+ result[module_name] = {
+ 'ast': tests[module_name]['ast'],
+ 'source_path': tests[module_name]['source_path'],
+ 'import_valid': tests[module_name]['import_valid'],
+ 'tests': {}
+ }
+ result[module_name]['tests'][test_name] = \
+ tests[module_name]['tests'][test_name]
+ return result
+
+ def find_untagged(self, tests):
+ """Filter all tests without uuid in metadata"""
+ def check_uuid_in_meta(module_name, test_name, tests):
+ idempotent_id = self._get_idempotent_id(
+ tests[module_name]['tests'][test_name])
+ return not idempotent_id
+ return self._filter_tests(check_uuid_in_meta, tests)
+
+ def report_collisions(self, tests):
+ """Reports collisions if there are any
+
+ Returns true if collisions exist.
+ """
+ uuids = {}
+
+ def report(module_name, test_name, tests):
+ test_uuid = self._get_idempotent_id(
+ tests[module_name]['tests'][test_name])
+ if not test_uuid:
+ return
+ if test_uuid in uuids:
+ error_str = "%s:%s\n uuid %s collision: %s<->%s\n%s:%s" % (
+ tests[module_name]['source_path'],
+ tests[module_name]['tests'][test_name].lineno,
+ test_uuid,
+ test_name,
+ uuids[test_uuid]['test_name'],
+ uuids[test_uuid]['source_path'],
+ uuids[test_uuid]['test_node'].lineno,
+ )
+ print(error_str)
+ print("cannot automatically resolve the collision, please "
+ "manually remove the duplicate value on the new test.")
+ return True
+ else:
+ uuids[test_uuid] = {
+ 'module': module_name,
+ 'test_name': test_name,
+ 'test_node': tests[module_name]['tests'][test_name],
+ 'source_path': tests[module_name]['source_path']
+ }
+ return bool(self._filter_tests(report, tests))
+
+ def report_untagged(self, tests):
+ """Reports untagged tests if there are any
+
+ Returns true if untagged tests exist.
+ """
+ def report(module_name, test_name, tests):
+ error_str = ("%s:%s\nmissing @decorators.idempotent_id"
+ "('...')\n%s\n") % (
+ tests[module_name]['source_path'],
+ tests[module_name]['tests'][test_name].lineno,
+ test_name
+ )
+ print(error_str)
+ return True
+ return bool(self._filter_tests(report, tests))
+
+ def fix_tests(self, tests):
+ """Add uuids to all specified in tests and fix it in source files"""
+ patcher = SourcePatcher()
+ for module_name in tests:
+ add_import_once = True
+ for test_name in tests[module_name]['tests']:
+ if not tests[module_name]['import_valid'] and add_import_once:
+ self._add_import_for_test_uuid(
+ patcher,
+ tests[module_name]['ast'],
+ tests[module_name]['source_path']
+ )
+ add_import_once = False
+ self._add_uuid_to_test(
+ patcher, tests[module_name]['tests'][test_name],
+ tests[module_name]['source_path'])
+ patcher.apply_patches()
+
+
+def run():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--package', action='store', dest='package',
+ default='tempest', type=str,
+ help='Package with tests')
+ parser.add_argument('--fix', action='store_true', dest='fix_tests',
+ help='Attempt to fix tests without UUIDs')
+ args = parser.parse_args()
+ sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+ pkg = importlib.import_module(args.package)
+ checker = TestChecker(pkg)
+ errors = False
+ tests = checker.get_tests()
+ untagged = checker.find_untagged(tests)
+ errors = checker.report_collisions(tests) or errors
+ if args.fix_tests and untagged:
+ checker.fix_tests(untagged)
+ else:
+ errors = checker.report_untagged(untagged) or errors
+ if errors:
+ sys.exit("@decorators.idempotent_id existence and uniqueness checks "
+ "failed\n"
+ "Run 'tox -v -e uuidgen' to automatically fix tests with\n"
+ "missing @decorators.idempotent_id decorators.")
+
+if __name__ == '__main__':
+ run()
diff --git a/tempest/lib/cmd/skip_tracker.py b/tempest/lib/cmd/skip_tracker.py
new file mode 100755
index 0000000..87806b7
--- /dev/null
+++ b/tempest/lib/cmd/skip_tracker.py
@@ -0,0 +1,168 @@
+#!/usr/bin/env python2
+
+# 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.
+
+"""
+Track test skips via launchpadlib API and raise alerts if a bug
+is fixed but a skip is still in the Tempest test code
+"""
+
+import argparse
+import os
+import re
+
+from oslo_log import log as logging
+
+try:
+ from launchpadlib import launchpad
+except ImportError:
+ launchpad = None
+
+LPCACHEDIR = os.path.expanduser('~/.launchpadlib/cache')
+LOG = logging.getLogger(__name__)
+
+BASEDIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../..'))
+TESTDIR = os.path.join(BASEDIR, 'tempest')
+
+
+def parse_args():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('test_path', nargs='?', default=TESTDIR,
+ help='Path of test dir')
+ return parser.parse_args()
+
+
+def info(msg, *args, **kwargs):
+ LOG.info(msg, *args, **kwargs)
+
+
+def debug(msg, *args, **kwargs):
+ LOG.debug(msg, *args, **kwargs)
+
+
+def find_skips(start):
+ """Find the entire list of skipped tests.
+
+ Returns a list of tuples (method, bug) that represent
+ test methods that have been decorated to skip because of
+ a particular bug.
+ """
+ results = {}
+ debug("Searching in %s", start)
+ for root, _dirs, files in os.walk(start):
+ for name in files:
+ if name.startswith('test_') and name.endswith('py'):
+ path = os.path.join(root, name)
+ debug("Searching in %s", path)
+ temp_result = find_skips_in_file(path)
+ for method_name, bug_no in temp_result:
+ if results.get(bug_no):
+ result_dict = results.get(bug_no)
+ if result_dict.get(name):
+ result_dict[name].append(method_name)
+ else:
+ result_dict[name] = [method_name]
+ results[bug_no] = result_dict
+ else:
+ results[bug_no] = {name: [method_name]}
+ return results
+
+
+def find_skips_in_file(path):
+ """Return the skip tuples in a test file."""
+ BUG_RE = re.compile(r'\s*@.*skip_because\(bug=[\'"](\d+)[\'"]')
+ DEF_RE = re.compile(r'\s*def (\w+)\(')
+ bug_found = False
+ results = []
+ with open(path, 'rb') as content:
+ lines = content.readlines()
+ for x, line in enumerate(lines):
+ if not bug_found:
+ res = BUG_RE.match(line)
+ if res:
+ bug_no = int(res.group(1))
+ debug("Found bug skip %s on line %d", bug_no, x + 1)
+ bug_found = True
+ else:
+ res = DEF_RE.match(line)
+ if res:
+ method = res.group(1)
+ debug("Found test method %s skips for bug %d",
+ method, bug_no)
+ results.append((method, bug_no))
+ bug_found = False
+ return results
+
+
+def get_results(result_dict):
+ results = []
+ for bug_no in result_dict:
+ for method in result_dict[bug_no]:
+ results.append((method, bug_no))
+ return results
+
+
+def main():
+ parser = parse_args()
+ results = find_skips(parser.test_path)
+ unique_bugs = sorted(set([bug for (method, bug) in get_results(results)]))
+ unskips = []
+ duplicates = []
+ info("Total bug skips found: %d", len(results))
+ info("Total unique bugs causing skips: %d", len(unique_bugs))
+ if launchpad is not None:
+ lp = launchpad.Launchpad.login_anonymously('grabbing bugs',
+ 'production',
+ LPCACHEDIR)
+ else:
+ print("To check the bug status launchpadlib should be installed")
+ exit(1)
+
+ for bug_no in unique_bugs:
+ bug = lp.bugs[bug_no]
+ duplicate = bug.duplicate_of_link
+ if duplicate is not None:
+ dup_id = duplicate.split('/')[-1]
+ duplicates.append((bug_no, dup_id))
+ for task in bug.bug_tasks:
+ info("Bug #%7s (%12s - %12s)", bug_no,
+ task.importance, task.status)
+ if task.status in ('Fix Released', 'Fix Committed'):
+ unskips.append(bug_no)
+
+ for bug_id, dup_id in duplicates:
+ if bug_id not in unskips:
+ dup_bug = lp.bugs[dup_id]
+ for task in dup_bug.bug_tasks:
+ info("Bug #%7s is a duplicate of Bug#%7s (%12s - %12s)",
+ bug_id, dup_id, task.importance, task.status)
+ if task.status in ('Fix Released', 'Fix Committed'):
+ unskips.append(bug_id)
+
+ unskips = sorted(set(unskips))
+ if unskips:
+ print("The following bugs have been fixed and the corresponding skips")
+ print("should be removed from the test cases:")
+ print()
+ for bug in unskips:
+ message = " %7s in " % bug
+ locations = ["%s" % x for x in results[bug].keys()]
+ message += " and ".join(locations)
+ print(message)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tempest/api_schema/__init__.py b/tempest/lib/common/__init__.py
similarity index 100%
copy from tempest/api_schema/__init__.py
copy to tempest/lib/common/__init__.py
diff --git a/tempest/lib/common/api_version_request.py b/tempest/lib/common/api_version_request.py
new file mode 100644
index 0000000..b2b68a6
--- /dev/null
+++ b/tempest/lib/common/api_version_request.py
@@ -0,0 +1,159 @@
+# Copyright 2014 IBM Corp.
+#
+# 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 re
+
+from tempest.lib import exceptions
+
+
+# Define the minimum and maximum version of the API across all of the
+# REST API. The format of the version is:
+# X.Y where:
+#
+# - X will only be changed if a significant backwards incompatible API
+# change is made which affects the API as whole. That is, something
+# that is only very very rarely incremented.
+#
+# - Y when you make any change to the API. Note that this includes
+# semantic changes which may not affect the input or output formats or
+# even originate in the API code layer. We are not distinguishing
+# between backwards compatible and backwards incompatible changes in
+# the versioning system. It must be made clear in the documentation as
+# to what is a backwards compatible change and what is a backwards
+# incompatible one.
+
+class APIVersionRequest(object):
+ """This class represents an API Version Request.
+
+ This class provides convenience methods for manipulation
+ and comparison of version numbers that we need to do to
+ implement microversions.
+
+ :param version_string: String representation of APIVersionRequest.
+ Correct format is 'X.Y', where 'X' and 'Y' are int values.
+ None value should be used to create Null APIVersionRequest,
+ which is equal to 0.0
+ """
+
+ # NOTE: This 'latest' version is a magic number, we assume any
+ # projects(Nova, etc.) never achieve this number.
+ latest_ver_major = 99999
+ latest_ver_minor = 99999
+
+ def __init__(self, version_string=None):
+ """Create an API version request object."""
+ # NOTE(gmann): 'version_string' as String "None" will be considered as
+ # invalid version string.
+ self.ver_major = 0
+ self.ver_minor = 0
+
+ if version_string is not None:
+ match = re.match(r"^([1-9]\d*)\.([1-9]\d*|0)$",
+ version_string)
+ if match:
+ self.ver_major = int(match.group(1))
+ self.ver_minor = int(match.group(2))
+ elif version_string == 'latest':
+ self.ver_major = self.latest_ver_major
+ self.ver_minor = self.latest_ver_minor
+ else:
+ raise exceptions.InvalidAPIVersionString(
+ version=version_string)
+
+ def __str__(self):
+ """Debug/Logging representation of object."""
+ return ("API Version Request: %s" % self.get_string())
+
+ def is_null(self):
+ """Checks whether version is null.
+
+ Return True if version object is null otherwise False.
+
+ :returns: boolean
+ """
+ return self.ver_major == 0 and self.ver_minor == 0
+
+ def _format_type_error(self, other):
+ return TypeError("'%(other)s' should be an instance of '%(cls)s'" %
+ {"other": other, "cls": self.__class__})
+
+ def __lt__(self, other):
+ if not isinstance(other, APIVersionRequest):
+ raise self._format_type_error(other)
+
+ return ((self.ver_major, self.ver_minor) <
+ (other.ver_major, other.ver_minor))
+
+ def __eq__(self, other):
+ if not isinstance(other, APIVersionRequest):
+ raise self._format_type_error(other)
+
+ return ((self.ver_major, self.ver_minor) ==
+ (other.ver_major, other.ver_minor))
+
+ def __gt__(self, other):
+ if not isinstance(other, APIVersionRequest):
+ raise self._format_type_error(other)
+
+ return ((self.ver_major, self.ver_minor) >
+ (other.ver_major, other.ver_minor))
+
+ def __le__(self, other):
+ return self < other or self == other
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def __ge__(self, other):
+ return self > other or self == other
+
+ def matches(self, min_version, max_version):
+ """Matches the version object.
+
+ Returns whether the version object represents a version
+ greater than or equal to the minimum version and less than
+ or equal to the maximum version.
+
+ :param min_version: Minimum acceptable version.
+ :param max_version: Maximum acceptable version.
+ :returns: boolean
+
+ If min_version is null then there is no minimum limit.
+ If max_version is null then there is no maximum limit.
+ If self is null then raise ValueError
+ """
+
+ if self.is_null():
+ raise ValueError
+ if max_version.is_null() and min_version.is_null():
+ return True
+ elif max_version.is_null():
+ return min_version <= self
+ elif min_version.is_null():
+ return self <= max_version
+ else:
+ return min_version <= self <= max_version
+
+ def get_string(self):
+ """Version string representation.
+
+ Converts object to string representation which if used to create
+ an APIVersionRequest object results in the same version request.
+ """
+ if self.is_null():
+ return None
+ if (self.ver_major == self.latest_ver_major and
+ self.ver_minor == self.latest_ver_minor):
+ return 'latest'
+ return "%s.%s" % (self.ver_major, self.ver_minor)
diff --git a/tempest/lib/common/api_version_utils.py b/tempest/lib/common/api_version_utils.py
new file mode 100644
index 0000000..bcb076b
--- /dev/null
+++ b/tempest/lib/common/api_version_utils.py
@@ -0,0 +1,178 @@
+# Copyright 2015 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.
+
+import testtools
+
+from tempest.lib.common import api_version_request
+from tempest.lib import exceptions
+
+
+LATEST_MICROVERSION = 'latest'
+
+
+class BaseMicroversionTest(object):
+ """Mixin class for API microversion test class."""
+
+ # NOTE: Basically, each microversion is small API change and we
+ # can use the same tests for most microversions in most cases.
+ # So it is nice to define the test class as possible to run
+ # for all microversions. We need to define microversion range
+ # (min_microversion, max_microversion) on each test class if necessary.
+ min_microversion = None
+ max_microversion = LATEST_MICROVERSION
+
+
+def check_skip_with_microversion(test_min_version, test_max_version,
+ cfg_min_version, cfg_max_version):
+ """Checks API microversions range and returns whether test needs to be skip
+
+ Compare the test and configured microversion range and returns
+ whether test microversion range is out of configured one.
+ This method can be used to skip the test based on configured and test
+ microversion range.
+
+ :param test_min_version: Test Minimum Microversion
+ :param test_max_version: Test Maximum Microversion
+ :param cfg_min_version: Configured Minimum Microversion
+ :param cfg_max_version: Configured Maximum Microversion
+ :returns: boolean
+ """
+
+ min_version = api_version_request.APIVersionRequest(test_min_version)
+ max_version = api_version_request.APIVersionRequest(test_max_version)
+ config_min_version = api_version_request.APIVersionRequest(cfg_min_version)
+ config_max_version = api_version_request.APIVersionRequest(cfg_max_version)
+ if ((min_version > max_version) or
+ (config_min_version > config_max_version)):
+ msg = ("Test Class versions [%s - %s]. "
+ "Configuration versions [%s - %s]."
+ % (min_version.get_string(),
+ max_version.get_string(),
+ config_min_version.get_string(),
+ config_max_version.get_string()))
+ raise exceptions.InvalidAPIVersionRange(msg)
+
+ # NOTE: Select tests which are in range of configuration like
+ # config min config max
+ # ----------------+--------------------------+----------------
+ # ...don't-select|
+ # ...select... ...select... ...select...
+ # |don't-select...
+ # ......................select............................
+ if (max_version < config_min_version or
+ config_max_version < min_version):
+ msg = ("The microversion range[%s - %s] of this test is out of the "
+ "configuration range[%s - %s]."
+ % (min_version.get_string(),
+ max_version.get_string(),
+ config_min_version.get_string(),
+ config_max_version.get_string()))
+ raise testtools.TestCase.skipException(msg)
+
+
+def select_request_microversion(test_min_version, cfg_min_version):
+ """Select microversion from test and configuration min version.
+
+ Compare requested microversion and return the maximum
+ microversion out of those.
+
+ :param test_min_version: Test Minimum Microversion
+ :param cfg_min_version: Configured Minimum Microversion
+ :returns: Selected microversion string
+ """
+
+ test_version = api_version_request.APIVersionRequest(test_min_version)
+ cfg_version = api_version_request.APIVersionRequest(cfg_min_version)
+ max_version = cfg_version if cfg_version >= test_version else test_version
+ return max_version.get_string()
+
+
+def assert_version_header_matches_request(api_microversion_header_name,
+ api_microversion,
+ response_header):
+ """Checks API microversion in response header
+
+ Verify whether microversion is present in response header
+ and with specified 'api_microversion' value.
+
+ :param api_microversion_header_name: Microversion header name
+ Example- "X-OpenStack-Nova-API-Version"
+ :param api_microversion: Microversion number like "2.10"
+ :param response_header: Response header where microversion is
+ expected to be present.
+ """
+ api_microversion_header_name = api_microversion_header_name.lower()
+ if (api_microversion_header_name not in response_header or
+ api_microversion != response_header[api_microversion_header_name]):
+ msg = ("Microversion header '%s' with value '%s' does not match in "
+ "response - %s. " % (api_microversion_header_name,
+ api_microversion,
+ response_header))
+ raise exceptions.InvalidHTTPResponseHeader(msg)
+
+
+def compare_version_header_to_response(api_microversion_header_name,
+ api_microversion,
+ response_header,
+ operation='eq'):
+ """Compares API microversion in response header to ``api_microversion``.
+
+ Compare the ``api_microversion`` value in response header if microversion
+ header is present in response, otherwise return false.
+
+ To make this function work for APIs which do not return microversion
+ header in response (example compute v2.0), this function does *not* raise
+ InvalidHTTPResponseHeader.
+
+ :param api_microversion_header_name: Microversion header name. Example:
+ 'Openstack-Api-Version'.
+ :param api_microversion: Microversion number. Example:
+
+ * '2.10' for the old-style header name, 'X-OpenStack-Nova-API-Version'
+ * 'Compute 2.10' for the new-style header name, 'Openstack-Api-Version'
+
+ :param response_header: Response header where microversion is
+ expected to be present.
+ :param operation: The boolean operation to use to compare the
+ ``api_microversion`` to the microversion in ``response_header``.
+ Can be 'lt', 'eq', 'gt', 'le', 'ne', 'ge'. Default is 'eq'. The
+ operation type should be based on the order of the arguments:
+ ``api_microversion`` <operation> ``response_header`` microversion.
+ :returns: True if the comparison is logically true, else False if the
+ comparison is logically false or if ``api_microversion_header_name`` is
+ missing in the ``response_header``.
+ :raises InvalidParam: If the operation is not lt, eq, gt, le, ne or ge.
+ """
+ api_microversion_header_name = api_microversion_header_name.lower()
+ if api_microversion_header_name not in response_header:
+ return False
+
+ op = getattr(api_version_request.APIVersionRequest,
+ '__%s__' % operation, None)
+
+ if op is None:
+ msg = ("Operation %s is invalid. Valid options include: lt, eq, gt, "
+ "le, ne, ge." % operation)
+ raise exceptions.InvalidParam(invalid_param=msg)
+
+ # Remove "volume" from "volume <microversion>", for example, so that the
+ # microversion can be converted to `APIVersionRequest`.
+ api_version = api_microversion.split(' ')[-1]
+ resp_version = response_header[api_microversion_header_name].split(' ')[-1]
+ if not op(
+ api_version_request.APIVersionRequest(api_version),
+ api_version_request.APIVersionRequest(resp_version)):
+ return False
+
+ return True
diff --git a/tempest/lib/common/cred_client.py b/tempest/lib/common/cred_client.py
new file mode 100644
index 0000000..a81f53c
--- /dev/null
+++ b/tempest/lib/common/cred_client.py
@@ -0,0 +1,212 @@
+# 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 abc
+
+from oslo_log import log as logging
+import six
+
+from tempest.lib import auth
+from tempest.lib import exceptions as lib_exc
+from tempest.lib.services.identity.v2 import identity_client as v2_identity
+
+LOG = logging.getLogger(__name__)
+
+
+@six.add_metaclass(abc.ABCMeta)
+class CredsClient(object):
+ """This class is a wrapper around the identity clients
+
+ to provide a single interface for managing credentials in both v2 and v3
+ cases. It's not bound to created credentials, only to a specific set of
+ admin credentials used for generating credentials.
+ """
+
+ def __init__(self, identity_client, projects_client, users_client,
+ roles_client):
+ # The client implies version and credentials
+ self.identity_client = identity_client
+ self.users_client = users_client
+ self.projects_client = projects_client
+ self.roles_client = roles_client
+
+ def create_user(self, username, password, project, email):
+ params = {'name': username,
+ 'password': password,
+ self.project_id_param: project['id'],
+ 'email': email}
+ user = self.users_client.create_user(**params)
+ if 'user' in user:
+ user = user['user']
+ return user
+
+ def delete_user(self, user_id):
+ self.users_client.delete_user(user_id)
+
+ @abc.abstractmethod
+ def create_project(self, name, description):
+ pass
+
+ def _check_role_exists(self, role_name):
+ try:
+ roles = self._list_roles()
+ lc_role_name = role_name.lower()
+ role = next(r for r in roles if r['name'].lower() == lc_role_name)
+ except StopIteration:
+ return None
+ return role
+
+ def create_user_role(self, role_name):
+ if not self._check_role_exists(role_name):
+ self.roles_client.create_role(name=role_name)
+
+ def assign_user_role(self, user, project, role_name):
+ role = self._check_role_exists(role_name)
+ if not role:
+ msg = 'No "%s" role found' % role_name
+ raise lib_exc.NotFound(msg)
+ try:
+ self.roles_client.create_user_role_on_project(project['id'],
+ user['id'],
+ role['id'])
+ except lib_exc.Conflict:
+ LOG.debug("Role %s already assigned on project %s for user %s",
+ role['id'], project['id'], user['id'])
+
+ @abc.abstractmethod
+ def get_credentials(self, user, project, password):
+ """Produces a Credentials object from the details provided
+
+ :param user: a user dict
+ :param project: a project dict
+ :param password: the password as a string
+ :return: a Credentials object with all the available credential details
+ """
+ pass
+
+ def _list_roles(self):
+ roles = self.roles_client.list_roles()['roles']
+ return roles
+
+
+class V2CredsClient(CredsClient):
+ project_id_param = 'tenantId'
+
+ def __init__(self, identity_client, projects_client, users_client,
+ roles_client):
+ super(V2CredsClient, self).__init__(identity_client,
+ projects_client,
+ users_client,
+ roles_client)
+
+ def create_project(self, name, description):
+ tenant = self.projects_client.create_tenant(
+ name=name, description=description)['tenant']
+ return tenant
+
+ def delete_project(self, project_id):
+ self.projects_client.delete_tenant(project_id)
+
+ def get_credentials(self, user, project, password):
+ # User and project already include both ID and name here,
+ # so there's no need to use the fill_in mode
+ return auth.get_credentials(
+ auth_url=None,
+ fill_in=False,
+ identity_version='v2',
+ username=user['name'], user_id=user['id'],
+ tenant_name=project['name'], tenant_id=project['id'],
+ password=password)
+
+
+class V3CredsClient(CredsClient):
+ project_id_param = 'project_id'
+
+ def __init__(self, identity_client, projects_client, users_client,
+ roles_client, domains_client, domain_name):
+ super(V3CredsClient, self).__init__(identity_client, projects_client,
+ users_client, roles_client)
+ self.domains_client = domains_client
+
+ try:
+ # Domain names must be unique, in any case a list is returned,
+ # selecting the first (and only) element
+ self.creds_domain = self.domains_client.list_domains(
+ name=domain_name)['domains'][0]
+ except lib_exc.NotFound:
+ # TODO(andrea) we could probably create the domain on the fly
+ msg = "Requested domain %s could not be found" % domain_name
+ raise lib_exc.InvalidCredentials(msg)
+
+ def create_project(self, name, description):
+ project = self.projects_client.create_project(
+ name=name, description=description,
+ domain_id=self.creds_domain['id'])['project']
+ return project
+
+ def delete_project(self, project_id):
+ self.projects_client.delete_project(project_id)
+
+ def get_credentials(self, user, project, password):
+ # User, project and domain already include both ID and name here,
+ # so there's no need to use the fill_in mode.
+ # NOTE(andreaf) We need to set all fields in the returned credentials.
+ # Scope is then used to pick only those relevant for the type of
+ # token needed by each service client.
+ return auth.get_credentials(
+ auth_url=None,
+ fill_in=False,
+ identity_version='v3',
+ username=user['name'], user_id=user['id'],
+ project_name=project['name'], project_id=project['id'],
+ password=password,
+ project_domain_id=self.creds_domain['id'],
+ project_domain_name=self.creds_domain['name'],
+ domain_id=self.creds_domain['id'],
+ domain_name=self.creds_domain['name'])
+
+ def assign_user_role_on_domain(self, user, role_name, domain=None):
+ """Assign the specified role on a domain
+
+ :param user: a user dict
+ :param role_name: name of the role to be assigned
+ :param domain: (optional) The domain to assign the role on. If not
+ specified the default domain of cred_client
+ """
+ # NOTE(andreaf) This method is very specific to the v3 case, and
+ # because of that it's not defined in the parent class.
+ if domain is None:
+ domain = self.creds_domain
+ role = self._check_role_exists(role_name)
+ if not role:
+ msg = 'No "%s" role found' % role_name
+ raise lib_exc.NotFound(msg)
+ try:
+ self.roles_client.create_user_role_on_domain(
+ domain['id'], user['id'], role['id'])
+ except lib_exc.Conflict:
+ LOG.debug("Role %s already assigned on domain %s for user %s",
+ role['id'], domain['id'], user['id'])
+
+
+def get_creds_client(identity_client,
+ projects_client,
+ users_client,
+ roles_client,
+ domains_client=None,
+ project_domain_name=None):
+ if isinstance(identity_client, v2_identity.IdentityClient):
+ return V2CredsClient(identity_client, projects_client, users_client,
+ roles_client)
+ else:
+ return V3CredsClient(identity_client, projects_client, users_client,
+ roles_client, domains_client, project_domain_name)
diff --git a/tempest/lib/common/cred_provider.py b/tempest/lib/common/cred_provider.py
new file mode 100644
index 0000000..42ed41b
--- /dev/null
+++ b/tempest/lib/common/cred_provider.py
@@ -0,0 +1,107 @@
+# Copyright (c) 2014 Deutsche Telekom AG
+# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
+# 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 abc
+
+import six
+
+from tempest.lib import auth
+from tempest.lib import exceptions
+
+
+@six.add_metaclass(abc.ABCMeta)
+class CredentialProvider(object):
+ def __init__(self, identity_version, name=None,
+ network_resources=None, credentials_domain=None,
+ admin_role=None, identity_uri=None):
+ """A CredentialProvider supplies credentials to test classes.
+
+ :param identity_version: Identity version of the credentials provided
+ :param name: Name of the calling test. Included in provisioned
+ credentials when credentials are provisioned on the fly
+ :param network_resources: Network resources required for the
+ credentials
+ :param credentials_domain: Domain credentials belong to
+ :param admin_role: Name of the role of the admin account
+ :param identity_uri: Identity URI of the target cloud. This *must* be
+ specified for anything to work.
+ """
+ self.identity_version = identity_version
+ self.identity_uri = identity_uri
+ self.name = name or "test_creds"
+ self.network_resources = network_resources
+ self.credentials_domain = credentials_domain or 'Default'
+ self.admin_role = admin_role
+ if not auth.is_identity_version_supported(self.identity_version):
+ raise exceptions.InvalidIdentityVersion(
+ identity_version=self.identity_version)
+
+ @abc.abstractmethod
+ def get_primary_creds(self):
+ return
+
+ @abc.abstractmethod
+ def get_admin_creds(self):
+ return
+
+ @abc.abstractmethod
+ def get_alt_creds(self):
+ return
+
+ @abc.abstractmethod
+ def clear_creds(self):
+ return
+
+ @abc.abstractmethod
+ def is_multi_user(self):
+ return
+
+ @abc.abstractmethod
+ def is_multi_tenant(self):
+ return
+
+ @abc.abstractmethod
+ def get_creds_by_roles(self, roles, force_new=False):
+ return
+
+ @abc.abstractmethod
+ def is_role_available(self, role):
+ return
+
+
+class TestResources(object):
+ """Readonly Credentials, with network resources added."""
+
+ def __init__(self, credentials):
+ self._credentials = credentials
+ self.network = None
+ self.subnet = None
+ self.router = None
+
+ def __getattr__(self, item):
+ return getattr(self._credentials, item)
+
+ def __str__(self):
+ _format = "Credentials: %s, Network: %s, Subnet: %s, Router: %s"
+ return _format % (self._credentials, self.network, self.subnet,
+ self.router)
+
+ def set_resources(self, **kwargs):
+ for key in kwargs:
+ if hasattr(self, key):
+ setattr(self, key, kwargs[key])
+
+ @property
+ def credentials(self):
+ return self._credentials
diff --git a/tempest/lib/common/dynamic_creds.py b/tempest/lib/common/dynamic_creds.py
new file mode 100644
index 0000000..f27e926
--- /dev/null
+++ b/tempest/lib/common/dynamic_creds.py
@@ -0,0 +1,477 @@
+# Copyright 2013 IBM Corp.
+#
+# 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 ipaddress
+
+import netaddr
+from oslo_log import log as logging
+import six
+
+from tempest.lib.common import cred_client
+from tempest.lib.common import cred_provider
+from tempest.lib.common.utils import data_utils
+from tempest.lib import exceptions as lib_exc
+from tempest.lib.services import clients
+
+LOG = logging.getLogger(__name__)
+
+
+class DynamicCredentialProvider(cred_provider.CredentialProvider):
+ """Creates credentials dynamically for tests
+
+ A credential provider that, based on an initial set of
+ admin credentials, creates new credentials on the fly for
+ tests to use and then discard.
+
+ :param str identity_version: identity API version to use `v2` or `v3`
+ :param str admin_role: name of the admin role added to admin users
+ :param str name: names of dynamic resources include this parameter
+ when specified
+ :param str credentials_domain: name of the domain where the users
+ are created. If not defined, the project
+ domain from admin_credentials is used
+ :param dict network_resources: network resources to be created for
+ the created credentials
+ :param Credentials admin_creds: initial admin credentials
+ :param bool identity_admin_domain_scope: Set to true if admin should be
+ scoped to the domain. By
+ default this is False and the
+ admin role is scoped to the
+ project.
+ :param str identity_admin_role: The role name to use for admin
+ :param list extra_roles: A list of strings for extra roles that should
+ be assigned to all created users
+ :param bool neutron_available: Whether we are running in an environemnt
+ with neutron
+ :param bool create_networks: Whether dynamic project networks should be
+ created or not
+ :param project_network_cidr: The CIDR to use for created project
+ networks
+ :param project_network_mask_bits: The network mask bits to use for
+ created project networks
+ :param public_network_id: The id for the public network to use
+ :param identity_admin_endpoint_type: The endpoint type for identity
+ admin clients. Defaults to public.
+ :param identity_uri: Identity URI of the target cloud
+ """
+
+ def __init__(self, identity_version, name=None, network_resources=None,
+ credentials_domain=None, admin_role=None, admin_creds=None,
+ identity_admin_domain_scope=False,
+ identity_admin_role='admin', extra_roles=None,
+ neutron_available=False, create_networks=True,
+ project_network_cidr=None, project_network_mask_bits=None,
+ public_network_id=None, resource_prefix=None,
+ identity_admin_endpoint_type='public', identity_uri=None):
+ super(DynamicCredentialProvider, self).__init__(
+ identity_version=identity_version, identity_uri=identity_uri,
+ admin_role=admin_role, name=name,
+ credentials_domain=credentials_domain,
+ network_resources=network_resources)
+ self.network_resources = network_resources
+ self._creds = {}
+ self.ports = []
+ self.resource_prefix = resource_prefix or ''
+ self.neutron_available = neutron_available
+ self.create_networks = create_networks
+ self.project_network_cidr = project_network_cidr
+ self.project_network_mask_bits = project_network_mask_bits
+ self.public_network_id = public_network_id
+ self.default_admin_creds = admin_creds
+ self.identity_admin_domain_scope = identity_admin_domain_scope
+ self.identity_admin_role = identity_admin_role or 'admin'
+ self.identity_admin_endpoint_type = identity_admin_endpoint_type
+ self.extra_roles = extra_roles or []
+ (self.identity_admin_client,
+ self.tenants_admin_client,
+ self.users_admin_client,
+ self.roles_admin_client,
+ self.domains_admin_client,
+ self.networks_admin_client,
+ self.routers_admin_client,
+ self.subnets_admin_client,
+ self.ports_admin_client,
+ self.security_groups_admin_client) = self._get_admin_clients(
+ identity_admin_endpoint_type)
+ # Domain where isolated credentials are provisioned (v3 only).
+ # Use that of the admin account is None is configured.
+ self.creds_domain_name = None
+ if self.identity_version == 'v3':
+ self.creds_domain_name = (
+ self.default_admin_creds.project_domain_name or
+ self.credentials_domain)
+ self.creds_client = cred_client.get_creds_client(
+ self.identity_admin_client,
+ self.tenants_admin_client,
+ self.users_admin_client,
+ self.roles_admin_client,
+ self.domains_admin_client,
+ self.creds_domain_name)
+
+ def _get_admin_clients(self, endpoint_type):
+ """Returns a tuple with instances of the following admin clients
+
+ (in this order):
+ identity
+ network
+ """
+ os = clients.ServiceClients(self.default_admin_creds,
+ self.identity_uri)
+ params = {'endpoint_type': endpoint_type}
+ if self.identity_version == 'v2':
+ return (os.identity_v2.IdentityClient(**params),
+ os.identity_v2.TenantsClient(**params),
+ os.identity_v2.UsersClient(**params),
+ os.identity_v2.RolesClient(**params), None,
+ os.network.NetworksClient(),
+ os.network.RoutersClient(),
+ os.network.SubnetsClient(),
+ os.network.PortsClient(),
+ os.network.SecurityGroupsClient())
+ else:
+ # We use a dedicated client manager for identity client in case we
+ # need a different token scope for them.
+ scope = 'domain' if self.identity_admin_domain_scope else 'project'
+ identity_os = clients.ServiceClients(self.default_admin_creds,
+ self.identity_uri,
+ scope=scope)
+ return (identity_os.identity_v3.IdentityClient(**params),
+ identity_os.identity_v3.ProjectsClient(**params),
+ identity_os.identity_v3.UsersClient(**params),
+ identity_os.identity_v3.RolesClient(**params),
+ identity_os.identity_v3.DomainsClient(**params),
+ os.network.NetworksClient(),
+ os.network.RoutersClient(),
+ os.network.SubnetsClient(),
+ os.network.PortsClient(),
+ os.network.SecurityGroupsClient())
+
+ def _create_creds(self, admin=False, roles=None):
+ """Create credentials with random name.
+
+ Creates project and user. When admin flag is True create user
+ with admin role. Assign user with additional roles (for example
+ _member_) and roles requested by caller.
+
+ :param admin: Flag if to assign to the user admin role
+ :type admin: bool
+ :param roles: Roles to assign for the user
+ :type roles: list
+ :return: Readonly Credentials with network resources
+ """
+ root = self.name
+
+ project_name = data_utils.rand_name(root, prefix=self.resource_prefix)
+ project_desc = project_name + "-desc"
+ project = self.creds_client.create_project(
+ name=project_name, description=project_desc)
+
+ # NOTE(andreaf) User and project can be distinguished from the context,
+ # having the same ID in both makes it easier to match them and debug.
+ username = project_name
+ user_password = data_utils.rand_password()
+ email = data_utils.rand_name(
+ root, prefix=self.resource_prefix) + "@example.com"
+ user = self.creds_client.create_user(
+ username, user_password, project, email)
+ role_assigned = False
+ if admin:
+ self.creds_client.assign_user_role(user, project, self.admin_role)
+ role_assigned = True
+ if (self.identity_version == 'v3' and
+ self.identity_admin_domain_scope):
+ self.creds_client.assign_user_role_on_domain(
+ user, self.identity_admin_role)
+ # Add roles specified in config file
+ for conf_role in self.extra_roles:
+ self.creds_client.assign_user_role(user, project, conf_role)
+ role_assigned = True
+ # Add roles requested by caller
+ if roles:
+ for role in roles:
+ self.creds_client.assign_user_role(user, project, role)
+ role_assigned = True
+ # NOTE(mtreinish) For a user to have access to a project with v3 auth
+ # it must beassigned a role on the project. So we need to ensure that
+ # our newly created user has a role on the newly created project.
+ if self.identity_version == 'v3' and not role_assigned:
+ try:
+ self.creds_client.create_user_role('Member')
+ except lib_exc.Conflict:
+ LOG.warning('Member role already exists, ignoring conflict.')
+ self.creds_client.assign_user_role(user, project, 'Member')
+
+ creds = self.creds_client.get_credentials(user, project, user_password)
+ return cred_provider.TestResources(creds)
+
+ def _create_network_resources(self, tenant_id):
+ """The function creates network resources in the given tenant.
+
+ The function checks if network_resources class member is empty,
+ In case it is, it will create a network, a subnet and a router for
+ the tenant according to the given tenant id parameter.
+ Otherwise it will create a network resource according
+ to the values from network_resources dict.
+
+ :param tenant_id: The tenant id to create resources for.
+ :type tenant_id: str
+ :raises: InvalidConfiguration, Exception
+ :returns: network resources(network,subnet,router)
+ :rtype: tuple
+ """
+ network = None
+ subnet = None
+ router = None
+ # Make sure settings
+ if self.network_resources:
+ if self.network_resources['router']:
+ if (not self.network_resources['subnet'] or
+ not self.network_resources['network']):
+ raise lib_exc.InvalidConfiguration(
+ 'A router requires a subnet and network')
+ elif self.network_resources['subnet']:
+ if not self.network_resources['network']:
+ raise lib_exc.InvalidConfiguration(
+ 'A subnet requires a network')
+ elif self.network_resources['dhcp']:
+ raise lib_exc.InvalidConfiguration('DHCP requires a subnet')
+
+ rand_name_root = data_utils.rand_name(
+ self.name, prefix=self.resource_prefix)
+ if not self.network_resources or self.network_resources['network']:
+ network_name = rand_name_root + "-network"
+ network = self._create_network(network_name, tenant_id)
+ try:
+ if not self.network_resources or self.network_resources['subnet']:
+ subnet_name = rand_name_root + "-subnet"
+ subnet = self._create_subnet(subnet_name, tenant_id,
+ network['id'])
+ if not self.network_resources or self.network_resources['router']:
+ router_name = rand_name_root + "-router"
+ router = self._create_router(router_name, tenant_id)
+ self._add_router_interface(router['id'], subnet['id'])
+ except Exception:
+ try:
+ if router:
+ self._clear_isolated_router(router['id'], router['name'])
+ if subnet:
+ self._clear_isolated_subnet(subnet['id'], subnet['name'])
+ if network:
+ self._clear_isolated_network(network['id'],
+ network['name'])
+ except Exception as cleanup_exception:
+ msg = "There was an exception trying to setup network " \
+ "resources for tenant %s, and this error happened " \
+ "trying to clean them up: %s"
+ LOG.warning(msg, tenant_id, cleanup_exception)
+ raise
+ return network, subnet, router
+
+ def _create_network(self, name, tenant_id):
+ resp_body = self.networks_admin_client.create_network(
+ name=name, tenant_id=tenant_id)
+ return resp_body['network']
+
+ def _create_subnet(self, subnet_name, tenant_id, network_id):
+ base_cidr = netaddr.IPNetwork(self.project_network_cidr)
+ mask_bits = self.project_network_mask_bits
+ for subnet_cidr in base_cidr.subnet(mask_bits):
+ try:
+ if self.network_resources:
+ resp_body = self.subnets_admin_client.\
+ create_subnet(
+ network_id=network_id, cidr=str(subnet_cidr),
+ name=subnet_name,
+ tenant_id=tenant_id,
+ enable_dhcp=self.network_resources['dhcp'],
+ ip_version=(ipaddress.ip_network(
+ six.text_type(subnet_cidr)).version))
+ else:
+ resp_body = self.subnets_admin_client.\
+ create_subnet(network_id=network_id,
+ cidr=str(subnet_cidr),
+ name=subnet_name,
+ tenant_id=tenant_id,
+ ip_version=(ipaddress.ip_network(
+ six.text_type(subnet_cidr)).version))
+ break
+ except lib_exc.BadRequest as e:
+ if 'overlaps with another subnet' not in str(e):
+ raise
+ else:
+ message = 'Available CIDR for subnet creation could not be found'
+ raise Exception(message)
+ return resp_body['subnet']
+
+ def _create_router(self, router_name, tenant_id):
+ kwargs = {'name': router_name,
+ 'tenant_id': tenant_id}
+ if self.public_network_id:
+ kwargs['external_gateway_info'] = dict(
+ network_id=self.public_network_id)
+ resp_body = self.routers_admin_client.create_router(**kwargs)
+ return resp_body['router']
+
+ def _add_router_interface(self, router_id, subnet_id):
+ self.routers_admin_client.add_router_interface(router_id,
+ subnet_id=subnet_id)
+
+ def get_credentials(self, credential_type):
+ if self._creds.get(str(credential_type)):
+ credentials = self._creds[str(credential_type)]
+ else:
+ if credential_type in ['primary', 'alt', 'admin']:
+ is_admin = (credential_type == 'admin')
+ credentials = self._create_creds(admin=is_admin)
+ else:
+ 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):
+ 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)
+ return credentials
+
+ def get_primary_creds(self):
+ return self.get_credentials('primary')
+
+ def get_admin_creds(self):
+ return self.get_credentials('admin')
+
+ def get_alt_creds(self):
+ return self.get_credentials('alt')
+
+ def get_creds_by_roles(self, roles, force_new=False):
+ roles = list(set(roles))
+ # The roles list as a str will become the index as the dict key for
+ # the created credentials set in the dynamic_creds dict.
+ exist_creds = self._creds.get(str(roles))
+ # If force_new flag is True 2 cred sets with the same roles are needed
+ # handle this by creating a separate index for old one to store it
+ # separately for cleanup
+ if exist_creds and force_new:
+ new_index = str(roles) + '-' + str(len(self._creds))
+ self._creds[new_index] = exist_creds
+ del self._creds[str(roles)]
+ return self.get_credentials(roles)
+
+ def _clear_isolated_router(self, router_id, router_name):
+ client = self.routers_admin_client
+ try:
+ client.delete_router(router_id)
+ except lib_exc.NotFound:
+ LOG.warning('router with name: %s not found for delete',
+ router_name)
+
+ def _clear_isolated_subnet(self, subnet_id, subnet_name):
+ client = self.subnets_admin_client
+ try:
+ client.delete_subnet(subnet_id)
+ except lib_exc.NotFound:
+ LOG.warning('subnet with name: %s not found for delete',
+ subnet_name)
+
+ def _clear_isolated_network(self, network_id, network_name):
+ net_client = self.networks_admin_client
+ try:
+ net_client.delete_network(network_id)
+ except lib_exc.NotFound:
+ LOG.warning('network with name: %s not found for delete',
+ network_name)
+
+ def _cleanup_default_secgroup(self, tenant):
+ nsg_client = self.security_groups_admin_client
+ resp_body = nsg_client.list_security_groups(tenant_id=tenant,
+ name="default")
+ secgroups_to_delete = resp_body['security_groups']
+ for secgroup in secgroups_to_delete:
+ try:
+ nsg_client.delete_security_group(secgroup['id'])
+ except lib_exc.NotFound:
+ LOG.warning('Security group %s, id %s not found for clean-up',
+ secgroup['name'], secgroup['id'])
+
+ def _clear_isolated_net_resources(self):
+ client = self.routers_admin_client
+ for cred in self._creds:
+ creds = self._creds.get(cred)
+ if (not creds or not any([creds.router, creds.network,
+ creds.subnet])):
+ continue
+ LOG.debug("Clearing network: %(network)s, "
+ "subnet: %(subnet)s, router: %(router)s",
+ {'network': creds.network, 'subnet': creds.subnet,
+ 'router': creds.router})
+ if (not self.network_resources or
+ (self.network_resources.get('router') and creds.subnet)):
+ try:
+ client.remove_router_interface(
+ creds.router['id'],
+ subnet_id=creds.subnet['id'])
+ except lib_exc.NotFound:
+ LOG.warning('router with name: %s not found for delete',
+ creds.router['name'])
+ self._clear_isolated_router(creds.router['id'],
+ creds.router['name'])
+ if (not self.network_resources or
+ self.network_resources.get('subnet')):
+ self._clear_isolated_subnet(creds.subnet['id'],
+ creds.subnet['name'])
+ if (not self.network_resources or
+ self.network_resources.get('network')):
+ self._clear_isolated_network(creds.network['id'],
+ creds.network['name'])
+
+ def clear_creds(self):
+ if not self._creds:
+ return
+ self._clear_isolated_net_resources()
+ for creds in six.itervalues(self._creds):
+ try:
+ self.creds_client.delete_user(creds.user_id)
+ except lib_exc.NotFound:
+ LOG.warning("user with name: %s not found for delete",
+ creds.username)
+ # NOTE(zhufl): Only when neutron's security_group ext is
+ # enabled, _cleanup_default_secgroup will not raise error. But
+ # here cannot use test_utils.is_extension_enabled for it will cause
+ # "circular dependency". So here just use try...except to
+ # ensure tenant deletion without big changes.
+ try:
+ if self.neutron_available:
+ self._cleanup_default_secgroup(creds.tenant_id)
+ except lib_exc.NotFound:
+ LOG.warning("failed to cleanup tenant %s's secgroup",
+ creds.tenant_name)
+ try:
+ self.creds_client.delete_project(creds.tenant_id)
+ except lib_exc.NotFound:
+ LOG.warning("tenant with name: %s not found for delete",
+ creds.tenant_name)
+ self._creds = {}
+
+ def is_multi_user(self):
+ return True
+
+ def is_multi_tenant(self):
+ return True
+
+ def is_role_available(self, role):
+ return True
diff --git a/tempest/lib/common/fixed_network.py b/tempest/lib/common/fixed_network.py
new file mode 100644
index 0000000..875a79d
--- /dev/null
+++ b/tempest/lib/common/fixed_network.py
@@ -0,0 +1,132 @@
+# 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 oslo_log import log as logging
+
+from tempest.lib.common.utils import test_utils
+from tempest.lib import exceptions
+
+LOG = logging.getLogger(__name__)
+
+
+def get_network_from_name(name, compute_networks_client):
+ """Get a full network dict from just a network name
+
+ :param str name: the name of the network to use
+ :param NetworksClient compute_networks_client: The network client
+ object to use for making the network lists api request
+ :return: The full dictionary for the network in question
+ :rtype: dict
+ :raises InvalidTestResource: If the name provided is invalid, the networks
+ list returns a 404, there are no found networks, or the found network
+ is invalid
+ """
+ caller = test_utils.find_test_caller()
+
+ if not name:
+ raise exceptions.InvalidTestResource(type='network', name=name)
+
+ networks = compute_networks_client.list_networks()['networks']
+ # NOTE(zhufl) compute networks_client uses 'label' as network name field,
+ # while neutron networks_client uses 'name' as network name field.
+ try:
+ networks = [n for n in networks if n['label'] == name]
+ except KeyError:
+ networks = [n for n in networks if n['name'] == name]
+
+ # Check that a network exists, else raise an InvalidConfigurationException
+ if len(networks) == 1:
+ network = sorted(networks)[0]
+ elif len(networks) > 1:
+ msg = ("Network with name: %s had multiple matching networks in the "
+ "list response: %s\n Unable to specify a single network" % (
+ name, networks))
+ if caller:
+ msg = '(%s) %s' % (caller, msg)
+ LOG.warning(msg)
+ raise exceptions.InvalidTestResource(type='network', name=name)
+ else:
+ msg = "Network with name: %s not found" % name
+ if caller:
+ msg = '(%s) %s' % (caller, msg)
+ LOG.warning(msg)
+ raise exceptions.InvalidTestResource(type='network', name=name)
+ # To be consistent between neutron and nova network always use name even
+ # if label is used in the api response. If neither is present than then
+ # the returned network is invalid.
+ name = network.get('name') or network.get('label')
+ if not name:
+ msg = "Network found from list doesn't contain a valid name or label"
+ if caller:
+ msg = '(%s) %s' % (caller, msg)
+ LOG.warning(msg)
+ raise exceptions.InvalidTestResource(type='network', name=name)
+ network['name'] = name
+ return network
+
+
+def get_tenant_network(creds_provider, compute_networks_client,
+ shared_network_name):
+ """Get a network usable by the primary tenant
+
+ :param creds_provider: instance of credential provider
+ :param compute_networks_client: compute network client. We want to have the
+ compute network client so we can have use a common approach for both
+ neutron and nova-network cases. If this is not an admin network
+ client, set_network_kwargs might fail in case fixed_network_name
+ is the network to be used, and it's not visible to the tenant
+ :param shared_network_name: name of the shared network to be used if no
+ tenant network is available in the creds provider
+ :returns: a dict with 'id' and 'name' of the network
+ """
+ caller = test_utils.find_test_caller()
+ net_creds = creds_provider.get_primary_creds()
+ network = getattr(net_creds, 'network', None)
+ if not network or not network.get('name'):
+ if shared_network_name:
+ msg = ('No valid network provided or created, defaulting to '
+ 'fixed_network_name')
+ if caller:
+ msg = '(%s) %s' % (caller, msg)
+ LOG.debug(msg)
+ try:
+ network = get_network_from_name(shared_network_name,
+ compute_networks_client)
+ except exceptions.InvalidTestResource:
+ network = {}
+ msg = ('Found network %s available for tenant' % network)
+ if caller:
+ msg = '(%s) %s' % (caller, msg)
+ LOG.info(msg)
+ return network
+
+
+def set_networks_kwarg(network, kwargs=None):
+ """Set 'networks' kwargs for a server create if missing
+
+ :param network: dict of network to be used with 'id' and 'name'
+ :param kwargs: server create kwargs to be enhanced
+ :return: new dict of kwargs updated to include networks
+ """
+ params = copy.copy(kwargs) or {}
+ if kwargs and 'networks' in kwargs:
+ return params
+
+ if network:
+ if 'id' in network.keys():
+ params.update({"networks": [{'uuid': network['id']}]})
+ else:
+ LOG.warning('The provided network dict: %s was invalid and did '
+ 'not contain an id', network)
+ return params
diff --git a/tempest/lib/common/http.py b/tempest/lib/common/http.py
new file mode 100644
index 0000000..738c37f
--- /dev/null
+++ b/tempest/lib/common/http.py
@@ -0,0 +1,101 @@
+# Copyright 2016 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import six
+import urllib3
+
+
+class ClosingProxyHttp(urllib3.ProxyManager):
+ def __init__(self, proxy_url, disable_ssl_certificate_validation=False,
+ ca_certs=None, timeout=None):
+ kwargs = {}
+
+ if disable_ssl_certificate_validation:
+ urllib3.disable_warnings()
+ kwargs['cert_reqs'] = 'CERT_NONE'
+ elif ca_certs:
+ kwargs['cert_reqs'] = 'CERT_REQUIRED'
+ kwargs['ca_certs'] = ca_certs
+
+ if timeout:
+ kwargs['timeout'] = timeout
+
+ super(ClosingProxyHttp, self).__init__(proxy_url, **kwargs)
+
+ def request(self, url, method, *args, **kwargs):
+
+ class Response(dict):
+ def __init__(self, info):
+ for key, value in info.getheaders().items():
+ self[key.lower()] = value
+ self.status = info.status
+ self['status'] = str(self.status)
+ self.reason = info.reason
+ self.version = info.version
+ self['content-location'] = url
+
+ original_headers = kwargs.get('headers', {})
+ new_headers = dict(original_headers, connection='close')
+ new_kwargs = dict(kwargs, headers=new_headers)
+
+ # Follow up to 5 redirections. Don't raise an exception if
+ # it's exceeded but return the HTTP 3XX response instead.
+ retry = urllib3.util.Retry(raise_on_redirect=False, redirect=5)
+ r = super(ClosingProxyHttp, self).request(method, url, retries=retry,
+ *args, **new_kwargs)
+ return Response(r), r.data
+
+
+class ClosingHttp(urllib3.poolmanager.PoolManager):
+ def __init__(self, disable_ssl_certificate_validation=False,
+ ca_certs=None, timeout=None):
+ kwargs = {}
+
+ if disable_ssl_certificate_validation:
+ urllib3.disable_warnings()
+ kwargs['cert_reqs'] = 'CERT_NONE'
+ elif ca_certs:
+ kwargs['cert_reqs'] = 'CERT_REQUIRED'
+ kwargs['ca_certs'] = ca_certs
+
+ if timeout:
+ kwargs['timeout'] = timeout
+
+ super(ClosingHttp, self).__init__(**kwargs)
+
+ def request(self, url, method, *args, **kwargs):
+
+ class Response(dict):
+ def __init__(self, info):
+ for key, value in info.getheaders().items():
+ # We assume HTTP header name to be string, not random
+ # bytes, thus ensure we have string keys.
+ self[six.u(key).lower()] = value
+ self.status = info.status
+ self['status'] = str(self.status)
+ self.reason = info.reason
+ self.version = info.version
+ self['content-location'] = url
+
+ original_headers = kwargs.get('headers', {})
+ new_headers = dict(original_headers, connection='close')
+ new_kwargs = dict(kwargs, headers=new_headers)
+
+ # Follow up to 5 redirections. Don't raise an exception if
+ # it's exceeded but return the HTTP 3XX response instead.
+ retry = urllib3.util.Retry(raise_on_redirect=False, redirect=5)
+ r = super(ClosingHttp, self).request(method, url, retries=retry,
+ *args, **new_kwargs)
+ return Response(r), r.data
diff --git a/tempest/lib/common/jsonschema_validator.py b/tempest/lib/common/jsonschema_validator.py
new file mode 100644
index 0000000..9a35b76
--- /dev/null
+++ b/tempest/lib/common/jsonschema_validator.py
@@ -0,0 +1,57 @@
+# Copyright 2016 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.
+import base64
+
+import jsonschema
+from oslo_utils import timeutils
+import six
+
+# JSON Schema validator and format checker used for JSON Schema validation
+JSONSCHEMA_VALIDATOR = jsonschema.Draft4Validator
+FORMAT_CHECKER = jsonschema.draft4_format_checker
+
+
+# NOTE(gmann): Add customized format checker for 'date-time' format because:
+# 1. jsonschema needs strict_rfc3339 or isodate module to be installed
+# for proper date-time checking as per rfc3339.
+# 2. Nova or other OpenStack components handle the date time format as
+# ISO 8601 which is defined in oslo_utils.timeutils
+# so this checker will validate the date-time as defined in
+# oslo_utils.timeutils
+@FORMAT_CHECKER.checks('iso8601-date-time')
+def _validate_datetime_format(instance):
+ try:
+ if isinstance(instance, jsonschema.compat.str_types):
+ timeutils.parse_isotime(instance)
+ except ValueError:
+ return False
+ else:
+ return True
+
+
+@jsonschema.FormatChecker.cls_checks('base64')
+def _validate_base64_format(instance):
+ try:
+ if isinstance(instance, six.text_type):
+ instance = instance.encode('utf-8')
+ base64.decodestring(instance)
+ except base64.binascii.Error:
+ return False
+ except TypeError:
+ # The name must be string type. If instance isn't string type, the
+ # TypeError will be raised at here.
+ return False
+
+ return True
diff --git a/tempest/lib/common/preprov_creds.py b/tempest/lib/common/preprov_creds.py
new file mode 100644
index 0000000..fcdeb17
--- /dev/null
+++ b/tempest/lib/common/preprov_creds.py
@@ -0,0 +1,374 @@
+# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
+#
+# 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 hashlib
+import os
+
+from oslo_concurrency import lockutils
+from oslo_log import log as logging
+import six
+import yaml
+
+from tempest.lib import auth
+from tempest.lib.common import cred_provider
+from tempest.lib.common import fixed_network
+from tempest.lib import exceptions as lib_exc
+from tempest.lib.services import clients
+
+LOG = logging.getLogger(__name__)
+
+
+def read_accounts_yaml(path):
+ try:
+ with open(path, 'r') as yaml_file:
+ accounts = yaml.safe_load(yaml_file)
+ except IOError:
+ raise lib_exc.InvalidConfiguration(
+ 'The path for the test accounts file: %s '
+ 'could not be found' % path)
+ return accounts
+
+
+class PreProvisionedCredentialProvider(cred_provider.CredentialProvider):
+ """Credentials provider using pre-provisioned accounts
+
+ This credentials provider loads the details of pre-provisioned
+ accounts from a YAML file, in the format specified by
+ ``etc/accounts.yaml.sample``. It locks accounts while in use, using the
+ external locking mechanism, allowing for multiple python processes
+ to share a single account file, and thus running tests in parallel.
+
+ The accounts_lock_dir must be generated using `lockutils.get_lock_path`
+ from the oslo.concurrency library. For instance::
+
+ accounts_lock_dir = os.path.join(lockutils.get_lock_path(CONF),
+ 'test_accounts')
+
+ Role names for object storage are optional as long as the
+ `operator` and `reseller_admin` credential types are not used in the
+ accounts file.
+
+ :param identity_version: identity version of the credentials
+ :param admin_role: name of the admin role
+ :param test_accounts_file: path to the accounts YAML file
+ :param accounts_lock_dir: the directory for external locking
+ :param name: name of the hash file (optional)
+ :param credentials_domain: name of the domain credentials belong to
+ (if no domain is configured)
+ :param object_storage_operator_role: name of the role
+ :param object_storage_reseller_admin_role: name of the role
+ :param identity_uri: Identity URI of the target cloud
+ """
+
+ # Exclude from the hash fields specific to v2 or v3 identity API
+ # i.e. only include user*, project*, tenant* and password
+ HASH_CRED_FIELDS = (set(auth.KeystoneV2Credentials.ATTRIBUTES) &
+ set(auth.KeystoneV3Credentials.ATTRIBUTES))
+
+ def __init__(self, identity_version, test_accounts_file,
+ accounts_lock_dir, name=None, credentials_domain=None,
+ admin_role=None, object_storage_operator_role=None,
+ object_storage_reseller_admin_role=None, identity_uri=None):
+ super(PreProvisionedCredentialProvider, self).__init__(
+ identity_version=identity_version, name=name,
+ admin_role=admin_role, credentials_domain=credentials_domain,
+ identity_uri=identity_uri)
+ self.test_accounts_file = test_accounts_file
+ if test_accounts_file:
+ accounts = read_accounts_yaml(self.test_accounts_file)
+ else:
+ raise lib_exc.InvalidCredentials("No accounts file specified")
+ self.hash_dict = self.get_hash_dict(
+ accounts, admin_role, object_storage_operator_role,
+ object_storage_reseller_admin_role)
+ self.accounts_dir = accounts_lock_dir
+ self._creds = {}
+
+ @classmethod
+ def _append_role(cls, role, account_hash, hash_dict):
+ if role in hash_dict['roles']:
+ hash_dict['roles'][role].append(account_hash)
+ else:
+ hash_dict['roles'][role] = [account_hash]
+ return hash_dict
+
+ @classmethod
+ def get_hash_dict(cls, accounts, admin_role,
+ object_storage_operator_role=None,
+ object_storage_reseller_admin_role=None):
+ hash_dict = {'roles': {}, 'creds': {}, 'networks': {}}
+
+ # Loop over the accounts read from the yaml file
+ for account in accounts:
+ roles = []
+ types = []
+ resources = []
+ if 'roles' in account:
+ roles = account.pop('roles')
+ if 'types' in account:
+ types = account.pop('types')
+ if 'resources' in account:
+ resources = account.pop('resources')
+ temp_hash = hashlib.md5()
+ account_for_hash = dict((k, v) for (k, v) in account.items()
+ if k in cls.HASH_CRED_FIELDS)
+ temp_hash.update(six.text_type(account_for_hash).encode('utf-8'))
+ temp_hash_key = temp_hash.hexdigest()
+ hash_dict['creds'][temp_hash_key] = account
+ for role in roles:
+ hash_dict = cls._append_role(role, temp_hash_key,
+ hash_dict)
+ # If types are set for the account append the matching role
+ # subdict with the hash
+ for type in types:
+ if type == 'admin':
+ hash_dict = cls._append_role(admin_role, temp_hash_key,
+ hash_dict)
+ elif type == 'operator':
+ if object_storage_operator_role:
+ hash_dict = cls._append_role(
+ object_storage_operator_role, temp_hash_key,
+ hash_dict)
+ else:
+ msg = ("Type 'operator' configured, but no "
+ "object_storage_operator_role specified")
+ raise lib_exc.InvalidCredentials(msg)
+ elif type == 'reseller_admin':
+ if object_storage_reseller_admin_role:
+ hash_dict = cls._append_role(
+ object_storage_reseller_admin_role,
+ temp_hash_key,
+ hash_dict)
+ else:
+ msg = ("Type 'reseller_admin' configured, but no "
+ "object_storage_reseller_admin_role specified")
+ raise lib_exc.InvalidCredentials(msg)
+ # Populate the network subdict
+ for resource in resources:
+ if resource == 'network':
+ hash_dict['networks'][temp_hash_key] = resources[resource]
+ else:
+ LOG.warning(
+ 'Unknown resource type %s, ignoring this field',
+ resource
+ )
+ return hash_dict
+
+ def is_multi_user(self):
+ return len(self.hash_dict['creds']) > 1
+
+ def is_multi_tenant(self):
+ return self.is_multi_user()
+
+ def _create_hash_file(self, hash_string):
+ path = os.path.join(os.path.join(self.accounts_dir, hash_string))
+ if not os.path.isfile(path):
+ with open(path, 'w') as fd:
+ fd.write(self.name)
+ return True
+ return False
+
+ @lockutils.synchronized('test_accounts_io', external=True)
+ def _get_free_hash(self, hashes):
+ # Cast as a list because in some edge cases a set will be passed in
+ hashes = list(hashes)
+ if not os.path.isdir(self.accounts_dir):
+ os.mkdir(self.accounts_dir)
+ # Create File from first hash (since none are in use)
+ self._create_hash_file(hashes[0])
+ return hashes[0]
+ names = []
+ for _hash in hashes:
+ res = self._create_hash_file(_hash)
+ if res:
+ return _hash
+ else:
+ path = os.path.join(os.path.join(self.accounts_dir,
+ _hash))
+ with open(path, 'r') as fd:
+ names.append(fd.read())
+ msg = ('Insufficient number of users provided. %s have allocated all '
+ 'the credentials for this allocation request' % ','.join(names))
+ raise lib_exc.InvalidCredentials(msg)
+
+ def _get_match_hash_list(self, roles=None):
+ hashes = []
+ if roles:
+ # Loop over all the creds for each role in the subdict and generate
+ # a list of cred lists for each role
+ for role in roles:
+ temp_hashes = self.hash_dict['roles'].get(role, None)
+ if not temp_hashes:
+ raise lib_exc.InvalidCredentials(
+ "No credentials with role: %s specified in the "
+ "accounts ""file" % role)
+ hashes.append(temp_hashes)
+ # Take the list of lists and do a boolean and between each list to
+ # find the creds which fall under all the specified roles
+ temp_list = set(hashes[0])
+ for hash_list in hashes[1:]:
+ temp_list = temp_list & set(hash_list)
+ hashes = temp_list
+ else:
+ hashes = self.hash_dict['creds'].keys()
+ # NOTE(mtreinish): admin is a special case because of the increased
+ # privilege set which could potentially cause issues on tests where
+ # that is not expected. So unless the admin role isn't specified do
+ # not allocate admin.
+ admin_hashes = self.hash_dict['roles'].get(self.admin_role,
+ None)
+ if ((not roles or self.admin_role not in roles) and
+ admin_hashes):
+ useable_hashes = [x for x in hashes if x not in admin_hashes]
+ else:
+ useable_hashes = hashes
+ return useable_hashes
+
+ def _sanitize_creds(self, creds):
+ temp_creds = creds.copy()
+ temp_creds.pop('password')
+ return temp_creds
+
+ def _get_creds(self, roles=None):
+ useable_hashes = self._get_match_hash_list(roles)
+ if not useable_hashes:
+ msg = 'No users configured for type/roles %s' % roles
+ raise lib_exc.InvalidCredentials(msg)
+ free_hash = self._get_free_hash(useable_hashes)
+ clean_creds = self._sanitize_creds(
+ self.hash_dict['creds'][free_hash])
+ LOG.info('%s allocated creds:\n%s', self.name, clean_creds)
+ return self._wrap_creds_with_network(free_hash)
+
+ @lockutils.synchronized('test_accounts_io', external=True)
+ def remove_hash(self, hash_string):
+ hash_path = os.path.join(self.accounts_dir, hash_string)
+ if not os.path.isfile(hash_path):
+ LOG.warning('Expected an account lock file %s to remove, but '
+ 'one did not exist', hash_path)
+ else:
+ os.remove(hash_path)
+ if not os.listdir(self.accounts_dir):
+ os.rmdir(self.accounts_dir)
+
+ def get_hash(self, creds):
+ for _hash in self.hash_dict['creds']:
+ # Comparing on the attributes that are expected in the YAML
+ init_attributes = creds.get_init_attributes()
+ # Only use the attributes initially used to calculate the hash
+ init_attributes = [x for x in init_attributes if
+ x in self.HASH_CRED_FIELDS]
+ hash_attributes = self.hash_dict['creds'][_hash].copy()
+ # NOTE(andreaf) Not all fields may be available on all credentials
+ # so defaulting to None for that case.
+ if all([getattr(creds, k, None) == hash_attributes.get(k, None) for
+ k in init_attributes]):
+ return _hash
+ raise AttributeError('Invalid credentials %s' % creds)
+
+ def remove_credentials(self, creds):
+ _hash = self.get_hash(creds)
+ clean_creds = self._sanitize_creds(self.hash_dict['creds'][_hash])
+ self.remove_hash(_hash)
+ LOG.info("%s returned allocated creds:\n%s", self.name, clean_creds)
+
+ def get_primary_creds(self):
+ if self._creds.get('primary'):
+ return self._creds.get('primary')
+ net_creds = self._get_creds()
+ self._creds['primary'] = net_creds
+ return net_creds
+
+ def get_alt_creds(self):
+ if self._creds.get('alt'):
+ return self._creds.get('alt')
+ net_creds = self._get_creds()
+ self._creds['alt'] = net_creds
+ return net_creds
+
+ def get_creds_by_roles(self, roles, force_new=False):
+ roles = list(set(roles))
+ exist_creds = self._creds.get(six.text_type(roles).encode(
+ 'utf-8'), None)
+ # The force kwarg is used to allocate an additional set of creds with
+ # the same role list. The index used for the previously allocation
+ # in the _creds dict will be moved.
+ if exist_creds and not force_new:
+ return exist_creds
+ elif exist_creds and force_new:
+ # NOTE(andreaf) In py3.x encode returns bytes, and b'' is bytes
+ # In py2.7 encode returns strings, and b'' is still string
+ new_index = six.text_type(roles).encode('utf-8') + b'-' + \
+ six.text_type(len(self._creds)).encode('utf-8')
+ self._creds[new_index] = exist_creds
+ net_creds = self._get_creds(roles=roles)
+ self._creds[six.text_type(roles).encode('utf-8')] = net_creds
+ return net_creds
+
+ def clear_creds(self):
+ for creds in self._creds.values():
+ self.remove_credentials(creds)
+
+ def get_admin_creds(self):
+ return self.get_creds_by_roles([self.admin_role])
+
+ def is_role_available(self, role):
+ if self.hash_dict['roles'].get(role):
+ return True
+ return False
+
+ def admin_available(self):
+ return self.is_role_available(self.admin_role)
+
+ def _wrap_creds_with_network(self, hash):
+ creds_dict = self.hash_dict['creds'][hash]
+ # Make sure a domain scope if defined for users in case of V3
+ # Make sure a tenant is available in case of V2
+ creds_dict = self._extend_credentials(creds_dict)
+ # This just builds a Credentials object, it does not validate
+ # nor fill with missing fields.
+ credential = auth.get_credentials(
+ auth_url=None, fill_in=False,
+ identity_version=self.identity_version, **creds_dict)
+ net_creds = cred_provider.TestResources(credential)
+ net_clients = clients.ServiceClients(credentials=credential,
+ identity_uri=self.identity_uri)
+ networks_client = net_clients.network.NetworksClient()
+ net_name = self.hash_dict['networks'].get(hash, None)
+ try:
+ network = fixed_network.get_network_from_name(
+ net_name, networks_client)
+ except lib_exc.InvalidTestResource:
+ network = {}
+ net_creds.set_resources(network=network)
+ return net_creds
+
+ def _extend_credentials(self, creds_dict):
+ # Add or remove credential domain fields to fit the identity version
+ domain_fields = set(x for x in auth.KeystoneV3Credentials.ATTRIBUTES
+ if 'domain' in x)
+ msg = 'Assuming they are valid in the default domain.'
+ if self.identity_version == 'v3':
+ if not domain_fields.intersection(set(creds_dict.keys())):
+ msg = 'Using credentials %s for v3 API calls. ' + msg
+ LOG.warning(msg, self._sanitize_creds(creds_dict))
+ creds_dict['domain_name'] = self.credentials_domain
+ if self.identity_version == 'v2':
+ if domain_fields.intersection(set(creds_dict.keys())):
+ msg = 'Using credentials %s for v2 API calls. ' + msg
+ LOG.warning(msg, self._sanitize_creds(creds_dict))
+ # Remove all valid domain attributes
+ for attr in domain_fields.intersection(set(creds_dict.keys())):
+ creds_dict.pop(attr)
+ return creds_dict
diff --git a/tempest/lib/common/rest_client.py b/tempest/lib/common/rest_client.py
new file mode 100644
index 0000000..22276d4
--- /dev/null
+++ b/tempest/lib/common/rest_client.py
@@ -0,0 +1,985 @@
+# Copyright 2012 OpenStack Foundation
+# Copyright 2013 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.
+
+import collections
+import email.utils
+import re
+import time
+
+import jsonschema
+from oslo_log import log as logging
+from oslo_serialization import jsonutils as json
+import six
+from six.moves import urllib
+
+from tempest.lib.common import http
+from tempest.lib.common import jsonschema_validator
+from tempest.lib.common.utils import test_utils
+from tempest.lib import exceptions
+
+# redrive rate limited calls at most twice
+MAX_RECURSION_DEPTH = 2
+
+# All the successful HTTP status codes from RFC 7231 & 4918
+HTTP_SUCCESS = (200, 201, 202, 203, 204, 205, 206, 207)
+
+# All the redirection HTTP status codes from RFC 7231 & 4918
+HTTP_REDIRECTION = (300, 301, 302, 303, 304, 305, 306, 307)
+
+# JSON Schema validator and format checker used for JSON Schema validation
+JSONSCHEMA_VALIDATOR = jsonschema_validator.JSONSCHEMA_VALIDATOR
+FORMAT_CHECKER = jsonschema_validator.FORMAT_CHECKER
+
+
+class RestClient(object):
+ """Unified OpenStack RestClient class
+
+ This class is used for building openstack api clients on top of. It is
+ intended to provide a base layer for wrapping outgoing http requests in
+ keystone auth as well as providing response code checking and error
+ handling.
+
+ :param auth_provider: an auth provider object used to wrap requests in auth
+ :param str service: The service name to use for the catalog lookup
+ :param str region: The region to use for the catalog lookup
+ :param str name: The endpoint name to use for the catalog lookup; this
+ returns only if the service exists
+ :param str endpoint_type: The endpoint type to use for the catalog lookup
+ :param int build_interval: Time in seconds between to status checks in
+ wait loops
+ :param int build_timeout: Timeout in seconds to wait for a wait operation.
+ :param bool disable_ssl_certificate_validation: Set to true to disable ssl
+ certificate validation
+ :param str ca_certs: File containing the CA Bundle to use in verifying a
+ TLS server cert
+ :param str trace_requests: Regex to use for specifying logging the entirety
+ of the request and response payload
+ :param str http_timeout: Timeout in seconds to wait for the http request to
+ return
+ :param str proxy_url: http proxy url to use.
+ """
+
+ # The version of the API this client implements
+ api_version = None
+
+ LOG = logging.getLogger(__name__)
+
+ def __init__(self, auth_provider, service, region,
+ endpoint_type='publicURL',
+ build_interval=1, build_timeout=60,
+ disable_ssl_certificate_validation=False, ca_certs=None,
+ trace_requests='', name=None, http_timeout=None,
+ proxy_url=None):
+ self.auth_provider = auth_provider
+ self.service = service
+ self.region = region
+ self.name = name
+ self.endpoint_type = endpoint_type
+ self.build_interval = build_interval
+ self.build_timeout = build_timeout
+ self.trace_requests = trace_requests
+
+ self._skip_path = False
+ self.general_header_lc = set(('cache-control', 'connection',
+ 'date', 'pragma', 'trailer',
+ 'transfer-encoding', 'via',
+ 'warning'))
+ self.response_header_lc = set(('accept-ranges', 'age', 'etag',
+ 'location', 'proxy-authenticate',
+ 'retry-after', 'server',
+ 'vary', 'www-authenticate'))
+ dscv = disable_ssl_certificate_validation
+
+ if proxy_url:
+ self.http_obj = http.ClosingProxyHttp(
+ proxy_url,
+ disable_ssl_certificate_validation=dscv, ca_certs=ca_certs,
+ timeout=http_timeout)
+ else:
+ self.http_obj = http.ClosingHttp(
+ disable_ssl_certificate_validation=dscv, ca_certs=ca_certs,
+ timeout=http_timeout)
+
+ def get_headers(self, accept_type=None, send_type=None):
+ """Return the default headers which will be used with outgoing requests
+
+ :param str accept_type: The media type to use for the Accept header, if
+ one isn't provided the object var TYPE will be
+ used
+ :param str send_type: The media-type to use for the Content-Type
+ header, if one isn't provided the object var
+ TYPE will be used
+ :rtype: dict
+ :return: The dictionary of headers which can be used in the headers
+ dict for outgoing request
+ """
+ if accept_type is None:
+ accept_type = 'json'
+ if send_type is None:
+ send_type = 'json'
+ return {'Content-Type': 'application/%s' % send_type,
+ 'Accept': 'application/%s' % accept_type}
+
+ def __str__(self):
+ STRING_LIMIT = 80
+ str_format = ("service:%s, base_url:%s, "
+ "filters: %s, build_interval:%s, build_timeout:%s"
+ "\ntoken:%s..., \nheaders:%s...")
+ return str_format % (self.service, self.base_url,
+ self.filters, self.build_interval,
+ self.build_timeout,
+ str(self.token)[0:STRING_LIMIT],
+ str(self.get_headers())[0:STRING_LIMIT])
+
+ @property
+ def user(self):
+ """The username used for requests
+
+ :rtype: string
+ :return: The username being used for requests
+ """
+
+ return self.auth_provider.credentials.username
+
+ @property
+ def user_id(self):
+ """The user_id used for requests
+
+ :rtype: string
+ :return: The user id being used for requests
+ """
+ return self.auth_provider.credentials.user_id
+
+ @property
+ def tenant_name(self):
+ """The tenant/project being used for requests
+
+ :rtype: string
+ :return: The tenant/project name being used for requests
+ """
+ return self.auth_provider.credentials.tenant_name
+
+ @property
+ def tenant_id(self):
+ """The tenant/project id being used for requests
+
+ :rtype: string
+ :return: The tenant/project id being used for requests
+ """
+ return self.auth_provider.credentials.tenant_id
+
+ @property
+ def password(self):
+ """The password being used for requests
+
+ :rtype: string
+ :return: The password being used for requests
+ """
+ return self.auth_provider.credentials.password
+
+ @property
+ def base_url(self):
+ return self.auth_provider.base_url(filters=self.filters)
+
+ @property
+ def token(self):
+ return self.auth_provider.get_token()
+
+ @property
+ def filters(self):
+ _filters = dict(
+ service=self.service,
+ endpoint_type=self.endpoint_type,
+ region=self.region,
+ name=self.name
+ )
+ if self.api_version is not None:
+ _filters['api_version'] = self.api_version
+ if self._skip_path:
+ _filters['skip_path'] = self._skip_path
+ return _filters
+
+ def skip_path(self):
+ """When set, ignore the path part of the base URL from the catalog"""
+ self._skip_path = True
+
+ def reset_path(self):
+ """When reset, use the base URL from the catalog as-is"""
+ self._skip_path = False
+
+ @classmethod
+ def expected_success(cls, expected_code, read_code):
+ """Check expected success response code against the http response
+
+ :param int expected_code: The response code that is expected.
+ Optionally a list of integers can be used
+ to specify multiple valid success codes
+ :param int read_code: The response code which was returned in the
+ response
+ :raises AssertionError: if the expected_code isn't a valid http success
+ response code
+ :raises exceptions.InvalidHttpSuccessCode: if the read code isn't an
+ expected http success code
+ """
+ if not isinstance(read_code, int):
+ raise TypeError("'read_code' must be an int instead of (%s)"
+ % type(read_code))
+
+ assert_msg = ("This function only allowed to use for HTTP status "
+ "codes which explicitly defined in the RFC 7231 & 4918. "
+ "{0} is not a defined Success Code!"
+ ).format(expected_code)
+ if isinstance(expected_code, list):
+ for code in expected_code:
+ assert code in HTTP_SUCCESS + HTTP_REDIRECTION, assert_msg
+ else:
+ assert expected_code in HTTP_SUCCESS + HTTP_REDIRECTION, assert_msg
+
+ # NOTE(afazekas): the http status code above 400 is processed by
+ # the _error_checker method
+ if read_code < 400:
+ pattern = ("Unexpected http success status code {0}, "
+ "The expected status code is {1}")
+ if ((not isinstance(expected_code, list) and
+ (read_code != expected_code)) or
+ (isinstance(expected_code, list) and
+ (read_code not in expected_code))):
+ details = pattern.format(read_code, expected_code)
+ raise exceptions.InvalidHttpSuccessCode(details)
+
+ def post(self, url, body, headers=None, extra_headers=False,
+ chunked=False):
+ """Send a HTTP POST request using keystone auth
+
+ :param str url: the relative url to send the post request to
+ :param dict body: the request body
+ :param dict headers: The headers to use for the request
+ :param bool extra_headers: Boolean value than indicates if the headers
+ returned by the get_headers() method are to
+ be used but additional headers are needed in
+ the request pass them in as a dict.
+ :param bool chunked: sends the body with chunked encoding
+ :return: a tuple with the first entry containing the response headers
+ and the second the response body
+ :rtype: tuple
+ """
+ return self.request('POST', url, extra_headers, headers, body, chunked)
+
+ def get(self, url, headers=None, extra_headers=False):
+ """Send a HTTP GET request using keystone service catalog and auth
+
+ :param str url: the relative url to send the post request to
+ :param dict headers: The headers to use for the request
+ :param bool extra_headers: Boolean value than indicates if the headers
+ returned by the get_headers() method are to
+ be used but additional headers are needed in
+ the request pass them in as a dict.
+ :return: a tuple with the first entry containing the response headers
+ and the second the response body
+ :rtype: tuple
+ """
+ return self.request('GET', url, extra_headers, headers)
+
+ def delete(self, url, headers=None, body=None, extra_headers=False):
+ """Send a HTTP DELETE request using keystone service catalog and auth
+
+ :param str url: the relative url to send the post request to
+ :param dict headers: The headers to use for the request
+ :param dict body: the request body
+ :param bool extra_headers: Boolean value than indicates if the headers
+ returned by the get_headers() method are to
+ be used but additional headers are needed in
+ the request pass them in as a dict.
+ :return: a tuple with the first entry containing the response headers
+ and the second the response body
+ :rtype: tuple
+ """
+ return self.request('DELETE', url, extra_headers, headers, body)
+
+ def patch(self, url, body, headers=None, extra_headers=False):
+ """Send a HTTP PATCH request using keystone service catalog and auth
+
+ :param str url: the relative url to send the post request to
+ :param dict body: the request body
+ :param dict headers: The headers to use for the request
+ :param bool extra_headers: Boolean value than indicates if the headers
+ returned by the get_headers() method are to
+ be used but additional headers are needed in
+ the request pass them in as a dict.
+ :return: a tuple with the first entry containing the response headers
+ and the second the response body
+ :rtype: tuple
+ """
+ return self.request('PATCH', url, extra_headers, headers, body)
+
+ def put(self, url, body, headers=None, extra_headers=False, chunked=False):
+ """Send a HTTP PUT request using keystone service catalog and auth
+
+ :param str url: the relative url to send the post request to
+ :param dict body: the request body
+ :param dict headers: The headers to use for the request
+ :param bool extra_headers: Boolean value than indicates if the headers
+ returned by the get_headers() method are to
+ be used but additional headers are needed in
+ the request pass them in as a dict.
+ :param bool chunked: sends the body with chunked encoding
+ :return: a tuple with the first entry containing the response headers
+ and the second the response body
+ :rtype: tuple
+ """
+ return self.request('PUT', url, extra_headers, headers, body, chunked)
+
+ def head(self, url, headers=None, extra_headers=False):
+ """Send a HTTP HEAD request using keystone service catalog and auth
+
+ :param str url: the relative url to send the post request to
+ :param dict headers: The headers to use for the request
+ :param bool extra_headers: Boolean value than indicates if the headers
+ returned by the get_headers() method are to
+ be used but additional headers are needed in
+ the request pass them in as a dict.
+ :return: a tuple with the first entry containing the response headers
+ and the second the response body
+ :rtype: tuple
+ """
+ return self.request('HEAD', url, extra_headers, headers)
+
+ def copy(self, url, headers=None, extra_headers=False):
+ """Send a HTTP COPY request using keystone service catalog and auth
+
+ :param str url: the relative url to send the post request to
+ :param dict headers: The headers to use for the request
+ :param bool extra_headers: Boolean value than indicates if the headers
+ returned by the get_headers() method are to
+ be used but additional headers are needed in
+ the request pass them in as a dict.
+ :return: a tuple with the first entry containing the response headers
+ and the second the response body
+ :rtype: tuple
+ """
+ return self.request('COPY', url, extra_headers, headers)
+
+ def get_versions(self):
+ """Get the versions on a endpoint from the keystone catalog
+
+ This method will make a GET request on the baseurl from the keystone
+ catalog to return a list of API versions. It is expected that a GET
+ on the endpoint in the catalog will return a list of supported API
+ versions.
+
+ :return: tuple with response headers and list of version numbers
+ :rtype: tuple
+ """
+ resp, body = self.get('')
+ body = self._parse_resp(body)
+ versions = map(lambda x: x['id'], body)
+ return resp, versions
+
+ def _get_request_id(self, resp):
+ for i in ('x-openstack-request-id', 'x-compute-request-id'):
+ if i in resp:
+ return resp[i]
+ return ""
+
+ def _safe_body(self, body, maxlen=4096):
+ # convert a structure into a string safely
+ try:
+ text = six.text_type(body)
+ except UnicodeDecodeError:
+ # if this isn't actually text, return marker that
+ return "<BinaryData: removed>"
+ if len(text) > maxlen:
+ return text[:maxlen]
+ else:
+ return text
+
+ def _log_request_start(self, method, req_url):
+ caller_name = test_utils.find_test_caller()
+ if self.trace_requests and re.search(self.trace_requests, caller_name):
+ self.LOG.debug('Starting Request (%s): %s %s', caller_name,
+ method, req_url)
+
+ def _log_request_full(self, resp, req_headers=None, req_body=None,
+ resp_body=None, extra=None):
+ if 'X-Auth-Token' in req_headers:
+ req_headers['X-Auth-Token'] = '<omitted>'
+ # A shallow copy is sufficient
+ resp_log = resp.copy()
+ if 'x-subject-token' in resp_log:
+ resp_log['x-subject-token'] = '<omitted>'
+ log_fmt = """Request - Headers: %s
+ Body: %s
+ Response - Headers: %s
+ Body: %s"""
+
+ self.LOG.debug(
+ log_fmt,
+ str(req_headers),
+ self._safe_body(req_body),
+ str(resp_log),
+ self._safe_body(resp_body),
+ extra=extra)
+
+ def _log_request(self, method, req_url, resp,
+ secs="", req_headers=None,
+ req_body=None, resp_body=None):
+ if req_headers is None:
+ req_headers = {}
+ # if we have the request id, put it in the right part of the log
+ extra = dict(request_id=self._get_request_id(resp))
+ # NOTE(sdague): while we still have 6 callers to this function
+ # we're going to just provide work around on who is actually
+ # providing timings by gracefully adding no content if they don't.
+ # Once we're down to 1 caller, clean this up.
+ caller_name = test_utils.find_test_caller()
+ if secs:
+ secs = " %.3fs" % secs
+ self.LOG.info(
+ 'Request (%s): %s %s %s%s',
+ caller_name,
+ resp['status'],
+ method,
+ req_url,
+ secs,
+ extra=extra)
+
+ # Also look everything at DEBUG if you want to filter this
+ # out, don't run at debug.
+ if self.LOG.isEnabledFor(logging.DEBUG):
+ self._log_request_full(resp, req_headers, req_body,
+ resp_body, extra)
+
+ def _parse_resp(self, body):
+ try:
+ body = json.loads(body)
+ except ValueError:
+ return body
+
+ # We assume, that if the first value of the deserialized body's
+ # item set is a dict or a list, that we just return the first value
+ # of deserialized body.
+ # Essentially "cutting out" the first placeholder element in a body
+ # that looks like this:
+ #
+ # {
+ # "users": [
+ # ...
+ # ]
+ # }
+ try:
+ # Ensure there are not more than one top-level keys
+ # NOTE(freerunner): Ensure, that JSON is not nullable to
+ # to prevent StopIteration Exception
+ if not hasattr(body, "keys") or len(body.keys()) != 1:
+ return body
+ # Just return the "wrapped" element
+ first_key, first_item = six.next(six.iteritems(body))
+ if isinstance(first_item, (dict, list)):
+ return first_item
+ except (ValueError, IndexError):
+ pass
+ return body
+
+ def response_checker(self, method, resp, resp_body):
+ """A sanity check on the response from a HTTP request
+
+ This method does a sanity check on whether the response from an HTTP
+ request conforms the HTTP RFC.
+
+ :param str method: The HTTP verb of the request associated with the
+ response being passed in.
+ :param resp: The response headers
+ :param resp_body: The body of the response
+ :raises ResponseWithNonEmptyBody: If the response with the status code
+ is not supposed to have a body
+ :raises ResponseWithEntity: If the response code is 205 but has an
+ entity
+ """
+ if (resp.status in set((204, 205, 304)) or resp.status < 200 or
+ method.upper() == 'HEAD') and resp_body:
+ raise exceptions.ResponseWithNonEmptyBody(status=resp.status)
+ # NOTE(afazekas):
+ # If the HTTP Status Code is 205
+ # 'The response MUST NOT include an entity.'
+ # A HTTP entity has an entity-body and an 'entity-header'.
+ # In the HTTP response specification (Section 6) the 'entity-header'
+ # 'generic-header' and 'response-header' are in OR relation.
+ # All headers not in the above two group are considered as entity
+ # header in every interpretation.
+
+ if (resp.status == 205 and
+ 0 != len(set(resp.keys()) - set(('status',)) -
+ self.response_header_lc - self.general_header_lc)):
+ raise exceptions.ResponseWithEntity()
+ # NOTE(afazekas)
+ # Now the swift sometimes (delete not empty container)
+ # returns with non json error response, we can create new rest class
+ # for swift.
+ # Usually RFC2616 says error responses SHOULD contain an explanation.
+ # The warning is normal for SHOULD/SHOULD NOT case
+
+ # Likely it will cause an error
+ if method != 'HEAD' and not resp_body and resp.status >= 400:
+ self.LOG.warning("status >= 400 response with empty body")
+
+ def _request(self, method, url, headers=None, body=None, chunked=False):
+ """A simple HTTP request interface."""
+ # Authenticate the request with the auth provider
+ req_url, req_headers, req_body = self.auth_provider.auth_request(
+ method, url, headers, body, self.filters)
+
+ # Do the actual request, and time it
+ start = time.time()
+ self._log_request_start(method, req_url)
+ resp, resp_body = self.raw_request(
+ req_url, method, headers=req_headers, body=req_body,
+ chunked=chunked
+ )
+ end = time.time()
+ self._log_request(method, req_url, resp, secs=(end - start),
+ req_headers=req_headers, req_body=req_body,
+ resp_body=resp_body)
+
+ # Verify HTTP response codes
+ self.response_checker(method, resp, resp_body)
+
+ return resp, resp_body
+
+ def raw_request(self, url, method, headers=None, body=None, chunked=False):
+ """Send a raw HTTP request without the keystone catalog or auth
+
+ This method sends a HTTP request in the same manner as the request()
+ method, however it does so without using keystone auth or the catalog
+ to determine the base url. Additionally no response handling is done
+ the results from the request are just returned.
+
+ :param str url: Full url to send the request
+ :param str method: The HTTP verb to use for the request
+ :param str headers: Headers to use for the request if none are specifed
+ the headers
+ :param str body: Body to send with the request
+ :param bool chunked: sends the body with chunked encoding
+ :rtype: tuple
+ :return: a tuple with the first entry containing the response headers
+ and the second the response body
+ """
+ if headers is None:
+ headers = self.get_headers()
+ return self.http_obj.request(url, method, headers=headers,
+ body=body, chunked=chunked)
+
+ def request(self, method, url, extra_headers=False, headers=None,
+ body=None, chunked=False):
+ """Send a HTTP request with keystone auth and using the catalog
+
+ This method will send an HTTP request using keystone auth in the
+ headers and the catalog to determine the endpoint to use for the
+ baseurl to send the request to. Additionally
+
+ When a response is received it will check it to see if an error
+ response was received. If it was an exception will be raised to enable
+ it to be handled quickly.
+
+ This method will also handle rate-limiting, if a 413 response code is
+ received it will retry the request after waiting the 'retry-after'
+ duration from the header.
+
+ :param str method: The HTTP verb to use for the request
+ :param str url: Relative url to send the request to
+ :param bool extra_headers: Boolean value than indicates if the headers
+ returned by the get_headers() method are to
+ be used but additional headers are needed in
+ the request pass them in as a dict.
+ :param dict headers: Headers to use for the request if none are
+ specifed the headers returned from the
+ get_headers() method are used. If the request
+ explicitly requires no headers use an empty dict.
+ :param str body: Body to send with the request
+ :param bool chunked: sends the body with chunked encoding
+ :rtype: tuple
+ :return: a tuple with the first entry containing the response headers
+ and the second the response body
+ :raises UnexpectedContentType: If the content-type of the response
+ isn't an expect type
+ :raises Unauthorized: If a 401 response code is received
+ :raises Forbidden: If a 403 response code is received
+ :raises NotFound: If a 404 response code is received
+ :raises BadRequest: If a 400 response code is received
+ :raises Gone: If a 410 response code is received
+ :raises Conflict: If a 409 response code is received
+ :raises PreconditionFailed: If a 412 response code is received
+ :raises OverLimit: If a 413 response code is received and over_limit is
+ not in the response body
+ :raises RateLimitExceeded: If a 413 response code is received and
+ over_limit is in the response body
+ :raises InvalidContentType: If a 415 response code is received
+ :raises UnprocessableEntity: If a 422 response code is received
+ :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+ and couldn't be parsed
+ :raises NotImplemented: If a 501 response code is received
+ :raises ServerFault: If a 500 response code is received
+ :raises UnexpectedResponseCode: If a response code above 400 is
+ received and it doesn't fall into any
+ of the handled checks
+ """
+ # if extra_headers is True
+ # default headers would be added to headers
+ retry = 0
+
+ if headers is None:
+ # NOTE(vponomaryov): if some client do not need headers,
+ # it should explicitly pass empty dict
+ headers = self.get_headers()
+ elif extra_headers:
+ try:
+ headers.update(self.get_headers())
+ except (ValueError, TypeError):
+ headers = self.get_headers()
+
+ resp, resp_body = self._request(method, url, headers=headers,
+ body=body, chunked=chunked)
+
+ while (resp.status == 413 and
+ 'retry-after' in resp and
+ not self.is_absolute_limit(
+ resp, self._parse_resp(resp_body)) and
+ retry < MAX_RECURSION_DEPTH):
+ retry += 1
+ delay = self._get_retry_after_delay(resp)
+ self.LOG.debug(
+ "Sleeping %s seconds based on retry-after header", delay
+ )
+ time.sleep(delay)
+ resp, resp_body = self._request(method, url,
+ headers=headers, body=body)
+ self._error_checker(resp, resp_body)
+ return resp, resp_body
+
+ def _get_retry_after_delay(self, resp):
+ """Extract the delay from the retry-after header.
+
+ This supports both integer and HTTP date formatted retry-after headers
+ per RFC 2616.
+
+ :param resp: The response containing the retry-after headers
+ :rtype: int
+ :return: The delay in seconds, clamped to be at least 1 second
+ :raises ValueError: On failing to parse the delay
+ """
+ delay = None
+ try:
+ delay = int(resp['retry-after'])
+ except (ValueError, KeyError):
+ pass
+
+ try:
+ retry_timestamp = self._parse_http_date(resp['retry-after'])
+ date_timestamp = self._parse_http_date(resp['date'])
+ delay = int(retry_timestamp - date_timestamp)
+ except (ValueError, OverflowError, KeyError):
+ pass
+
+ if delay is None:
+ raise ValueError(
+ "Failed to parse retry-after header %r as either int or "
+ "HTTP-date." % resp.get('retry-after')
+ )
+
+ # Retry-after headers do not have sub-second precision. Clients may
+ # receive a delay of 0. After sleeping 0 seconds, we would (likely) hit
+ # another 413. To avoid this, always sleep at least 1 second.
+ return max(1, delay)
+
+ def _parse_http_date(self, val):
+ """Parse an HTTP date, like 'Fri, 31 Dec 1999 23:59:59 GMT'.
+
+ Return an epoch timestamp (float), as returned by time.mktime().
+ """
+ parts = email.utils.parsedate(val)
+ if not parts:
+ raise ValueError("Failed to parse date %s" % val)
+ return time.mktime(parts)
+
+ def _error_checker(self, resp, resp_body):
+
+ # NOTE(mtreinish): Check for httplib response from glance_http. The
+ # object can't be used here because importing httplib breaks httplib2.
+ # If another object from a class not imported were passed here as
+ # resp this could possibly fail
+ if str(type(resp)) == "<type 'instance'>":
+ ctype = resp.getheader('content-type')
+ else:
+ try:
+ ctype = resp['content-type']
+ # NOTE(mtreinish): Keystone delete user responses doesn't have a
+ # content-type header. (They don't have a body) So just pretend it
+ # is set.
+ except KeyError:
+ ctype = 'application/json'
+
+ # It is not an error response
+ if resp.status < 400:
+ return
+
+ # NOTE(zhipengh): There is a purposefully duplicate of content-type
+ # with the only difference is with or without spaces, as specified
+ # in RFC7231.
+ JSON_ENC = ['application/json', 'application/json; charset=utf-8',
+ 'application/json;charset=utf-8']
+
+ # NOTE(mtreinish): This is for compatibility with Glance and swift
+ # APIs. These are the return content types that Glance api v1
+ # (and occasionally swift) are using.
+ # NOTE(zhipengh): There is a purposefully duplicate of content-type
+ # with the only difference is with or without spaces, as specified
+ # in RFC7231.
+ TXT_ENC = ['text/plain', 'text/html', 'text/html; charset=utf-8',
+ 'text/plain; charset=utf-8', 'text/html;charset=utf-8',
+ 'text/plain;charset=utf-8']
+
+ if ctype.lower() in JSON_ENC:
+ parse_resp = True
+ elif ctype.lower() in TXT_ENC:
+ parse_resp = False
+ else:
+ raise exceptions.UnexpectedContentType(str(resp.status),
+ resp=resp)
+
+ if resp.status == 401:
+ if parse_resp:
+ resp_body = self._parse_resp(resp_body)
+ raise exceptions.Unauthorized(resp_body, resp=resp)
+
+ if resp.status == 403:
+ if parse_resp:
+ resp_body = self._parse_resp(resp_body)
+ raise exceptions.Forbidden(resp_body, resp=resp)
+
+ if resp.status == 404:
+ if parse_resp:
+ resp_body = self._parse_resp(resp_body)
+ raise exceptions.NotFound(resp_body, resp=resp)
+
+ if resp.status == 400:
+ if parse_resp:
+ resp_body = self._parse_resp(resp_body)
+ raise exceptions.BadRequest(resp_body, resp=resp)
+
+ if resp.status == 410:
+ if parse_resp:
+ resp_body = self._parse_resp(resp_body)
+ raise exceptions.Gone(resp_body, resp=resp)
+
+ if resp.status == 409:
+ if parse_resp:
+ resp_body = self._parse_resp(resp_body)
+ raise exceptions.Conflict(resp_body, resp=resp)
+
+ if resp.status == 412:
+ if parse_resp:
+ resp_body = self._parse_resp(resp_body)
+ raise exceptions.PreconditionFailed(resp_body, resp=resp)
+
+ if resp.status == 413:
+ if parse_resp:
+ resp_body = self._parse_resp(resp_body)
+ if self.is_absolute_limit(resp, resp_body):
+ raise exceptions.OverLimit(resp_body, resp=resp)
+ else:
+ raise exceptions.RateLimitExceeded(resp_body, resp=resp)
+
+ if resp.status == 415:
+ if parse_resp:
+ resp_body = self._parse_resp(resp_body)
+ raise exceptions.InvalidContentType(resp_body, resp=resp)
+
+ if resp.status == 422:
+ if parse_resp:
+ resp_body = self._parse_resp(resp_body)
+ raise exceptions.UnprocessableEntity(resp_body, resp=resp)
+
+ if resp.status in (500, 501):
+ message = resp_body
+ if parse_resp:
+ try:
+ resp_body = self._parse_resp(resp_body)
+ except ValueError:
+ # If response body is a non-json string message.
+ # Use resp_body as is and raise InvalidResponseBody
+ # exception.
+ raise exceptions.InvalidHTTPResponseBody(message)
+ else:
+ if isinstance(resp_body, dict):
+ # I'm seeing both computeFault
+ # and cloudServersFault come back.
+ # Will file a bug to fix, but leave as is for now.
+ if 'cloudServersFault' in resp_body:
+ message = resp_body['cloudServersFault']['message']
+ elif 'computeFault' in resp_body:
+ message = resp_body['computeFault']['message']
+ elif 'error' in resp_body:
+ message = resp_body['error']['message']
+ elif 'message' in resp_body:
+ message = resp_body['message']
+ else:
+ message = resp_body
+
+ if resp.status == 501:
+ raise exceptions.NotImplemented(resp_body, resp=resp,
+ message=message)
+ else:
+ raise exceptions.ServerFault(resp_body, resp=resp,
+ message=message)
+
+ if resp.status >= 400:
+ raise exceptions.UnexpectedResponseCode(str(resp.status),
+ resp=resp)
+
+ def is_absolute_limit(self, resp, resp_body):
+ if (not isinstance(resp_body, collections.Mapping) or
+ 'retry-after' not in resp):
+ return True
+ return 'exceed' in resp_body.get('message', 'blabla')
+
+ def wait_for_resource_deletion(self, id):
+ """Waits for a resource to be deleted
+
+ This method will loop over is_resource_deleted until either
+ is_resource_deleted returns True or the build timeout is reached. This
+ depends on is_resource_deleted being implemented
+
+ :param str id: The id of the resource to check
+ :raises TimeoutException: If the build_timeout has elapsed and the
+ resource still hasn't been deleted
+ """
+ start_time = int(time.time())
+ while True:
+ if self.is_resource_deleted(id):
+ return
+ if int(time.time()) - start_time >= self.build_timeout:
+ message = ('Failed to delete %(resource_type)s %(id)s within '
+ 'the required time (%(timeout)s s).' %
+ {'resource_type': self.resource_type, 'id': id,
+ 'timeout': self.build_timeout})
+ caller = test_utils.find_test_caller()
+ if caller:
+ message = '(%s) %s' % (caller, message)
+ raise exceptions.TimeoutException(message)
+ time.sleep(self.build_interval)
+
+ def is_resource_deleted(self, id):
+ """Subclasses override with specific deletion detection."""
+ message = ('"%s" does not implement is_resource_deleted'
+ % self.__class__.__name__)
+ raise NotImplementedError(message)
+
+ @property
+ def resource_type(self):
+ """Returns the primary type of resource this client works with."""
+ return 'resource'
+
+ @classmethod
+ def validate_response(cls, schema, resp, body):
+ # Only check the response if the status code is a success code
+ # TODO(cyeoh): Eventually we should be able to verify that a failure
+ # code if it exists is something that we expect. This is explicitly
+ # declared in the V3 API and so we should be able to export this in
+ # the response schema. For now we'll ignore it.
+ if resp.status in HTTP_SUCCESS + HTTP_REDIRECTION:
+ cls.expected_success(schema['status_code'], resp.status)
+
+ # Check the body of a response
+ body_schema = schema.get('response_body')
+ if body_schema:
+ try:
+ jsonschema.validate(body, body_schema,
+ cls=JSONSCHEMA_VALIDATOR,
+ format_checker=FORMAT_CHECKER)
+ except jsonschema.ValidationError as ex:
+ msg = ("HTTP response body is invalid (%s)" % ex)
+ raise exceptions.InvalidHTTPResponseBody(msg)
+ else:
+ if body:
+ msg = ("HTTP response body should not exist (%s)" % body)
+ raise exceptions.InvalidHTTPResponseBody(msg)
+
+ # Check the header of a response
+ header_schema = schema.get('response_header')
+ if header_schema:
+ try:
+ jsonschema.validate(resp, header_schema,
+ cls=JSONSCHEMA_VALIDATOR,
+ format_checker=FORMAT_CHECKER)
+ except jsonschema.ValidationError as ex:
+ msg = ("HTTP response header is invalid (%s)" % ex)
+ raise exceptions.InvalidHTTPResponseHeader(msg)
+
+ def _get_base_version_url(self):
+ # TODO(oomichi): This method can be used for auth's replace_version().
+ # So it is nice to have common logic for the maintenance.
+ endpoint = self.base_url
+ url = urllib.parse.urlsplit(endpoint)
+ new_path = re.split(r'(^|/)+v\d+(\.\d+)?', url.path)[0]
+ url = list(url)
+ url[2] = new_path + '/'
+ return urllib.parse.urlunsplit(url)
+
+
+class ResponseBody(dict):
+ """Class that wraps an http response and dict body into a single value.
+
+ Callers that receive this object will normally use it as a dict but
+ can extract the response if needed.
+ """
+
+ def __init__(self, response, body=None):
+ body_data = body or {}
+ self.update(body_data)
+ self.response = response
+
+ def __str__(self):
+ body = super(ResponseBody, self).__str__()
+ return "response: %s\nBody: %s" % (self.response, body)
+
+
+class ResponseBodyData(object):
+ """Class that wraps an http response and string data into a single value.
+
+ """
+
+ def __init__(self, response, data):
+ self.response = response
+ self.data = data
+
+ def __str__(self):
+ return "response: %s\nBody: %s" % (self.response, self.data)
+
+
+class ResponseBodyList(list):
+ """Class that wraps an http response and list body into a single value.
+
+ Callers that receive this object will normally use it as a list but
+ can extract the response if needed.
+ """
+
+ def __init__(self, response, body=None):
+ body_data = body or []
+ self.extend(body_data)
+ self.response = response
+
+ def __str__(self):
+ body = super(ResponseBodyList, self).__str__()
+ return "response: %s\nBody: %s" % (self.response, body)
diff --git a/tempest/lib/common/ssh.py b/tempest/lib/common/ssh.py
new file mode 100644
index 0000000..d4ec6ad
--- /dev/null
+++ b/tempest/lib/common/ssh.py
@@ -0,0 +1,219 @@
+# 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.
+
+
+import select
+import socket
+import time
+import warnings
+
+from oslo_log import log as logging
+import six
+
+from tempest.lib import exceptions
+
+
+with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ import paramiko
+
+
+LOG = logging.getLogger(__name__)
+
+
+class Client(object):
+
+ def __init__(self, host, username, password=None, timeout=300, pkey=None,
+ channel_timeout=10, look_for_keys=False, key_filename=None,
+ port=22, proxy_client=None):
+ """SSH client.
+
+ Many of parameters are just passed to the underlying implementation
+ as it is. See the paramiko documentation for more details.
+ http://docs.paramiko.org/en/2.1/api/client.html#paramiko.client.SSHClient.connect
+
+ :param host: Host to login.
+ :param username: SSH username.
+ :param password: SSH password, or a password to unlock private key.
+ :param timeout: Timeout in seconds, including retries.
+ Default is 300 seconds.
+ :param pkey: Private key.
+ :param channel_timeout: Channel timeout in seconds, passed to the
+ paramiko. Default is 10 seconds.
+ :param look_for_keys: Whether or not to search for private keys
+ in ``~/.ssh``. Default is False.
+ :param key_filename: Filename for private key to use.
+ :param port: SSH port number.
+ :param proxy_client: Another SSH client to provide a transport
+ for ssh-over-ssh. The default is None, which means
+ not to use ssh-over-ssh.
+ :type proxy_client: ``tempest.lib.common.ssh.Client`` object
+ """
+ self.host = host
+ self.username = username
+ self.port = port
+ self.password = password
+ if isinstance(pkey, six.string_types):
+ pkey = paramiko.RSAKey.from_private_key(
+ six.StringIO(str(pkey)))
+ self.pkey = pkey
+ self.look_for_keys = look_for_keys
+ self.key_filename = key_filename
+ self.timeout = int(timeout)
+ self.channel_timeout = float(channel_timeout)
+ self.buf_size = 1024
+ self.proxy_client = proxy_client
+ self._proxy_conn = None
+
+ def _get_ssh_connection(self, sleep=1.5, backoff=1):
+ """Returns an ssh connection to the specified host."""
+ bsleep = sleep
+ ssh = paramiko.SSHClient()
+ ssh.set_missing_host_key_policy(
+ paramiko.AutoAddPolicy())
+ _start_time = time.time()
+ if self.pkey is not None:
+ LOG.info("Creating ssh connection to '%s:%d' as '%s'"
+ " with public key authentication",
+ self.host, self.port, self.username)
+ else:
+ LOG.info("Creating ssh connection to '%s:%d' as '%s'"
+ " with password %s",
+ self.host, self.port, self.username, str(self.password))
+ attempts = 0
+ while True:
+ if self.proxy_client is not None:
+ proxy_chan = self._get_proxy_channel()
+ else:
+ proxy_chan = None
+ try:
+ ssh.connect(self.host, port=self.port, username=self.username,
+ password=self.password,
+ look_for_keys=self.look_for_keys,
+ key_filename=self.key_filename,
+ timeout=self.channel_timeout, pkey=self.pkey,
+ sock=proxy_chan)
+ LOG.info("ssh connection to %s@%s successfully created",
+ self.username, self.host)
+ return ssh
+ except (EOFError,
+ socket.error, socket.timeout,
+ paramiko.SSHException) as e:
+ ssh.close()
+ if self._is_timed_out(_start_time):
+ LOG.exception("Failed to establish authenticated ssh"
+ " connection to %s@%s after %d attempts",
+ self.username, self.host, attempts)
+ raise exceptions.SSHTimeout(host=self.host,
+ user=self.username,
+ password=self.password)
+ bsleep += backoff
+ attempts += 1
+ LOG.warning("Failed to establish authenticated ssh"
+ " connection to %s@%s (%s). Number attempts: %s."
+ " Retry after %d seconds.",
+ self.username, self.host, e, attempts, bsleep)
+ time.sleep(bsleep)
+
+ def _is_timed_out(self, start_time):
+ return (time.time() - self.timeout) > start_time
+
+ @staticmethod
+ def _can_system_poll():
+ return hasattr(select, 'poll')
+
+ def exec_command(self, cmd, encoding="utf-8"):
+ """Execute the specified command on the server
+
+ Note that this method is reading whole command outputs to memory, thus
+ shouldn't be used for large outputs.
+
+ :param str cmd: Command to run at remote server.
+ :param str encoding: Encoding for result from paramiko.
+ Result will not be decoded if None.
+ :returns: data read from standard output of the command.
+ :raises: SSHExecCommandFailed if command returns nonzero
+ status. The exception contains command status stderr content.
+ :raises: TimeoutException if cmd doesn't end when timeout expires.
+ """
+ ssh = self._get_ssh_connection()
+ transport = ssh.get_transport()
+ with transport.open_session() as channel:
+ channel.fileno() # Register event pipe
+ channel.exec_command(cmd)
+ channel.shutdown_write()
+
+ # If the executing host is linux-based, poll the channel
+ if self._can_system_poll():
+ out_data_chunks = []
+ err_data_chunks = []
+ poll = select.poll()
+ poll.register(channel, select.POLLIN)
+ start_time = time.time()
+
+ while True:
+ ready = poll.poll(self.channel_timeout)
+ if not any(ready):
+ if not self._is_timed_out(start_time):
+ continue
+ raise exceptions.TimeoutException(
+ "Command: '{0}' executed on host '{1}'.".format(
+ cmd, self.host))
+ if not ready[0]: # If there is nothing to read.
+ continue
+ out_chunk = err_chunk = None
+ if channel.recv_ready():
+ out_chunk = channel.recv(self.buf_size)
+ out_data_chunks += out_chunk,
+ if channel.recv_stderr_ready():
+ err_chunk = channel.recv_stderr(self.buf_size)
+ err_data_chunks += err_chunk,
+ if not err_chunk and not out_chunk:
+ break
+ out_data = b''.join(out_data_chunks)
+ err_data = b''.join(err_data_chunks)
+ # Just read from the channels
+ else:
+ out_file = channel.makefile('rb', self.buf_size)
+ err_file = channel.makefile_stderr('rb', self.buf_size)
+ out_data = out_file.read()
+ err_data = err_file.read()
+ if encoding:
+ out_data = out_data.decode(encoding)
+ err_data = err_data.decode(encoding)
+
+ exit_status = channel.recv_exit_status()
+
+ if 0 != exit_status:
+ raise exceptions.SSHExecCommandFailed(
+ command=cmd, exit_status=exit_status,
+ stderr=err_data, stdout=out_data)
+ return out_data
+
+ def test_connection_auth(self):
+ """Raises an exception when we can not connect to server via ssh."""
+ connection = self._get_ssh_connection()
+ connection.close()
+
+ def _get_proxy_channel(self):
+ conn = self.proxy_client._get_ssh_connection()
+ # Keep a reference to avoid g/c
+ # https://github.com/paramiko/paramiko/issues/440
+ self._proxy_conn = conn
+ transport = conn.get_transport()
+ chan = transport.open_session()
+ cmd = 'nc %s %s' % (self.host, self.port)
+ chan.exec_command(cmd)
+ return chan
diff --git a/tempest/lib/common/thread.py b/tempest/lib/common/thread.py
new file mode 100644
index 0000000..510fc36
--- /dev/null
+++ b/tempest/lib/common/thread.py
@@ -0,0 +1,29 @@
+# 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.
+
+# This make disable relative module import
+from __future__ import absolute_import
+
+
+import six
+
+if six.PY2:
+ # module thread is removed in Python 3
+ from thread import get_ident # noqa: H237,F401
+
+else:
+ # On Python3 thread module has been deprecated and get_ident has been moved
+ # to threading module
+ from threading import get_ident # noqa: F401
diff --git a/tempest/stress/__init__.py b/tempest/lib/common/utils/__init__.py
similarity index 100%
copy from tempest/stress/__init__.py
copy to tempest/lib/common/utils/__init__.py
diff --git a/tempest/lib/common/utils/data_utils.py b/tempest/lib/common/utils/data_utils.py
new file mode 100644
index 0000000..c5df590
--- /dev/null
+++ b/tempest/lib/common/utils/data_utils.py
@@ -0,0 +1,181 @@
+# 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.
+
+import itertools
+import random
+import string
+import uuid
+
+from oslo_utils import uuidutils
+import six.moves
+
+
+def rand_uuid():
+ """Generate a random UUID string
+
+ :return: a random UUID (e.g. '1dc12c7d-60eb-4b61-a7a2-17cf210155b6')
+ :rtype: string
+ """
+ return uuidutils.generate_uuid()
+
+
+def rand_uuid_hex():
+ """Generate a random UUID hex string
+
+ :return: a random UUID (e.g. '0b98cf96d90447bda4b46f31aeb1508c')
+ :rtype: string
+ """
+ return uuid.uuid4().hex
+
+
+def rand_name(name='', prefix='tempest'):
+ """Generate a random name that includes a random number
+
+ :param str name: The name that you want to include
+ :param str prefix: The prefix that you want to include
+ :return: a random name. The format is
+ '<prefix>-<name>-<random number>'.
+ (e.g. 'prefixfoo-namebar-154876201')
+ :rtype: string
+ """
+ randbits = str(random.randint(1, 0x7fffffff))
+ rand_name = randbits
+ if name:
+ rand_name = name + '-' + rand_name
+ if prefix:
+ rand_name = prefix + '-' + rand_name
+ return rand_name
+
+
+def rand_password(length=15):
+ """Generate a random password
+
+ :param int length: The length of password that you expect to set
+ (If it's smaller than 3, it's same as 3.)
+ :return: a random password. The format is
+ '<random upper letter>-<random number>-<random special character>
+ -<random ascii letters or digit characters or special symbols>'
+ (e.g. 'G2*ac8&lKFFgh%2')
+ :rtype: string
+ """
+ upper = random.choice(string.ascii_uppercase)
+ ascii_char = string.ascii_letters
+ digits = string.digits
+ digit = random.choice(string.digits)
+ puncs = '~!@#%^&*_=+'
+ punc = random.choice(puncs)
+ seed = ascii_char + digits + puncs
+ pre = upper + digit + punc
+ password = pre + ''.join(random.choice(seed) for x in range(length - 3))
+ return password
+
+
+def rand_url():
+ """Generate a random url that includes a random number
+
+ :return: a random url. The format is 'https://url-<random number>.com'.
+ (e.g. 'https://url-154876201.com')
+ :rtype: string
+ """
+ randbits = str(random.randint(1, 0x7fffffff))
+ return 'https://url-' + randbits + '.com'
+
+
+def rand_int_id(start=0, end=0x7fffffff):
+ """Generate a random integer value
+
+ :param int start: The value that you expect to start here
+ :param int end: The value that you expect to end here
+ :return: a random integer value
+ :rtype: int
+ """
+ return random.randint(start, end)
+
+
+def rand_mac_address():
+ """Generate an Ethernet MAC address
+
+ :return: an random Ethernet MAC address
+ :rtype: string
+ """
+ # NOTE(vish): We would prefer to use 0xfe here to ensure that linux
+ # bridge mac addresses don't change, but it appears to
+ # conflict with libvirt, so we use the next highest octet
+ # that has the unicast and locally administered bits set
+ # properly: 0xfa.
+ # Discussion: https://bugs.launchpad.net/nova/+bug/921838
+ mac = [0xfa, 0x16, 0x3e,
+ random.randint(0x00, 0xff),
+ random.randint(0x00, 0xff),
+ random.randint(0x00, 0xff)]
+ return ':'.join(["%02x" % x for x in mac])
+
+
+def rand_infiniband_guid_address():
+ """Generate an Infiniband GUID address
+
+ :return: an random Infiniband GUID address
+ :rtype: string
+ """
+ guid = []
+ for i in range(8):
+ guid.append("%02x" % random.randint(0x00, 0xff))
+ return ':'.join(guid)
+
+
+def parse_image_id(image_ref):
+ """Return the image id from a given image ref
+
+ This function just returns the last word of the given image ref string
+ splitting with '/'.
+ :param str image_ref: a string that includes the image id
+ :return: the image id string
+ :rtype: string
+ """
+ return image_ref.rsplit('/')[-1]
+
+
+def arbitrary_string(size=4, base_text=None):
+ """Return size characters from base_text
+
+ This generates a string with an arbitrary number of characters, generated
+ by looping the base_text string. If the size is smaller than the size of
+ base_text, returning string is shrunk to the size.
+ :param int size: a returning characters size
+ :param str base_text: a string you want to repeat
+ :return: size string
+ :rtype: string
+ """
+ if not base_text:
+ base_text = 'test'
+ return ''.join(itertools.islice(itertools.cycle(base_text), size))
+
+
+def random_bytes(size=1024):
+ """Return size randomly selected bytes as a string
+
+ :param int size: a returning bytes size
+ :return: size randomly bytes
+ :rtype: string
+ """
+ return b''.join([six.int2byte(random.randint(0, 255))
+ for i in range(size)])
+
+
+# Courtesy of http://stackoverflow.com/a/312464
+def chunkify(sequence, chunksize):
+ """Yield successive chunks from `sequence`."""
+ for i in range(0, len(sequence), chunksize):
+ yield sequence[i:i + chunksize]
diff --git a/tempest/api/baremetal/__init__.py b/tempest/lib/common/utils/linux/__init__.py
similarity index 100%
rename from tempest/api/baremetal/__init__.py
rename to tempest/lib/common/utils/linux/__init__.py
diff --git a/tempest/lib/common/utils/linux/remote_client.py b/tempest/lib/common/utils/linux/remote_client.py
new file mode 100644
index 0000000..8ac1d38
--- /dev/null
+++ b/tempest/lib/common/utils/linux/remote_client.py
@@ -0,0 +1,153 @@
+# 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 functools
+import re
+import sys
+
+import netaddr
+from oslo_log import log as logging
+import six
+
+from tempest.lib.common import ssh
+from tempest.lib.common.utils import test_utils
+import tempest.lib.exceptions
+
+LOG = logging.getLogger(__name__)
+
+
+def debug_ssh(function):
+ """Decorator to generate extra debug info in case off SSH failure"""
+ @functools.wraps(function)
+ def wrapper(self, *args, **kwargs):
+ try:
+ return function(self, *args, **kwargs)
+ except Exception as e:
+ caller = test_utils.find_test_caller() or "not found"
+ if not isinstance(e, tempest.lib.exceptions.SSHTimeout):
+ message = ('Executing command on %(ip)s failed. '
+ 'Error: %(error)s' % {'ip': self.ip_address,
+ 'error': e})
+ message = '(%s) %s' % (caller, message)
+ LOG.error(message)
+ raise
+ else:
+ try:
+ original_exception = sys.exc_info()
+ if self.server:
+ msg = 'Caller: %s. Timeout trying to ssh to server %s'
+ LOG.debug(msg, caller, self.server)
+ if self.console_output_enabled and self.servers_client:
+ try:
+ msg = 'Console log for server %s: %s'
+ console_log = (
+ self.servers_client.get_console_output(
+ self.server['id'])['output'])
+ LOG.debug(msg, self.server['id'], console_log)
+ except Exception:
+ msg = 'Could not get console_log for server %s'
+ LOG.debug(msg, self.server['id'])
+ # re-raise the original ssh timeout exception
+ six.reraise(*original_exception)
+ finally:
+ # Delete the traceback to avoid circular references
+ _, _, trace = original_exception
+ del trace
+ return wrapper
+
+
+class RemoteClient(object):
+
+ def __init__(self, ip_address, username, password=None, pkey=None,
+ server=None, servers_client=None, ssh_timeout=300,
+ connect_timeout=60, console_output_enabled=True,
+ ssh_shell_prologue="set -eu -o pipefail; PATH=$PATH:/sbin;",
+ ping_count=1, ping_size=56):
+ """Executes commands in a VM over ssh
+
+ :param ip_address: IP address to ssh to
+ :param username: Ssh username
+ :param password: Ssh password
+ :param pkey: Ssh public key
+ :param server: Server dict, used for debugging purposes
+ :param servers_client: Servers client, used for debugging purposes
+ :param ssh_timeout: Timeout in seconds to wait for the ssh banner
+ :param connect_timeout: Timeout in seconds to wait for TCP connection
+ :param console_output_enabled: Support serial console output?
+ :param ssh_shell_prologue: Shell fragments to use before command
+ :param ping_count: Number of ping packets
+ :param ping_size: Packet size for ping packets
+ """
+ self.server = server
+ self.servers_client = servers_client
+ self.ip_address = ip_address
+ self.console_output_enabled = console_output_enabled
+ self.ssh_shell_prologue = ssh_shell_prologue
+ self.ping_count = ping_count
+ self.ping_size = ping_size
+
+ self.ssh_client = ssh.Client(ip_address, username, password,
+ ssh_timeout, pkey=pkey,
+ channel_timeout=connect_timeout)
+
+ @debug_ssh
+ def exec_command(self, cmd):
+ # Shell options below add more clearness on failures,
+ # path is extended for some non-cirros guest oses (centos7)
+ cmd = self.ssh_shell_prologue + " " + cmd
+ LOG.debug("Remote command: %s", cmd)
+ return self.ssh_client.exec_command(cmd)
+
+ @debug_ssh
+ def validate_authentication(self):
+ """Validate ssh connection and authentication
+
+ This method raises an Exception when the validation fails.
+ """
+ self.ssh_client.test_connection_auth()
+
+ def ping_host(self, host, count=None, size=None, nic=None):
+ if count is None:
+ count = self.ping_count
+ if size is None:
+ size = self.ping_size
+
+ addr = netaddr.IPAddress(host)
+ cmd = 'ping6' if addr.version == 6 else 'ping'
+ if nic:
+ cmd = 'sudo {cmd} -I {nic}'.format(cmd=cmd, nic=nic)
+ cmd += ' -c{0} -w{0} -s{1} {2}'.format(count, size, host)
+ return self.exec_command(cmd)
+
+ def mount_config_drive(self):
+ """Mount the config drive inside a virtual machine
+
+ This method will not unmount the config drive, so unmount_config_drive
+ must be used for cleanup.
+ """
+ cmd_blkid = 'blkid | grep -i config-2'
+ result = self.exec_command(cmd_blkid)
+ dev_name = re.match('([^:]+)', result).group()
+
+ try:
+ self.exec_command('sudo mount %s /mnt' % dev_name)
+ except tempest.lib.exceptions.SSHExecCommandFailed:
+ # So the command failed, let's try to know why and print some
+ # useful information.
+ lsblk = self.exec_command('sudo lsblk --fs --ascii')
+ LOG.error("Mounting %s on /mnt failed. Right after the "
+ "failure 'lsblk' in the guest reported:\n%s",
+ dev_name, lsblk)
+ raise
+
+ def unmount_config_drive(self):
+ self.exec_command('sudo umount /mnt')
diff --git a/tempest/lib/common/utils/misc.py b/tempest/lib/common/utils/misc.py
new file mode 100644
index 0000000..f13b4c8
--- /dev/null
+++ b/tempest/lib/common/utils/misc.py
@@ -0,0 +1,37 @@
+# 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_log import log as logging
+
+from tempest.lib.common.utils import test_utils
+
+LOG = logging.getLogger(__name__)
+
+
+def singleton(cls):
+ """Simple wrapper for classes that should only have a single instance."""
+ instances = {}
+
+ def getinstance():
+ if cls not in instances:
+ instances[cls] = cls()
+ return instances[cls]
+ return getinstance
+
+
+def find_test_caller(*args, **kwargs):
+ LOG.warning("tempest.lib.common.utils.misc.find_test_caller is deprecated "
+ "in favor of tempest.lib.common.utils.test_utils."
+ "find_test_caller")
+ test_utils.find_test_caller(*args, **kwargs)
diff --git a/tempest/lib/common/utils/test_utils.py b/tempest/lib/common/utils/test_utils.py
new file mode 100644
index 0000000..2a9f3a9
--- /dev/null
+++ b/tempest/lib/common/utils/test_utils.py
@@ -0,0 +1,114 @@
+# Copyright 2016 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+import inspect
+import re
+import time
+
+from oslo_log import log as logging
+
+from tempest.lib import exceptions
+
+LOG = logging.getLogger(__name__)
+
+
+def find_test_caller():
+ """Find the caller class and test name.
+
+ Because we know that the interesting things that call us are
+ test_* methods, and various kinds of setUp / tearDown, we
+ can look through the call stack to find appropriate methods,
+ and the class we were in when those were called.
+ """
+ caller_name = None
+ names = []
+ frame = inspect.currentframe()
+ is_cleanup = False
+ # Start climbing the ladder until we hit a good method
+ while True:
+ try:
+ frame = frame.f_back
+ name = frame.f_code.co_name
+ names.append(name)
+ if re.search("^(test_|setUp|tearDown)", name):
+ cname = ""
+ if 'self' in frame.f_locals:
+ cname = frame.f_locals['self'].__class__.__name__
+ if 'cls' in frame.f_locals:
+ cname = frame.f_locals['cls'].__name__
+ caller_name = cname + ":" + name
+ break
+ elif re.search("^_run_cleanup", name):
+ is_cleanup = True
+ elif name == 'main':
+ caller_name = 'main'
+ break
+ else:
+ cname = ""
+ if 'self' in frame.f_locals:
+ cname = frame.f_locals['self'].__class__.__name__
+ if 'cls' in frame.f_locals:
+ cname = frame.f_locals['cls'].__name__
+
+ # the fact that we are running cleanups is indicated pretty
+ # deep in the stack, so if we see that we want to just
+ # start looking for a real class name, and declare victory
+ # once we do.
+ if is_cleanup and cname:
+ if not re.search("^RunTest", cname):
+ caller_name = cname + ":_run_cleanups"
+ break
+ except Exception:
+ break
+ # prevents frame leaks
+ del frame
+ if caller_name is None:
+ LOG.debug("Sane call name not found in %s", names)
+ return caller_name
+
+
+def call_and_ignore_notfound_exc(func, *args, **kwargs):
+ """Call the given function and pass if a `NotFound` exception is raised."""
+ try:
+ return func(*args, **kwargs)
+ except exceptions.NotFound:
+ pass
+
+
+def call_until_true(func, duration, sleep_for, *args, **kwargs):
+ """Call the given function until it returns True (and return True)
+
+ or until the specified duration (in seconds) elapses (and return False).
+
+ :param func: A callable that returns True on success.
+ :param duration: The number of seconds for which to attempt a
+ successful call of the function.
+ :param sleep_for: The number of seconds to sleep after an unsuccessful
+ invocation of the function.
+ :param args: args that are passed to func.
+ :param kwargs: kwargs that are passed to func.
+ """
+ now = time.time()
+ begin_time = now
+ timeout = now + duration
+ func_name = getattr(func, '__name__', getattr(func.__class__, '__name__'))
+ while now < timeout:
+ if func(*args, **kwargs):
+ LOG.debug("Call %s returns true in %f seconds",
+ func_name, time.time() - begin_time)
+ return True
+ time.sleep(sleep_for)
+ now = time.time()
+ LOG.debug("Call %s returns false in %f seconds", func_name, duration)
+ return False
diff --git a/tempest/lib/common/validation_resources.py b/tempest/lib/common/validation_resources.py
new file mode 100644
index 0000000..c35a01a
--- /dev/null
+++ b/tempest/lib/common/validation_resources.py
@@ -0,0 +1,457 @@
+# Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
+# Copyright (c) 2017 IBM Corp.
+# 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 fixtures
+from oslo_log import log as logging
+from oslo_utils import excutils
+
+from tempest.lib.common.utils import data_utils
+from tempest.lib import exceptions as lib_exc
+
+LOG = logging.getLogger(__name__)
+
+
+def _network_service(clients, use_neutron):
+ # Internal helper to select the right network clients
+ if use_neutron:
+ return clients.network
+ else:
+ return clients.compute
+
+
+def create_ssh_security_group(clients, add_rule=False, ethertype='IPv4',
+ use_neutron=True):
+ """Create a security group for ping/ssh testing
+
+ Create a security group to be attached to a VM using the nova or neutron
+ clients. If rules are added, the group can be attached to a VM to enable
+ connectivity validation over ICMP and further testing over SSH.
+
+ :param clients: Instance of `tempest.lib.services.clients.ServiceClients`
+ or of a subclass of it. Resources are provisioned using clients from
+ `clients`.
+ :param add_rule: Whether security group rules are provisioned or not.
+ Defaults to `False`.
+ :param ethertype: 'IPv4' or 'IPv6'. Honoured only in case neutron is used.
+ :param use_neutron: When True resources are provisioned via neutron, when
+ False resources are provisioned via nova.
+ :returns: A dictionary with the security group as returned by the API.
+
+ Examples::
+
+ from tempest.common import validation_resources as vr
+ from tempest.lib import auth
+ from tempest.lib.services import clients
+
+ creds = auth.get_credentials('http://mycloud/identity/v3',
+ username='me', project_name='me',
+ password='secret', domain_name='Default')
+ osclients = clients.ServiceClients(creds, 'http://mycloud/identity/v3')
+ # Security group for IPv4 tests
+ sg4 = vr.create_ssh_security_group(osclients, add_rule=True)
+ # Security group for IPv6 tests
+ sg6 = vr.create_ssh_security_group(osclients, ethertype='IPv6',
+ add_rule=True)
+ """
+ network_service = _network_service(clients, use_neutron)
+ security_groups_client = network_service.SecurityGroupsClient()
+ security_group_rules_client = network_service.SecurityGroupRulesClient()
+ # Security Group clients for nova and neutron behave the same
+ sg_name = data_utils.rand_name('securitygroup-')
+ sg_description = data_utils.rand_name('description-')
+ security_group = security_groups_client.create_security_group(
+ name=sg_name, description=sg_description)['security_group']
+ # Security Group Rules clients require different parameters depending on
+ # the network service in use
+ if add_rule:
+ try:
+ if use_neutron:
+ security_group_rules_client.create_security_group_rule(
+ security_group_id=security_group['id'],
+ protocol='tcp',
+ ethertype=ethertype,
+ port_range_min=22,
+ port_range_max=22,
+ direction='ingress')
+ security_group_rules_client.create_security_group_rule(
+ security_group_id=security_group['id'],
+ protocol='icmp',
+ ethertype=ethertype,
+ direction='ingress')
+ else:
+ security_group_rules_client.create_security_group_rule(
+ parent_group_id=security_group['id'], ip_protocol='tcp',
+ from_port=22, to_port=22)
+ security_group_rules_client.create_security_group_rule(
+ parent_group_id=security_group['id'], ip_protocol='icmp',
+ from_port=-1, to_port=-1)
+ except Exception as sgc_exc:
+ # If adding security group rules fails, we cleanup the SG before
+ # re-raising the failure up
+ with excutils.save_and_reraise_exception():
+ try:
+ msg = ('Error while provisioning security group rules in '
+ 'security group %s. Trying to cleanup.')
+ # The exceptions logging is already handled, so using
+ # debug here just to provide more context
+ LOG.debug(msg, sgc_exc)
+ clear_validation_resources(
+ clients, keypair=None, floating_ip=None,
+ security_group=security_group,
+ use_neutron=use_neutron)
+ except Exception as cleanup_exc:
+ msg = ('Error during cleanup of a security group. '
+ 'The cleanup was triggered by an exception during '
+ 'the provisioning of security group rules.\n'
+ 'Provisioning exception: %s\n'
+ 'First cleanup exception: %s')
+ LOG.exception(msg, sgc_exc, cleanup_exc)
+ LOG.debug("SSH Validation resource security group with tcp and icmp "
+ "rules %s created", sg_name)
+ return security_group
+
+
+def create_validation_resources(clients, keypair=False, floating_ip=False,
+ security_group=False,
+ security_group_rules=False,
+ ethertype='IPv4', use_neutron=True,
+ floating_network_id=None,
+ floating_network_name=None):
+ """Provision resources for VM ping/ssh testing
+
+ Create resources required to be able to ping / ssh a virtual machine:
+ keypair, security group, security group rules and a floating IP.
+ Which of those resources are required may depend on the cloud setup and on
+ the specific test and it can be controlled via the corresponding
+ arguments.
+
+ Provisioned resources are returned in a dictionary.
+
+ :param clients: Instance of `tempest.lib.services.clients.ServiceClients`
+ or of a subclass of it. Resources are provisioned using clients from
+ `clients`.
+ :param keypair: Whether to provision a keypair. Defaults to False.
+ :param floating_ip: Whether to provision a floating IP. Defaults to False.
+ :param security_group: Whether to provision a security group. Defaults to
+ False.
+ :param security_group_rules: Whether to provision security group rules.
+ Defaults to False.
+ :param ethertype: 'IPv4' or 'IPv6'. Honoured only in case neutron is used.
+ :param use_neutron: When True resources are provisioned via neutron, when
+ False resources are provisioned via nova.
+ :param floating_network_id: The id of the network used to provision a
+ floating IP. Only used if a floating IP is requested and with neutron.
+ :param floating_network_name: The name of the floating IP pool used to
+ provision the floating IP. Only used if a floating IP is requested and
+ with nova-net.
+ :returns: A dictionary with the resources in the format they are returned
+ by the API. Valid keys are 'keypair', 'floating_ip' and
+ 'security_group'.
+
+ Examples::
+
+ from tempest.common import validation_resources as vr
+ from tempest.lib import auth
+ from tempest.lib.services import clients
+
+ creds = auth.get_credentials('http://mycloud/identity/v3',
+ username='me', project_name='me',
+ password='secret', domain_name='Default')
+ osclients = clients.ServiceClients(creds, 'http://mycloud/identity/v3')
+ # Request keypair and floating IP
+ resources = dict(keypair=True, security_group=False,
+ security_group_rules=False, floating_ip=True)
+ resources = vr.create_validation_resources(
+ osclients, use_neutron=True,
+ floating_network_id='4240E68E-23DA-4C82-AC34-9FEFAA24521C',
+ **resources)
+
+ # The floating IP to be attached to the VM
+ floating_ip = resources['floating_ip']['ip']
+ """
+ # Create and Return the validation resources required to validate a VM
+ msg = ('Requested validation resources keypair %s, floating IP %s, '
+ 'security group %s')
+ LOG.debug(msg, keypair, floating_ip, security_group)
+ validation_data = {}
+ try:
+ if keypair:
+ keypair_name = data_utils.rand_name('keypair')
+ validation_data.update(
+ clients.compute.KeyPairsClient().create_keypair(
+ name=keypair_name))
+ LOG.debug("Validation resource key %s created", keypair_name)
+ if security_group:
+ validation_data['security_group'] = create_ssh_security_group(
+ clients, add_rule=security_group_rules,
+ use_neutron=use_neutron, ethertype=ethertype)
+ if floating_ip:
+ floating_ip_client = _network_service(
+ clients, use_neutron).FloatingIPsClient()
+ if use_neutron:
+ floatingip = floating_ip_client.create_floatingip(
+ floating_network_id=floating_network_id)
+ # validation_resources['floating_ip'] has historically looked
+ # like a compute API POST /os-floating-ips response, so we need
+ # to mangle it a bit for a Neutron response with different
+ # fields.
+ validation_data['floating_ip'] = floatingip['floatingip']
+ validation_data['floating_ip']['ip'] = (
+ floatingip['floatingip']['floating_ip_address'])
+ else:
+ # NOTE(mriedem): The os-floating-ips compute API was deprecated
+ # in the 2.36 microversion. Any tests for CRUD operations on
+ # floating IPs using the compute API should be capped at 2.35.
+ validation_data.update(floating_ip_client.create_floating_ip(
+ pool=floating_network_name))
+ LOG.debug("Validation resource floating IP %s created",
+ validation_data['floating_ip'])
+ except Exception as prov_exc:
+ # If something goes wrong, cleanup as much as possible before we
+ # re-raise the exception
+ with excutils.save_and_reraise_exception():
+ if validation_data:
+ # Cleanup may fail as well
+ try:
+ msg = ('Error while provisioning validation resources %s. '
+ 'Trying to cleanup what we provisioned so far: %s')
+ # The exceptions logging is already handled, so using
+ # debug here just to provide more context
+ LOG.debug(msg, prov_exc, str(validation_data))
+ clear_validation_resources(
+ clients,
+ keypair=validation_data.get('keypair', None),
+ floating_ip=validation_data.get('floating_ip', None),
+ security_group=validation_data.get('security_group',
+ None),
+ use_neutron=use_neutron)
+ except Exception as cleanup_exc:
+ msg = ('Error during cleanup of validation resources. '
+ 'The cleanup was triggered by an exception during '
+ 'the provisioning step.\n'
+ 'Provisioning exception: %s\n'
+ 'First cleanup exception: %s')
+ LOG.exception(msg, prov_exc, cleanup_exc)
+ return validation_data
+
+
+def clear_validation_resources(clients, keypair=None, floating_ip=None,
+ security_group=None, use_neutron=True):
+ """Cleanup resources for VM ping/ssh testing
+
+ Cleanup a set of resources provisioned via `create_validation_resources`.
+ In case of errors during cleanup, the exception is logged and the cleanup
+ process is continued. The first exception that was raised is re-raised
+ after the cleanup is complete.
+
+ :param clients: Instance of `tempest.lib.services.clients.ServiceClients`
+ or of a subclass of it. Resources are provisioned using clients from
+ `clients`.
+ :param keypair: A dictionary with the keypair to be deleted. Defaults to
+ None.
+ :param floating_ip: A dictionary with the floating_ip to be deleted.
+ Defaults to None.
+ :param security_group: A dictionary with the security_group to be deleted.
+ Defaults to None.
+ :param use_neutron: When True resources are provisioned via neutron, when
+ False resources are provisioned via nova.
+
+ Examples::
+
+ from tempest.common import validation_resources as vr
+ from tempest.lib import auth
+ from tempest.lib.services import clients
+
+ creds = auth.get_credentials('http://mycloud/identity/v3',
+ username='me', project_name='me',
+ password='secret', domain_name='Default')
+ osclients = clients.ServiceClients(creds, 'http://mycloud/identity/v3')
+ # Request keypair and floating IP
+ resources = dict(keypair=True, security_group=False,
+ security_group_rules=False, floating_ip=True)
+ resources = vr.create_validation_resources(
+ osclients, validation_resources=resources, use_neutron=True,
+ floating_network_id='4240E68E-23DA-4C82-AC34-9FEFAA24521C')
+
+ # Now cleanup the resources
+ try:
+ vr.clear_validation_resources(osclients, use_neutron=True,
+ **resources)
+ except Exception as e:
+ LOG.exception('Something went wrong during cleanup, ignoring')
+ """
+ has_exception = None
+ if keypair:
+ keypair_client = clients.compute.KeyPairsClient()
+ keypair_name = keypair['name']
+ try:
+ keypair_client.delete_keypair(keypair_name)
+ except lib_exc.NotFound:
+ LOG.warning(
+ "Keypair %s is not found when attempting to delete",
+ keypair_name
+ )
+ except Exception as exc:
+ LOG.exception('Exception raised while deleting key %s',
+ keypair_name)
+ if not has_exception:
+ has_exception = exc
+ network_service = _network_service(clients, use_neutron)
+ if security_group:
+ security_group_client = network_service.SecurityGroupsClient()
+ sec_id = security_group['id']
+ try:
+ security_group_client.delete_security_group(sec_id)
+ security_group_client.wait_for_resource_deletion(sec_id)
+ except lib_exc.NotFound:
+ LOG.warning("Security group %s is not found when attempting "
+ "to delete", sec_id)
+ except lib_exc.Conflict as exc:
+ LOG.exception('Conflict while deleting security '
+ 'group %s VM might not be deleted', sec_id)
+ if not has_exception:
+ has_exception = exc
+ except Exception as exc:
+ LOG.exception('Exception raised while deleting security '
+ 'group %s', sec_id)
+ if not has_exception:
+ has_exception = exc
+ if floating_ip:
+ floating_ip_client = network_service.FloatingIPsClient()
+ fip_id = floating_ip['id']
+ try:
+ if use_neutron:
+ floating_ip_client.delete_floatingip(fip_id)
+ else:
+ floating_ip_client.delete_floating_ip(fip_id)
+ except lib_exc.NotFound:
+ LOG.warning('Floating ip %s not found while attempting to '
+ 'delete', fip_id)
+ except Exception as exc:
+ LOG.exception('Exception raised while deleting ip %s', fip_id)
+ if not has_exception:
+ has_exception = exc
+ if has_exception:
+ raise has_exception
+
+
+class ValidationResourcesFixture(fixtures.Fixture):
+ """Fixture to provision and cleanup validation resources"""
+
+ DICT_KEYS = ['keypair', 'security_group', 'floating_ip']
+
+ def __init__(self, clients, keypair=False, floating_ip=False,
+ security_group=False, security_group_rules=False,
+ ethertype='IPv4', use_neutron=True, floating_network_id=None,
+ floating_network_name=None):
+ """Create a ValidationResourcesFixture
+
+ Create a ValidationResourcesFixture fixtures, which provisions the
+ resources required to be able to ping / ssh a virtual machine upon
+ setUp and clears them out upon cleanup. Resources are keypair,
+ security group, security group rules and a floating IP - depending
+ on the params.
+
+ The fixture exposes a dictionary that includes provisioned resources.
+
+ :param clients: `tempest.lib.services.clients.ServiceClients` or of a
+ subclass of it. Resources are provisioned using clients from
+ `clients`.
+ :param keypair: Whether to provision a keypair. Defaults to False.
+ :param floating_ip: Whether to provision a floating IP.
+ Defaults to False.
+ :param security_group: Whether to provision a security group.
+ Defaults to False.
+ :param security_group_rules: Whether to provision security group rules.
+ Defaults to False.
+ :param ethertype: 'IPv4' or 'IPv6'. Honoured only if neutron is used.
+ :param use_neutron: When True resources are provisioned via neutron,
+ when False resources are provisioned via nova.
+ :param floating_network_id: The id of the network used to provision a
+ floating IP. Only used if a floating IP is requested in case
+ neutron is used.
+ :param floating_network_name: The name of the floating IP pool used to
+ provision the floating IP. Only used if a floating IP is requested
+ and with nova-net.
+ :returns: A dictionary with the same keys as the input
+ `validation_resources` and the resources for values in the format
+ they are returned by the API.
+
+ Examples::
+
+ from tempest.common import validation_resources as vr
+ from tempest.lib import auth
+ from tempest.lib.services import clients
+ import testtools
+
+
+ class TestWithVR(testtools.TestCase):
+
+ def setUp(self):
+ creds = auth.get_credentials(
+ 'http://mycloud/identity/v3',
+ username='me', project_name='me',
+ password='secret', domain_name='Default')
+
+ osclients = clients.ServiceClients(
+ creds, 'http://mycloud/identity/v3')
+ # Request keypair and floating IP
+ resources = dict(keypair=True, security_group=False,
+ security_group_rules=False,
+ floating_ip=True)
+ network_id = '4240E68E-23DA-4C82-AC34-9FEFAA24521C'
+ self.vr = self.useFixture(vr.ValidationResourcesFixture(
+ osclients, use_neutron=True,
+ floating_network_id=network_id,
+ **resources)
+
+ def test_use_ip(self):
+ # The floating IP to be attached to the VM
+ floating_ip = self.vr['floating_ip']['ip']
+ """
+ self._clients = clients
+ self._keypair = keypair
+ self._floating_ip = floating_ip
+ self._security_group = security_group
+ self._security_group_rules = security_group_rules
+ self._ethertype = ethertype
+ self._use_neutron = use_neutron
+ self._floating_network_id = floating_network_id
+ self._floating_network_name = floating_network_name
+ self._validation_resources = None
+
+ def _setUp(self):
+ msg = ('Requested setup of ValidationResources keypair %s, floating '
+ 'IP %s, security group %s')
+ LOG.debug(msg, self._keypair, self._floating_ip, self._security_group)
+ self._validation_resources = create_validation_resources(
+ self._clients, keypair=self._keypair,
+ floating_ip=self._floating_ip,
+ security_group=self._security_group,
+ security_group_rules=self._security_group_rules,
+ ethertype=self._ethertype, use_neutron=self._use_neutron,
+ floating_network_id=self._floating_network_id,
+ floating_network_name=self._floating_network_name)
+ # If provisioning raises an exception we won't have anything to
+ # cleanup here, so we don't need a try-finally around provisioning
+ vr = self._validation_resources
+ self.addCleanup(clear_validation_resources, self._clients,
+ keypair=vr.get('keypair', None),
+ floating_ip=vr.get('floating_ip', None),
+ security_group=vr.get('security_group', None),
+ use_neutron=self._use_neutron)
+
+ @property
+ def resources(self):
+ return self._validation_resources
diff --git a/tempest/lib/decorators.py b/tempest/lib/decorators.py
new file mode 100644
index 0000000..e99dd24
--- /dev/null
+++ b/tempest/lib/decorators.py
@@ -0,0 +1,104 @@
+# Copyright 2015 Hewlett-Packard Development Company, L.P.
+#
+# 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 functools
+import uuid
+
+from oslo_log import log as logging
+import six
+import testtools
+
+LOG = logging.getLogger(__name__)
+
+
+def skip_because(*args, **kwargs):
+ """A decorator useful to skip tests hitting known bugs
+
+ @param bug: bug number causing the test to skip
+ @param condition: optional condition to be True for the skip to have place
+ """
+ def decorator(f):
+ @functools.wraps(f)
+ def wrapper(*func_args, **func_kwargs):
+ skip = False
+ if "condition" in kwargs:
+ if kwargs["condition"] is True:
+ skip = True
+ else:
+ skip = True
+ if "bug" in kwargs and skip is True:
+ if not kwargs['bug'].isdigit():
+ raise ValueError('bug must be a valid bug number')
+ msg = "Skipped until Bug: %s is resolved." % kwargs["bug"]
+ raise testtools.TestCase.skipException(msg)
+ return f(*func_args, **func_kwargs)
+ return wrapper
+ return decorator
+
+
+def related_bug(bug, status_code=None):
+ """A decorator useful to know solutions from launchpad bug reports
+
+ @param bug: The launchpad bug number causing the test
+ @param status_code: The status code related to the bug report
+ """
+ def decorator(f):
+ @functools.wraps(f)
+ def wrapper(*func_args, **func_kwargs):
+ try:
+ return f(*func_args, **func_kwargs)
+ except Exception as exc:
+ exc_status_code = getattr(exc, 'status_code', None)
+ if status_code is None or status_code == exc_status_code:
+ LOG.error('Hints: This test was made for the bug %s. '
+ 'The failure could be related to '
+ 'https://launchpad.net/bugs/%s', bug, bug)
+ raise exc
+ return wrapper
+ return decorator
+
+
+def idempotent_id(id):
+ """Stub for metadata decorator"""
+ if not isinstance(id, six.string_types):
+ raise TypeError('Test idempotent_id must be string not %s'
+ '' % type(id).__name__)
+ uuid.UUID(id)
+
+ def decorator(f):
+ f = testtools.testcase.attr('id-%s' % id)(f)
+ if f.__doc__:
+ f.__doc__ = 'Test idempotent id: %s\n%s' % (id, f.__doc__)
+ else:
+ f.__doc__ = 'Test idempotent id: %s' % id
+ return f
+ return decorator
+
+
+def attr(**kwargs):
+ """A decorator which applies the testtools attr decorator
+
+ This decorator applies the testtools.testcase.attr if it is in the list of
+ attributes to testtools we want to apply.
+ """
+
+ def decorator(f):
+ if 'type' in kwargs and isinstance(kwargs['type'], str):
+ f = testtools.testcase.attr(kwargs['type'])(f)
+ elif 'type' in kwargs and isinstance(kwargs['type'], list):
+ for attr in kwargs['type']:
+ f = testtools.testcase.attr(attr)(f)
+ return f
+
+ return decorator
diff --git a/tempest/lib/exceptions.py b/tempest/lib/exceptions.py
new file mode 100644
index 0000000..13af890
--- /dev/null
+++ b/tempest/lib/exceptions.py
@@ -0,0 +1,282 @@
+# 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.
+
+import testtools
+
+
+class TempestException(Exception):
+ """Base Tempest Exception
+
+ To correctly use this class, inherit from it and define
+ a 'message' property. That message will get printf'd
+ with the keyword arguments provided to the constructor.
+ """
+ message = "An unknown exception occurred"
+
+ def __init__(self, *args, **kwargs):
+ super(TempestException, self).__init__()
+ try:
+ self._error_string = self.message % kwargs
+ except Exception:
+ # at least get the core message out if something happened
+ self._error_string = self.message
+ if args:
+ # If there is a non-kwarg parameter, assume it's the error
+ # message or reason description and tack it on to the end
+ # of the exception message
+ # Convert all arguments into their string representations...
+ args = ["%s" % arg for arg in args]
+ self._error_string = (self._error_string +
+ "\nDetails: %s" % '\n'.join(args))
+
+ def __str__(self):
+ return self._error_string
+
+ def __repr__(self):
+ return self._error_string
+
+
+class RestClientException(TempestException,
+ testtools.TestCase.failureException):
+ def __init__(self, resp_body=None, *args, **kwargs):
+ if 'resp' in kwargs:
+ self.resp = kwargs.get('resp')
+ self.resp_body = resp_body
+ message = kwargs.get("message", resp_body)
+ super(RestClientException, self).__init__(message, *args, **kwargs)
+
+
+class OtherRestClientException(RestClientException):
+ pass
+
+
+class ServerRestClientException(RestClientException):
+ pass
+
+
+class ClientRestClientException(RestClientException):
+ pass
+
+
+class InvalidHttpSuccessCode(OtherRestClientException):
+ message = "The success code is different than the expected one"
+
+
+class BadRequest(ClientRestClientException):
+ status_code = 400
+ message = "Bad request"
+
+
+class Unauthorized(ClientRestClientException):
+ status_code = 401
+ message = 'Unauthorized'
+
+
+class Forbidden(ClientRestClientException):
+ status_code = 403
+ message = "Forbidden"
+
+
+class NotFound(ClientRestClientException):
+ status_code = 404
+ message = "Object not found"
+
+
+class Conflict(ClientRestClientException):
+ status_code = 409
+ message = "Conflict with state of target resource"
+
+
+class Gone(ClientRestClientException):
+ status_code = 410
+ message = "The requested resource is no longer available"
+
+
+class PreconditionFailed(ClientRestClientException):
+ status_code = 412
+ message = "Precondition Failed"
+
+
+class RateLimitExceeded(ClientRestClientException):
+ status_code = 413
+ message = "Rate limit exceeded"
+
+
+class OverLimit(ClientRestClientException):
+ status_code = 413
+ message = "Request entity is too large"
+
+
+class InvalidContentType(ClientRestClientException):
+ status_code = 415
+ message = "Invalid content type provided"
+
+
+class UnprocessableEntity(ClientRestClientException):
+ status_code = 422
+ message = "Unprocessable entity"
+
+
+class ServerFault(ServerRestClientException):
+ status_code = 500
+ message = "Got server fault"
+
+
+class NotImplemented(ServerRestClientException):
+ status_code = 501
+ message = "Got NotImplemented error"
+
+
+class TimeoutException(OtherRestClientException):
+ message = "Request timed out"
+
+
+class ResponseWithNonEmptyBody(OtherRestClientException):
+ message = ("RFC Violation! Response with %(status)d HTTP Status Code "
+ "MUST NOT have a body")
+
+
+class ResponseWithEntity(OtherRestClientException):
+ message = ("RFC Violation! Response with 205 HTTP Status Code "
+ "MUST NOT have an entity")
+
+
+class InvalidHTTPResponseBody(OtherRestClientException):
+ message = "HTTP response body is invalid json or xml"
+
+
+class InvalidHTTPResponseHeader(OtherRestClientException):
+ message = "HTTP response header is invalid"
+
+
+class UnexpectedContentType(OtherRestClientException):
+ message = "Unexpected content type provided"
+
+
+class UnexpectedResponseCode(OtherRestClientException):
+ message = "Unexpected response code received"
+
+
+class InvalidConfiguration(TempestException):
+ message = "Invalid Configuration"
+
+
+class InvalidIdentityVersion(TempestException):
+ message = "Invalid version %(identity_version)s of the identity service"
+
+
+class InvalidStructure(TempestException):
+ message = "Invalid structure of table with details"
+
+
+class InvalidAPIVersionString(TempestException):
+ message = ("API Version String %(version)s is of invalid format. Must "
+ "be of format MajorNum.MinorNum or string 'latest'.")
+
+
+class JSONSchemaNotFound(TempestException):
+ message = ("JSON Schema for %(version)s is not found in\n"
+ " %(schema_versions_info)s")
+
+
+class InvalidAPIVersionRange(TempestException):
+ message = ("The API version range is invalid.")
+
+
+class BadAltAuth(TempestException):
+ """Used when trying and failing to change to alt creds.
+
+ If alt creds end up the same as primary creds, use this
+ exception. This is often going to be the case when you assume
+ project_id is in the url, but it's not.
+
+ """
+ message = "The alt auth looks the same as primary auth for %(part)s"
+
+
+class CommandFailed(Exception):
+ def __init__(self, returncode, cmd, output, stderr):
+ super(CommandFailed, self).__init__()
+ self.returncode = returncode
+ self.cmd = cmd
+ self.stdout = output
+ self.stderr = stderr
+
+ def __str__(self):
+ return ("Command '%s' returned non-zero exit status %d.\n"
+ "stdout:\n%s\n"
+ "stderr:\n%s" % (self.cmd,
+ self.returncode,
+ self.stdout,
+ self.stderr))
+
+
+class IdentityError(TempestException):
+ message = "Got identity error"
+
+
+class EndpointNotFound(TempestException):
+ message = "Endpoint not found"
+
+
+class InvalidCredentials(TempestException):
+ message = "Invalid Credentials"
+
+
+class InvalidScope(TempestException):
+ message = "Invalid Scope %(scope)s for %(auth_provider)s"
+
+
+class SSHTimeout(TempestException):
+ message = ("Connection to the %(host)s via SSH timed out.\n"
+ "User: %(user)s, Password: %(password)s")
+
+
+class SSHExecCommandFailed(TempestException):
+ """Raised when remotely executed command returns nonzero status."""
+ message = ("Command '%(command)s', exit status: %(exit_status)d, "
+ "stderr:\n%(stderr)s\n"
+ "stdout:\n%(stdout)s")
+
+
+class UnknownServiceClient(TempestException):
+ message = "Service clients named %(services)s are not known"
+
+
+class ServiceClientRegistrationException(TempestException):
+ message = ("Error registering module %(name)s in path %(module_path)s, "
+ "with service %(service_version)s and clients "
+ "%(client_names)s: %(detailed_error)s")
+
+
+class PluginRegistrationException(TempestException):
+ message = "Error registering plugin %(name)s: %(detailed_error)s"
+
+
+class VolumeBackupException(TempestException):
+ message = "Volume backup %(backup_id)s failed and is in ERROR status"
+
+
+class DeleteErrorException(TempestException):
+ message = ("Resource %(resource_id)s failed to delete "
+ "and is in ERROR status")
+
+
+class InvalidTestResource(TempestException):
+ message = "%(name)s is not a valid %(type)s, or the name is ambiguous"
+
+
+class InvalidParam(TempestException):
+ message = ("Invalid Parameter passed: %(invalid_param)s")
diff --git a/tempest/tests/services/__init__.py b/tempest/lib/services/__init__.py
similarity index 100%
copy from tempest/tests/services/__init__.py
copy to tempest/lib/services/__init__.py
diff --git a/tempest/lib/services/clients.py b/tempest/lib/services/clients.py
new file mode 100644
index 0000000..8918a8c
--- /dev/null
+++ b/tempest/lib/services/clients.py
@@ -0,0 +1,465 @@
+# Copyright 2012 OpenStack Foundation
+# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P.
+# 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 copy
+import importlib
+import inspect
+import sys
+import warnings
+
+from debtcollector import removals
+from oslo_log import log as logging
+import testtools
+
+from tempest.lib import auth
+from tempest.lib.common.utils import misc
+from tempest.lib import exceptions
+from tempest.lib.services import compute
+from tempest.lib.services import identity
+from tempest.lib.services import image
+from tempest.lib.services import network
+from tempest.lib.services import object_storage
+from tempest.lib.services import volume
+
+warnings.simplefilter("once")
+LOG = logging.getLogger(__name__)
+
+
+def tempest_modules():
+ """Dict of service client modules available in Tempest.
+
+ Provides a dict of stable service modules available in Tempest, with
+ ``service_version`` as key, and the module object as value.
+ """
+ return {
+ 'compute': compute,
+ 'identity.v2': identity.v2,
+ 'identity.v3': identity.v3,
+ 'image.v1': image.v1,
+ 'image.v2': image.v2,
+ 'network': network,
+ 'object-storage': object_storage,
+ 'volume.v1': volume.v1,
+ 'volume.v2': volume.v2,
+ 'volume.v3': volume.v3
+ }
+
+
+def available_modules():
+ """Set of service client modules available in Tempest and plugins
+
+ Set of stable service clients from Tempest and service clients exposed
+ by plugins. This set of available modules can be used for automatic
+ configuration.
+
+ :raise PluginRegistrationException: if a plugin exposes a service_version
+ already defined by Tempest or another plugin.
+
+ Examples::
+
+ from tempest import config
+ params = {}
+ for service_version in available_modules():
+ service = service_version.split('.')[0]
+ params[service] = config.service_client_config(service)
+ service_clients = ServiceClients(creds, identity_uri,
+ client_parameters=params)
+ """
+ extra_service_versions = set([])
+ _tempest_modules = set(tempest_modules())
+ plugin_services = ClientsRegistry().get_service_clients()
+ name_conflicts = []
+ for plugin_name in plugin_services:
+ plug_service_versions = set([x['service_version'] for x in
+ plugin_services[plugin_name]])
+ # If a plugin exposes a duplicate service_version raise an exception
+ if plug_service_versions:
+ if not plug_service_versions.isdisjoint(extra_service_versions):
+ detailed_error = (
+ 'Plugin %s is trying to register a service %s already '
+ 'claimed by another one' % (plugin_name,
+ extra_service_versions &
+ plug_service_versions))
+ name_conflicts.append(exceptions.PluginRegistrationException(
+ name=plugin_name, detailed_error=detailed_error))
+ extra_service_versions |= plug_service_versions
+ if name_conflicts:
+ LOG.error(
+ 'Failed to list available modules due to name conflicts: %s',
+ name_conflicts)
+ raise testtools.MultipleExceptions(*name_conflicts)
+ return _tempest_modules | extra_service_versions
+
+
+@misc.singleton
+class ClientsRegistry(object):
+ """Registry of all service clients available from plugins"""
+
+ def __init__(self):
+ self._service_clients = {}
+
+ def register_service_client(self, plugin_name, service_client_data):
+ if plugin_name in self._service_clients:
+ detailed_error = 'Clients for plugin %s already registered'
+ raise exceptions.PluginRegistrationException(
+ name=plugin_name,
+ detailed_error=detailed_error % plugin_name)
+ self._service_clients[plugin_name] = service_client_data
+ LOG.debug("Successfully registered plugin %s in the service client "
+ "registry with configuration: %s", plugin_name,
+ service_client_data)
+
+ def get_service_clients(self):
+ return self._service_clients
+
+
+class ClientsFactory(object):
+ """Builds service clients for a service client module
+
+ This class implements the logic of feeding service client parameters
+ to service clients from a specific module. It allows setting the
+ parameters once and obtaining new instances of the clients without the
+ need of passing any parameter.
+
+ ClientsFactory can be used directly, or consumed via the `ServiceClients`
+ class, which manages the authorization part.
+ """
+
+ def __init__(self, module_path, client_names, auth_provider, **kwargs):
+ """Initialises the client factory
+
+ :param module_path: Path to module that includes all service clients.
+ All service client classes must be exposed by a single module.
+ If they are separated in different modules, defining __all__
+ in the root module can help, similar to what is done by service
+ clients in tempest.
+ :param client_names: List or set of names of the service client
+ classes.
+ :param auth_provider: The auth provider used to initialise client.
+ :param kwargs: Parameters to be passed to all clients. Parameters
+ values can be overwritten when clients are initialised, but
+ parameters cannot be deleted.
+ :raise ImportError: if the specified module_path cannot be imported
+
+ Example::
+
+ # Get credentials and an auth_provider
+ clients = ClientsFactory(
+ module_path='my_service.my_service_clients',
+ client_names=['ServiceClient1', 'ServiceClient2'],
+ auth_provider=auth_provider,
+ service='my_service',
+ region='region1')
+ my_api_client = clients.MyApiClient()
+ my_api_client_region2 = clients.MyApiClient(region='region2')
+
+ """
+ # Import the module. If it's not importable, the raised exception
+ # provides good enough information about what happened
+ _module = importlib.import_module(module_path)
+ # If any of the classes is not in the module we fail
+ for class_name in client_names:
+ # TODO(andreaf) This always passes all parameters to all clients.
+ # In future to allow clients to specify the list of parameters
+ # that they accept based out of a list of standard ones.
+
+ # Obtain the class
+ klass = self._get_class(_module, class_name)
+ final_kwargs = copy.copy(kwargs)
+
+ # Set the function as an attribute of the factory
+ setattr(self, class_name, self._get_partial_class(
+ klass, auth_provider, final_kwargs))
+
+ def _get_partial_class(self, klass, auth_provider, kwargs):
+
+ # Define a function that returns a new class instance by
+ # combining default kwargs with extra ones
+ def partial_class(alias=None, **later_kwargs):
+ """Returns a callable the initialises a service client
+
+ Builds a callable that accepts kwargs, which are passed through
+ to the __init__ of the service client, along with a set of defaults
+ set in factory at factory __init__ time.
+ Original args in the service client can only be passed as kwargs.
+
+ It accepts one extra parameter 'alias' compared to the original
+ service client. When alias is provided, the returned callable will
+ also set an attribute called with a name defined in 'alias', which
+ contains the instance of the service client.
+
+ :param alias: str Name of the attribute set on the factory once
+ the callable is invoked which contains the initialised
+ service client. If None, no attribute is set.
+ :param later_kwargs: kwargs passed through to the service client
+ __init__ on top of defaults set at factory level.
+ """
+ kwargs.update(later_kwargs)
+ _client = klass(auth_provider=auth_provider, **kwargs)
+ if alias:
+ setattr(self, alias, _client)
+ return _client
+
+ return partial_class
+
+ @classmethod
+ def _get_class(cls, module, class_name):
+ klass = getattr(module, class_name, None)
+ if not klass:
+ msg = 'Invalid class name, %s is not found in %s'
+ raise AttributeError(msg % (class_name, module))
+ if not inspect.isclass(klass):
+ msg = 'Expected a class, got %s of type %s instead'
+ raise TypeError(msg % (klass, type(klass)))
+ return klass
+
+
+class ServiceClients(object):
+ """Service client provider class
+
+ The ServiceClients object provides a useful means for tests to access
+ service clients configured for a specified set of credentials.
+ It hides some of the complexity from the authorization and configuration
+ layers.
+
+ Examples::
+
+ # johndoe is a tempest.lib.auth.Credentials type instance
+ johndoe_clients = clients.ServiceClients(johndoe, identity_uri)
+
+ # List servers in default region
+ johndoe_servers_client = johndoe_clients.compute.ServersClient()
+ johndoe_servers = johndoe_servers_client.list_servers()
+
+ # List servers in Region B
+ johndoe_servers_client_B = johndoe_clients.compute.ServersClient(
+ region='B')
+ johndoe_servers = johndoe_servers_client_B.list_servers()
+
+ """
+ # NOTE(andreaf) This class does not depend on tempest configuration
+ # and its meant for direct consumption by external clients such as tempest
+ # plugins. Tempest provides a wrapper class, `clients.Manager`, that
+ # initialises this class using values from tempest CONF object. The wrapper
+ # class should only be used by tests hosted in Tempest.
+
+ @removals.removed_kwarg('client_parameters')
+ def __init__(self, credentials, identity_uri, region=None, scope='project',
+ disable_ssl_certificate_validation=True, ca_certs=None,
+ trace_requests='', client_parameters=None, proxy_url=None):
+ """Service Clients provider
+
+ Instantiate a `ServiceClients` object, from a set of credentials and an
+ identity URI. The identity version is inferred from the credentials
+ object. Optionally auth scope can be provided.
+
+ A few parameters can be given a value which is applied as default
+ for all service clients: region, dscv, ca_certs, trace_requests.
+
+ Parameters dscv, ca_certs and trace_requests all apply to the auth
+ provider as well as any service clients provided by this manager.
+
+ Any other client parameter should be set via ClientsRegistry.
+
+ Client parameter used to be set via client_parameters, but this is
+ deprecated, and it is actually already not honoured
+ anymore: https://launchpad.net/bugs/1680915.
+
+ The list of available parameters is defined in the service clients
+ interfaces. For reference, most clients will accept 'region',
+ 'service', 'endpoint_type', 'build_timeout' and 'build_interval', which
+ are all inherited from RestClient.
+
+ The `config` module in Tempest exposes an helper function
+ `service_client_config` that can be used to extract from configuration
+ a dictionary ready to be injected in kwargs.
+
+ Exceptions are:
+ - Token clients for 'identity' must be given an 'auth_url' parameter
+ - Volume client for 'volume' accepts 'default_volume_size'
+ - Servers client from 'compute' accepts 'enable_instance_password'
+
+ If Tempest configuration is used, parameters will be loaded in the
+ Registry automatically for all service client (Tempest stable ones
+ and plugins).
+
+ Examples::
+
+ identity_params = config.service_client_config('identity')
+ params = {
+ 'identity': identity_params,
+ 'compute': {'region': 'region2'}}
+ manager = lib_manager.Manager(
+ my_creds, identity_uri, client_parameters=params)
+
+ :param credentials: An instance of `auth.Credentials`
+ :param identity_uri: URI of the identity API. This should be a
+ mandatory parameter, and it will so soon.
+ :param region: Default value of region for service clients.
+ :param scope: default scope for tokens produced by the auth provider
+ :param disable_ssl_certificate_validation: Applies to auth and to all
+ service clients.
+ :param ca_certs: Applies to auth and to all service clients.
+ :param trace_requests: Applies to auth and to all service clients.
+ :param client_parameters: Dictionary with parameters for service
+ clients. Keys of the dictionary are the service client service
+ name, as declared in `service_clients.available_modules()` except
+ for the version. Values are dictionaries of parameters that are
+ going to be passed to all clients in the service client module.
+ :param proxy_url: Applies to auth and to all service clients, set a
+ proxy url for the clients to use.
+ """
+ self._registered_services = set([])
+ self.credentials = credentials
+ self.identity_uri = identity_uri
+ if not identity_uri:
+ raise exceptions.InvalidCredentials(
+ 'ServiceClients requires a non-empty identity_uri.')
+ self.region = region
+ # Check if passed or default credentials are valid
+ if not self.credentials.is_valid():
+ raise exceptions.InvalidCredentials()
+ # Get the identity classes matching the provided credentials
+ # TODO(andreaf) Define a new interface in Credentials to get
+ # the API version from an instance
+ identity = [(k, auth.IDENTITY_VERSION[k][1]) for k in
+ auth.IDENTITY_VERSION.keys() if
+ isinstance(self.credentials, auth.IDENTITY_VERSION[k][0])]
+ # Zero matches or more than one are both not valid.
+ if len(identity) != 1:
+ raise exceptions.InvalidCredentials()
+ self.auth_version, auth_provider_class = identity[0]
+ self.dscv = disable_ssl_certificate_validation
+ self.ca_certs = ca_certs
+ self.trace_requests = trace_requests
+ self.proxy_url = proxy_url
+ # Creates an auth provider for the credentials
+ self.auth_provider = auth_provider_class(
+ self.credentials, self.identity_uri, scope=scope,
+ disable_ssl_certificate_validation=self.dscv,
+ ca_certs=self.ca_certs, trace_requests=self.trace_requests,
+ proxy_url=proxy_url)
+
+ # Setup some defaults for client parameters of registered services
+ client_parameters = client_parameters or {}
+ self.parameters = {}
+
+ # Parameters are provided for unversioned services
+ all_modules = available_modules()
+ unversioned_services = set(
+ [x.split('.')[0] for x in all_modules])
+ for service in unversioned_services:
+ self.parameters[service] = self._setup_parameters(
+ client_parameters.pop(service, {}))
+ # Check that no client parameters was supplied for unregistered clients
+ if client_parameters:
+ raise exceptions.UnknownServiceClient(
+ services=list(client_parameters.keys()))
+
+ # Register service clients from the registry (__tempest__ and plugins)
+ clients_registry = ClientsRegistry()
+ plugin_service_clients = clients_registry.get_service_clients()
+ registration_errors = []
+ for plugin in plugin_service_clients:
+ service_clients = plugin_service_clients[plugin]
+ # Each plugin returns a list of service client parameters
+ for service_client in service_clients:
+ # NOTE(andreaf) If a plugin cannot register, stop the
+ # registration process, log some details to help
+ # troubleshooting, and re-raise
+ try:
+ self.register_service_client_module(**service_client)
+ except Exception:
+ registration_errors.append(sys.exc_info())
+ LOG.exception(
+ 'Failed to register service client from plugin %s '
+ 'with parameters %s', plugin, service_client)
+ if registration_errors:
+ raise testtools.MultipleExceptions(*registration_errors)
+
+ def register_service_client_module(self, name, service_version,
+ module_path, client_names, **kwargs):
+ """Register a service client module
+
+ Initiates a client factory for the specified module, using this
+ class auth_provider, and accessible via a `name` attribute in the
+ service client.
+
+ :param name: Name used to access the client
+ :param service_version: Name of the service complete with version.
+ Used to track registered services. When a plugin implements it,
+ it can be used by other plugins to obtain their configuration.
+ :param module_path: Path to module that includes all service clients.
+ All service client classes must be exposed by a single module.
+ If they are separated in different modules, defining __all__
+ in the root module can help, similar to what is done by service
+ clients in tempest.
+ :param client_names: List or set of names of service client classes.
+ :param kwargs: Extra optional parameters to be passed to all clients.
+ ServiceClient provides defaults for region, dscv, ca_certs, http
+ proxies and trace_requests.
+ :raise ServiceClientRegistrationException: if the provided name is
+ already in use or if service_version is already registered.
+ :raise ImportError: if module_path cannot be imported.
+ """
+ if hasattr(self, name):
+ using_name = getattr(self, name)
+ detailed_error = 'Module name already in use: %s' % using_name
+ raise exceptions.ServiceClientRegistrationException(
+ name=name, service_version=service_version,
+ module_path=module_path, client_names=client_names,
+ detailed_error=detailed_error)
+ if service_version in self.registered_services:
+ detailed_error = 'Service %s already registered.' % service_version
+ raise exceptions.ServiceClientRegistrationException(
+ name=name, service_version=service_version,
+ module_path=module_path, client_names=client_names,
+ detailed_error=detailed_error)
+ params = dict(region=self.region,
+ disable_ssl_certificate_validation=self.dscv,
+ ca_certs=self.ca_certs,
+ trace_requests=self.trace_requests,
+ proxy_url=self.proxy_url)
+ params.update(kwargs)
+ # Instantiate the client factory
+ _factory = ClientsFactory(module_path=module_path,
+ client_names=client_names,
+ auth_provider=self.auth_provider,
+ **params)
+ # Adds the client factory to the service_client
+ setattr(self, name, _factory)
+ # Add the name of the new service in self.SERVICES for discovery
+ self._registered_services.add(service_version)
+
+ @property
+ def registered_services(self):
+ return self._registered_services
+
+ def _setup_parameters(self, parameters):
+ """Setup default values for client parameters
+
+ Region by default is the region passed as an __init__ parameter.
+ Checks that no parameter for an unknown service is provided.
+ """
+ _parameters = {}
+ # Use region from __init__
+ if self.region:
+ _parameters['region'] = self.region
+ # Update defaults with specified parameters
+ _parameters.update(parameters)
+ # If any parameter is left, parameters for an unknown service were
+ # provided as input. Fail rather than ignore silently.
+ return _parameters
diff --git a/tempest/lib/services/compute/__init__.py b/tempest/lib/services/compute/__init__.py
new file mode 100644
index 0000000..91e896a
--- /dev/null
+++ b/tempest/lib/services/compute/__init__.py
@@ -0,0 +1,77 @@
+# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P.
+#
+# 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.compute.agents_client import AgentsClient
+from tempest.lib.services.compute.aggregates_client import AggregatesClient
+from tempest.lib.services.compute.availability_zone_client import \
+ AvailabilityZoneClient
+from tempest.lib.services.compute.baremetal_nodes_client import \
+ BaremetalNodesClient
+from tempest.lib.services.compute.certificates_client import \
+ CertificatesClient
+from tempest.lib.services.compute.extensions_client import \
+ ExtensionsClient
+from tempest.lib.services.compute.fixed_ips_client import FixedIPsClient
+from tempest.lib.services.compute.flavors_client import FlavorsClient
+from tempest.lib.services.compute.floating_ip_pools_client import \
+ FloatingIPPoolsClient
+from tempest.lib.services.compute.floating_ips_bulk_client import \
+ FloatingIPsBulkClient
+from tempest.lib.services.compute.floating_ips_client import \
+ FloatingIPsClient
+from tempest.lib.services.compute.hosts_client import HostsClient
+from tempest.lib.services.compute.hypervisor_client import \
+ HypervisorClient
+from tempest.lib.services.compute.images_client import ImagesClient
+from tempest.lib.services.compute.instance_usage_audit_log_client import \
+ InstanceUsagesAuditLogClient
+from tempest.lib.services.compute.interfaces_client import InterfacesClient
+from tempest.lib.services.compute.keypairs_client import KeyPairsClient
+from tempest.lib.services.compute.limits_client import LimitsClient
+from tempest.lib.services.compute.migrations_client import MigrationsClient
+from tempest.lib.services.compute.networks_client import NetworksClient
+from tempest.lib.services.compute.quota_classes_client import \
+ QuotaClassesClient
+from tempest.lib.services.compute.quotas_client import QuotasClient
+from tempest.lib.services.compute.security_group_default_rules_client import \
+ SecurityGroupDefaultRulesClient
+from tempest.lib.services.compute.security_group_rules_client import \
+ SecurityGroupRulesClient
+from tempest.lib.services.compute.security_groups_client import \
+ SecurityGroupsClient
+from tempest.lib.services.compute.server_groups_client import \
+ ServerGroupsClient
+from tempest.lib.services.compute.servers_client import ServersClient
+from tempest.lib.services.compute.services_client import ServicesClient
+from tempest.lib.services.compute.snapshots_client import SnapshotsClient
+from tempest.lib.services.compute.tenant_networks_client import \
+ TenantNetworksClient
+from tempest.lib.services.compute.tenant_usages_client import \
+ TenantUsagesClient
+from tempest.lib.services.compute.versions_client import VersionsClient
+from tempest.lib.services.compute.volumes_client import \
+ VolumesClient
+
+__all__ = ['AgentsClient', 'AggregatesClient', 'AvailabilityZoneClient',
+ 'BaremetalNodesClient', 'CertificatesClient', 'ExtensionsClient',
+ 'FixedIPsClient', 'FlavorsClient', 'FloatingIPPoolsClient',
+ 'FloatingIPsBulkClient', 'FloatingIPsClient', 'HostsClient',
+ 'HypervisorClient', 'ImagesClient', 'InstanceUsagesAuditLogClient',
+ 'InterfacesClient', 'KeyPairsClient', 'LimitsClient',
+ 'MigrationsClient', 'NetworksClient', 'QuotaClassesClient',
+ 'QuotasClient', 'SecurityGroupDefaultRulesClient',
+ 'SecurityGroupRulesClient', 'SecurityGroupsClient',
+ 'ServerGroupsClient', 'ServersClient', 'ServicesClient',
+ 'SnapshotsClient', 'TenantNetworksClient', 'TenantUsagesClient',
+ 'VersionsClient', 'VolumesClient']
diff --git a/tempest/lib/services/compute/agents_client.py b/tempest/lib/services/compute/agents_client.py
new file mode 100644
index 0000000..408f75d
--- /dev/null
+++ b/tempest/lib/services/compute/agents_client.py
@@ -0,0 +1,76 @@
+# Copyright 2014 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 oslo_serialization import jsonutils as json
+from six.moves.urllib import parse as urllib
+
+from tempest.lib.api_schema.response.compute.v2_1 import agents as schema
+from tempest.lib.common import rest_client
+from tempest.lib.services.compute import base_compute_client
+
+
+class AgentsClient(base_compute_client.BaseComputeClient):
+ """Tests Agents API"""
+
+ def list_agents(self, **params):
+ """List all agent builds.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#list-agent-builds
+ """
+ url = 'os-agents'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.validate_response(schema.list_agents, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def create_agent(self, **kwargs):
+ """Create an agent build.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#create-agent-build
+ """
+ post_body = json.dumps({'agent': kwargs})
+ resp, body = self.post('os-agents', post_body)
+ body = json.loads(body)
+ self.validate_response(schema.create_agent, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_agent(self, agent_id):
+ """Delete an existing agent build.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#delete-agent-build
+ """
+ resp, body = self.delete("os-agents/%s" % agent_id)
+ self.validate_response(schema.delete_agent, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_agent(self, agent_id, **kwargs):
+ """Update an agent build.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#update-agent-build
+ """
+ put_body = json.dumps({'para': kwargs})
+ resp, body = self.put('os-agents/%s' % agent_id, put_body)
+ body = json.loads(body)
+ self.validate_response(schema.update_agent, resp, body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/compute/aggregates_client.py b/tempest/lib/services/compute/aggregates_client.py
new file mode 100644
index 0000000..713d7a3
--- /dev/null
+++ b/tempest/lib/services/compute/aggregates_client.py
@@ -0,0 +1,126 @@
+# Copyright 2013 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 oslo_serialization import jsonutils as json
+
+from tempest.lib.api_schema.response.compute.v2_1 import aggregates as schema
+from tempest.lib.common import rest_client
+from tempest.lib import exceptions as lib_exc
+from tempest.lib.services.compute import base_compute_client
+
+
+class AggregatesClient(base_compute_client.BaseComputeClient):
+
+ def list_aggregates(self):
+ """Get aggregate list."""
+ resp, body = self.get("os-aggregates")
+ body = json.loads(body)
+ self.validate_response(schema.list_aggregates, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_aggregate(self, aggregate_id):
+ """Get details of the given aggregate."""
+ resp, body = self.get("os-aggregates/%s" % aggregate_id)
+ body = json.loads(body)
+ self.validate_response(schema.get_aggregate, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def create_aggregate(self, **kwargs):
+ """Create a new aggregate.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#create-aggregate
+ """
+ post_body = json.dumps({'aggregate': kwargs})
+ resp, body = self.post('os-aggregates', post_body)
+
+ body = json.loads(body)
+ self.validate_response(schema.create_aggregate, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_aggregate(self, aggregate_id, **kwargs):
+ """Update an aggregate.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#update-aggregate
+ """
+ put_body = json.dumps({'aggregate': kwargs})
+ resp, body = self.put('os-aggregates/%s' % aggregate_id, put_body)
+
+ body = json.loads(body)
+ self.validate_response(schema.update_aggregate, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_aggregate(self, aggregate_id):
+ """Delete the given aggregate."""
+ resp, body = self.delete("os-aggregates/%s" % aggregate_id)
+ self.validate_response(schema.delete_aggregate, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def is_resource_deleted(self, id):
+ try:
+ self.show_aggregate(id)
+ except lib_exc.NotFound:
+ return True
+ return False
+
+ @property
+ def resource_type(self):
+ """Return the primary type of resource this client works with."""
+ return 'aggregate'
+
+ def add_host(self, aggregate_id, **kwargs):
+ """Add a host to the given aggregate.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#add-host
+ """
+ post_body = json.dumps({'add_host': kwargs})
+ resp, body = self.post('os-aggregates/%s/action' % aggregate_id,
+ post_body)
+ body = json.loads(body)
+ self.validate_response(schema.aggregate_add_remove_host, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def remove_host(self, aggregate_id, **kwargs):
+ """Remove a host from the given aggregate.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#remove-host
+ """
+ post_body = json.dumps({'remove_host': kwargs})
+ resp, body = self.post('os-aggregates/%s/action' % aggregate_id,
+ post_body)
+ body = json.loads(body)
+ self.validate_response(schema.aggregate_add_remove_host, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def set_metadata(self, aggregate_id, **kwargs):
+ """Replace the aggregate's existing metadata with new metadata.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#create-or-update-aggregate-metadata
+ """
+ post_body = json.dumps({'set_metadata': kwargs})
+ resp, body = self.post('os-aggregates/%s/action' % aggregate_id,
+ post_body)
+ body = json.loads(body)
+ self.validate_response(schema.aggregate_set_metadata, resp, body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/compute/availability_zone_client.py b/tempest/lib/services/compute/availability_zone_client.py
new file mode 100644
index 0000000..a911191
--- /dev/null
+++ b/tempest/lib/services/compute/availability_zone_client.py
@@ -0,0 +1,36 @@
+# Copyright 2013 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 oslo_serialization import jsonutils as json
+
+from tempest.lib.api_schema.response.compute.v2_1 import availability_zone \
+ as schema
+from tempest.lib.common import rest_client
+from tempest.lib.services.compute import base_compute_client
+
+
+class AvailabilityZoneClient(base_compute_client.BaseComputeClient):
+
+ def list_availability_zones(self, detail=False):
+ url = 'os-availability-zone'
+ schema_list = schema.list_availability_zone_list
+ if detail:
+ url += '/detail'
+ schema_list = schema.list_availability_zone_list_detail
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.validate_response(schema_list, resp, body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/compute/baremetal_nodes_client.py b/tempest/lib/services/compute/baremetal_nodes_client.py
new file mode 100644
index 0000000..e44c195
--- /dev/null
+++ b/tempest/lib/services/compute/baremetal_nodes_client.py
@@ -0,0 +1,52 @@
+# Copyright 2015 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 oslo_serialization import jsonutils as json
+from six.moves.urllib import parse as urllib
+
+from tempest.lib.api_schema.response.compute.v2_1 import baremetal_nodes \
+ as schema
+from tempest.lib.common import rest_client
+from tempest.lib.services.compute import base_compute_client
+
+
+class BaremetalNodesClient(base_compute_client.BaseComputeClient):
+ """Tests Baremetal API"""
+
+ def list_baremetal_nodes(self, **params):
+ """List all baremetal nodes.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#list-bare-metal-nodes
+ """
+ url = 'os-baremetal-nodes'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.validate_response(schema.list_baremetal_nodes, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_baremetal_node(self, baremetal_node_id):
+ """Show the details of a single baremetal node.
+
+ For more information, please refer to the official API reference:
+ https://developer.openstack.org/api-ref/compute/#show-bare-metal-node-details
+ """
+ url = 'os-baremetal-nodes/%s' % baremetal_node_id
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.validate_response(schema.get_baremetal_node, resp, body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/compute/base_compute_client.py b/tempest/lib/services/compute/base_compute_client.py
new file mode 100644
index 0000000..a706a79
--- /dev/null
+++ b/tempest/lib/services/compute/base_compute_client.py
@@ -0,0 +1,91 @@
+# Copyright 2015 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.common import api_version_request
+from tempest.lib.common import api_version_utils
+from tempest.lib.common import rest_client
+from tempest.lib import exceptions
+
+COMPUTE_MICROVERSION = None
+
+
+class BaseComputeClient(rest_client.RestClient):
+ """Base compute service clients class to support microversion.
+
+ This class adds microversion to API request header if that is set
+ and provides interface to select appropriate JSON schema file for
+ response validation.
+
+ :param auth_provider: An auth provider object used to wrap requests in
+ auth
+ :param str service: The service name to use for the catalog lookup
+ :param str region: The region to use for the catalog lookup
+ :param kwargs: kwargs required by rest_client.RestClient
+ """
+
+ api_microversion_header_name = 'X-OpenStack-Nova-API-Version'
+
+ def get_headers(self):
+ headers = super(BaseComputeClient, self).get_headers()
+ if COMPUTE_MICROVERSION:
+ headers[self.api_microversion_header_name] = COMPUTE_MICROVERSION
+ return headers
+
+ def request(self, method, url, extra_headers=False, headers=None,
+ body=None, chunked=False):
+ resp, resp_body = super(BaseComputeClient, self).request(
+ method, url, extra_headers, headers, body, chunked)
+ if (COMPUTE_MICROVERSION and
+ COMPUTE_MICROVERSION != api_version_utils.LATEST_MICROVERSION):
+ api_version_utils.assert_version_header_matches_request(
+ self.api_microversion_header_name,
+ COMPUTE_MICROVERSION,
+ resp)
+ return resp, resp_body
+
+ def get_schema(self, schema_versions_info):
+ """Get JSON schema
+
+ This method provides the matching schema for requested
+ microversion.
+
+ :param schema_versions_info: List of dict which provides schema
+ information with range of valid versions.
+
+ Example::
+
+ schema_versions_info = [
+ {'min': None, 'max': '2.1', 'schema': schemav21},
+ {'min': '2.2', 'max': '2.9', 'schema': schemav22},
+ {'min': '2.10', 'max': None, 'schema': schemav210}]
+ """
+ schema = None
+ version = api_version_request.APIVersionRequest(COMPUTE_MICROVERSION)
+ for items in schema_versions_info:
+ min_version = api_version_request.APIVersionRequest(items['min'])
+ max_version = api_version_request.APIVersionRequest(items['max'])
+ # This is case where COMPUTE_MICROVERSION is None, which means
+ # request without microversion So select base v2.1 schema.
+ if version.is_null() and items['min'] is None:
+ schema = items['schema']
+ break
+ # else select appropriate schema as per COMPUTE_MICROVERSION
+ elif version.matches(min_version, max_version):
+ schema = items['schema']
+ break
+ if schema is None:
+ raise exceptions.JSONSchemaNotFound(
+ version=version.get_string(),
+ schema_versions_info=schema_versions_info)
+ return schema
diff --git a/tempest/lib/services/compute/certificates_client.py b/tempest/lib/services/compute/certificates_client.py
new file mode 100644
index 0000000..822756c
--- /dev/null
+++ b/tempest/lib/services/compute/certificates_client.py
@@ -0,0 +1,38 @@
+# Copyright 2013 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.api_schema.response.compute.v2_1 import certificates as schema
+from tempest.lib.common import rest_client
+from tempest.lib.services.compute import base_compute_client
+
+
+class CertificatesClient(base_compute_client.BaseComputeClient):
+
+ def show_certificate(self, certificate_id):
+ url = "os-certificates/%s" % certificate_id
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.validate_response(schema.get_certificate, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def create_certificate(self):
+ """Create a certificate."""
+ url = "os-certificates"
+ resp, body = self.post(url, None)
+ body = json.loads(body)
+ self.validate_response(schema.create_certificate, resp, body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/compute/extensions_client.py b/tempest/lib/services/compute/extensions_client.py
new file mode 100644
index 0000000..afaf282
--- /dev/null
+++ b/tempest/lib/services/compute/extensions_client.py
@@ -0,0 +1,35 @@
+# 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.api_schema.response.compute.v2_1 import extensions as schema
+from tempest.lib.common import rest_client
+from tempest.lib.services.compute import base_compute_client
+
+
+class ExtensionsClient(base_compute_client.BaseComputeClient):
+
+ def list_extensions(self):
+ url = 'extensions'
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.validate_response(schema.list_extensions, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_extension(self, extension_alias):
+ resp, body = self.get('extensions/%s' % extension_alias)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/compute/fixed_ips_client.py b/tempest/lib/services/compute/fixed_ips_client.py
new file mode 100644
index 0000000..968646c
--- /dev/null
+++ b/tempest/lib/services/compute/fixed_ips_client.py
@@ -0,0 +1,42 @@
+# Copyright 2013 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.api_schema.response.compute.v2_1 import fixed_ips as schema
+from tempest.lib.common import rest_client
+from tempest.lib.services.compute import base_compute_client
+
+
+class FixedIPsClient(base_compute_client.BaseComputeClient):
+
+ def show_fixed_ip(self, fixed_ip):
+ url = "os-fixed-ips/%s" % fixed_ip
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.validate_response(schema.get_fixed_ip, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def reserve_fixed_ip(self, fixed_ip, **kwargs):
+ """Reserve/Unreserve a fixed IP.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#reserve-or-release-a-fixed-ip
+ """
+ url = "os-fixed-ips/%s/action" % fixed_ip
+ resp, body = self.post(url, json.dumps(kwargs))
+ self.validate_response(schema.reserve_unreserve_fixed_ip, resp, body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/compute/flavors_client.py b/tempest/lib/services/compute/flavors_client.py
new file mode 100644
index 0000000..4923d7e
--- /dev/null
+++ b/tempest/lib/services/compute/flavors_client.py
@@ -0,0 +1,257 @@
+# 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.api_schema.response.compute.v2_1 import flavors as schema
+from tempest.lib.api_schema.response.compute.v2_1 import flavors_access \
+ 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.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#list-flavors
+ https://developer.openstack.org/api-ref/compute/#list-flavors-with-details
+ """
+ url = '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)
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.validate_response(_schema, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_flavor(self, flavor_id):
+ """Shows details for a flavor.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#show-flavor-details
+ """
+ resp, body = self.get("flavors/%s" % flavor_id)
+ 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 create_flavor(self, **kwargs):
+ """Create a new 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/#create-flavor
+ """
+ if 'ephemeral' in kwargs:
+ kwargs['OS-FLV-EXT-DATA:ephemeral'] = kwargs.pop('ephemeral')
+ if 'is_public' in kwargs:
+ kwargs['os-flavor-access:is_public'] = kwargs.pop('is_public')
+
+ post_body = json.dumps({'flavor': kwargs})
+ resp, body = self.post('flavors', post_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 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):
+ """Delete the given flavor.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#delete-flavor
+ """
+ resp, body = self.delete("flavors/{0}".format(flavor_id))
+ self.validate_response(schema.delete_flavor, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def is_resource_deleted(self, id):
+ # Did not use show_flavor(id) for verification as it gives
+ # 200 ok even for deleted id. LP #981263
+ # we can remove the loop here and use get by ID when bug gets sortedout
+ flavors = self.list_flavors(detail=True)['flavors']
+ for flavor in flavors:
+ if flavor['id'] == id:
+ return False
+ return True
+
+ @property
+ def resource_type(self):
+ """Return the primary type of resource this client works with."""
+ return 'flavor'
+
+ def set_flavor_extra_spec(self, flavor_id, **kwargs):
+ """Set extra Specs to the mentioned flavor.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#create-extra-specs-for-a-flavor
+ """
+ post_body = json.dumps({'extra_specs': kwargs})
+ resp, body = self.post('flavors/%s/os-extra_specs' % flavor_id,
+ post_body)
+ body = json.loads(body)
+ self.validate_response(schema_extra_specs.set_get_flavor_extra_specs,
+ resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_flavor_extra_specs(self, flavor_id):
+ """Get extra Specs details of the mentioned flavor.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#list-extra-specs-for-a-flavor
+ """
+ resp, body = self.get('flavors/%s/os-extra_specs' % flavor_id)
+ body = json.loads(body)
+ self.validate_response(schema_extra_specs.set_get_flavor_extra_specs,
+ resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_flavor_extra_spec(self, flavor_id, key):
+ """Get extra Specs key-value of the mentioned flavor and key.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#show-an-extra-spec-for-a-flavor
+ """
+ resp, body = self.get('flavors/%s/os-extra_specs/%s' % (flavor_id,
+ key))
+ body = json.loads(body)
+ self.validate_response(
+ schema_extra_specs.set_get_flavor_extra_specs_key,
+ resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_flavor_extra_spec(self, flavor_id, key, **kwargs):
+ """Update specified extra Specs of the mentioned flavor and key.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#update-an-extra-spec-for-a-flavor
+ """
+ resp, body = self.put('flavors/%s/os-extra_specs/%s' %
+ (flavor_id, key), json.dumps(kwargs))
+ body = json.loads(body)
+ self.validate_response(
+ schema_extra_specs.set_get_flavor_extra_specs_key,
+ resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def unset_flavor_extra_spec(self, flavor_id, key): # noqa
+ # NOTE: This noqa is for passing T111 check and we cannot rename
+ # to keep backwards compatibility.
+ """Unset extra Specs from the mentioned flavor.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#delete-an-extra-spec-for-a-flavor
+ """
+ resp, body = self.delete('flavors/%s/os-extra_specs/%s' %
+ (flavor_id, key))
+ self.validate_response(schema.unset_flavor_extra_specs, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_flavor_access(self, flavor_id):
+ """Get flavor access information given the flavor id.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#list-flavor-access-information-for-given-flavor
+ """
+ resp, body = self.get('flavors/%s/os-flavor-access' % flavor_id)
+ body = json.loads(body)
+ self.validate_response(schema_access.add_remove_list_flavor_access,
+ resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def add_flavor_access(self, flavor_id, tenant_id):
+ """Add flavor access for the specified tenant.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#add-flavor-access-to-tenant-addtenantaccess-action
+ """
+ post_body = {
+ 'addTenantAccess': {
+ 'tenant': tenant_id
+ }
+ }
+ post_body = json.dumps(post_body)
+ resp, body = self.post('flavors/%s/action' % flavor_id, post_body)
+ body = json.loads(body)
+ self.validate_response(schema_access.add_remove_list_flavor_access,
+ resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def remove_flavor_access(self, flavor_id, tenant_id):
+ """Remove flavor access from the specified tenant.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#remove-flavor-access-from-tenant-removetenantaccess-action
+ """
+ post_body = {
+ 'removeTenantAccess': {
+ 'tenant': tenant_id
+ }
+ }
+ post_body = json.dumps(post_body)
+ resp, body = self.post('flavors/%s/action' % flavor_id, post_body)
+ body = json.loads(body)
+ self.validate_response(schema_access.add_remove_list_flavor_access,
+ resp, body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/compute/floating_ip_pools_client.py b/tempest/lib/services/compute/floating_ip_pools_client.py
new file mode 100644
index 0000000..d3af050
--- /dev/null
+++ b/tempest/lib/services/compute/floating_ip_pools_client.py
@@ -0,0 +1,35 @@
+# 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.api_schema.response.compute.v2_1 import floating_ips as schema
+from tempest.lib.common import rest_client
+from tempest.lib.services.compute import base_compute_client
+
+
+class FloatingIPPoolsClient(base_compute_client.BaseComputeClient):
+
+ def list_floating_ip_pools(self, params=None):
+ """Gets all floating IP Pools list."""
+ url = 'os-floating-ip-pools'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.validate_response(schema.list_floating_ip_pools, resp, body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/compute/floating_ips_bulk_client.py b/tempest/lib/services/compute/floating_ips_bulk_client.py
new file mode 100644
index 0000000..5f06009
--- /dev/null
+++ b/tempest/lib/services/compute/floating_ips_bulk_client.py
@@ -0,0 +1,51 @@
+# 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.api_schema.response.compute.v2_1 import floating_ips as schema
+from tempest.lib.common import rest_client
+from tempest.lib.services.compute import base_compute_client
+
+
+class FloatingIPsBulkClient(base_compute_client.BaseComputeClient):
+
+ def create_floating_ips_bulk(self, ip_range, pool, interface):
+ """Allocate floating IPs in bulk."""
+ post_body = {
+ 'ip_range': ip_range,
+ 'pool': pool,
+ 'interface': interface
+ }
+ post_body = json.dumps({'floating_ips_bulk_create': post_body})
+ resp, body = self.post('os-floating-ips-bulk', post_body)
+ body = json.loads(body)
+ self.validate_response(schema.create_floating_ips_bulk, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_floating_ips_bulk(self):
+ """Gets all floating IPs in bulk."""
+ resp, body = self.get('os-floating-ips-bulk')
+ body = json.loads(body)
+ self.validate_response(schema.list_floating_ips_bulk, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_floating_ips_bulk(self, ip_range):
+ """Deletes the provided floating IPs in bulk."""
+ post_body = json.dumps({'ip_range': ip_range})
+ resp, body = self.put('os-floating-ips-bulk/delete', post_body)
+ body = json.loads(body)
+ self.validate_response(schema.delete_floating_ips_bulk, resp, body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/compute/floating_ips_client.py b/tempest/lib/services/compute/floating_ips_client.py
new file mode 100644
index 0000000..5364d97
--- /dev/null
+++ b/tempest/lib/services/compute/floating_ips_client.py
@@ -0,0 +1,120 @@
+# 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.api_schema.response.compute.v2_1 import floating_ips as schema
+from tempest.lib.common import rest_client
+from tempest.lib import exceptions as lib_exc
+from tempest.lib.services.compute import base_compute_client
+
+
+class FloatingIPsClient(base_compute_client.BaseComputeClient):
+
+ def list_floating_ips(self, **params):
+ """Returns a list of all floating IPs filtered by any parameters.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#list-floating-ip-addresses
+ """
+ url = 'os-floating-ips'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.validate_response(schema.list_floating_ips, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_floating_ip(self, floating_ip_id):
+ """Get the details of a floating IP.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#show-floating-ip-address-details
+ """
+ url = "os-floating-ips/%s" % floating_ip_id
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.validate_response(schema.create_get_floating_ip, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def create_floating_ip(self, **kwargs):
+ """Allocate a floating IP to the project.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#create-allocate-floating-ip-address
+ """
+ url = 'os-floating-ips'
+ post_body = json.dumps(kwargs)
+ resp, body = self.post(url, post_body)
+ body = json.loads(body)
+ self.validate_response(schema.create_get_floating_ip, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_floating_ip(self, floating_ip_id):
+ """Deletes the provided floating IP from the project.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#delete-deallocate-floating-ip-address
+ """
+ url = "os-floating-ips/%s" % floating_ip_id
+ resp, body = self.delete(url)
+ self.validate_response(schema.add_remove_floating_ip, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def associate_floating_ip_to_server(self, floating_ip, server_id):
+ """Associate the provided floating IP to a specific server."""
+ url = "servers/%s/action" % server_id
+ post_body = {
+ 'addFloatingIp': {
+ 'address': floating_ip,
+ }
+ }
+
+ post_body = json.dumps(post_body)
+ resp, body = self.post(url, post_body)
+ self.validate_response(schema.add_remove_floating_ip, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def disassociate_floating_ip_from_server(self, floating_ip, server_id):
+ """Disassociate the provided floating IP from a specific server."""
+ url = "servers/%s/action" % server_id
+ post_body = {
+ 'removeFloatingIp': {
+ 'address': floating_ip,
+ }
+ }
+
+ post_body = json.dumps(post_body)
+ resp, body = self.post(url, post_body)
+ self.validate_response(schema.add_remove_floating_ip, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def is_resource_deleted(self, id):
+ try:
+ self.show_floating_ip(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 'floating_ip'
diff --git a/tempest/lib/services/compute/hosts_client.py b/tempest/lib/services/compute/hosts_client.py
new file mode 100644
index 0000000..1fdd907
--- /dev/null
+++ b/tempest/lib/services/compute/hosts_client.py
@@ -0,0 +1,104 @@
+# Copyright 2013 IBM Corp.
+#
+# 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.api_schema.response.compute.v2_1 import hosts as schema
+from tempest.lib.common import rest_client
+from tempest.lib.services.compute import base_compute_client
+
+
+class HostsClient(base_compute_client.BaseComputeClient):
+
+ def list_hosts(self, **params):
+ """List all hosts.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#list-hosts
+ """
+
+ url = 'os-hosts'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.validate_response(schema.list_hosts, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_host(self, hostname):
+ """Show detail information for the host."""
+
+ resp, body = self.get("os-hosts/%s" % hostname)
+ body = json.loads(body)
+ self.validate_response(schema.get_host_detail, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_host(self, hostname, **kwargs):
+ """Update a host.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#update-host-status
+ """
+
+ request_body = {
+ 'status': None,
+ 'maintenance_mode': None,
+ }
+ request_body.update(**kwargs)
+ request_body = json.dumps(request_body)
+
+ resp, body = self.put("os-hosts/%s" % hostname, request_body)
+ body = json.loads(body)
+ self.validate_response(schema.update_host, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def startup_host(self, hostname): # noqa
+ # NOTE: This noqa is for passing T110 check and we cannot rename
+ # to keep backwards compatibility. Actually, the root problem
+ # of this is a wrong API design. GET operation should not change
+ # resource status, but current API does that.
+ """Startup a host."""
+
+ resp, body = self.get("os-hosts/%s/startup" % hostname)
+ body = json.loads(body)
+ self.validate_response(schema.startup_host, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def shutdown_host(self, hostname): # noqa
+ # NOTE: This noqa is for passing T110 check and we cannot rename
+ # to keep backwards compatibility. Actually, the root problem
+ # of this is a wrong API design. GET operation should not change
+ # resource status, but current API does that.
+ """Shutdown a host."""
+
+ resp, body = self.get("os-hosts/%s/shutdown" % hostname)
+ body = json.loads(body)
+ self.validate_response(schema.shutdown_host, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def reboot_host(self, hostname): # noqa
+ # NOTE: This noqa is for passing T110 check and we cannot rename
+ # to keep backwards compatibility. Actually, the root problem
+ # of this is a wrong API design. GET operation should not change
+ # resource status, but current API does that.
+ """Reboot a host."""
+
+ resp, body = self.get("os-hosts/%s/reboot" % hostname)
+ body = json.loads(body)
+ self.validate_response(schema.reboot_host, resp, body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/compute/hypervisor_client.py b/tempest/lib/services/compute/hypervisor_client.py
new file mode 100644
index 0000000..23c304e
--- /dev/null
+++ b/tempest/lib/services/compute/hypervisor_client.py
@@ -0,0 +1,73 @@
+# Copyright 2013 IBM 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.api_schema.response.compute.v2_1 import hypervisors as schema
+from tempest.lib.common import rest_client
+from tempest.lib.services.compute import base_compute_client
+
+
+class HypervisorClient(base_compute_client.BaseComputeClient):
+
+ def list_hypervisors(self, detail=False):
+ """List hypervisors information."""
+ url = 'os-hypervisors'
+ _schema = schema.list_search_hypervisors
+ if detail:
+ url += '/detail'
+ _schema = schema.list_hypervisors_detail
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.validate_response(_schema, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_hypervisor(self, hypervisor_id):
+ """Display the details of the specified hypervisor."""
+ resp, body = self.get('os-hypervisors/%s' % hypervisor_id)
+ body = json.loads(body)
+ self.validate_response(schema.get_hypervisor, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_servers_on_hypervisor(self, hypervisor_name):
+ """List instances belonging to the specified hypervisor."""
+ resp, body = self.get('os-hypervisors/%s/servers' % hypervisor_name)
+ body = json.loads(body)
+ self.validate_response(schema.get_hypervisors_servers, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_hypervisor_statistics(self):
+ """Get hypervisor statistics over all compute nodes."""
+ resp, body = self.get('os-hypervisors/statistics')
+ body = json.loads(body)
+ self.validate_response(schema.get_hypervisor_statistics, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_hypervisor_uptime(self, hypervisor_id):
+ """Display the uptime of the specified hypervisor."""
+ resp, body = self.get('os-hypervisors/%s/uptime' % hypervisor_id)
+ body = json.loads(body)
+ self.validate_response(schema.get_hypervisor_uptime, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def search_hypervisor(self, hypervisor_name): # noqa
+ # NOTE: This noqa is for passing T110 check and we cannot rename
+ # to keep backwards compatibility.
+ """Search specified hypervisor."""
+ resp, body = self.get('os-hypervisors/%s/search' % hypervisor_name)
+ body = json.loads(body)
+ self.validate_response(schema.list_search_hypervisors, resp, body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/compute/images_client.py b/tempest/lib/services/compute/images_client.py
new file mode 100644
index 0000000..0f4eb42
--- /dev/null
+++ b/tempest/lib/services/compute/images_client.py
@@ -0,0 +1,158 @@
+# 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.api_schema.response.compute.v2_1 import images as schema
+from tempest.lib.api_schema.response.compute.v2_45 import images as schemav245
+from tempest.lib.common import rest_client
+from tempest.lib import exceptions as lib_exc
+from tempest.lib.services.compute import base_compute_client
+
+
+class ImagesClient(base_compute_client.BaseComputeClient):
+
+ schema_versions_info = [
+ {'min': None, 'max': '2.44', 'schema': schema},
+ {'min': '2.45', 'max': None, 'schema': schemav245}]
+
+ def create_image(self, server_id, **kwargs):
+ """Create an image of the original server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#create-image-createimage-action
+ """
+
+ post_body = {'createImage': kwargs}
+ post_body = json.dumps(post_body)
+ resp, body = self.post('servers/%s/action' % server_id,
+ post_body)
+ _schema = self.get_schema(self.schema_versions_info)
+ if body:
+ body = json.loads(body)
+ self.validate_response(_schema.create_image, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_images(self, detail=False, **params):
+ """Return a list of all images filtered by any parameter.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#list-images
+ https://developer.openstack.org/api-ref/compute/#list-images-with-details
+ """
+ url = 'images'
+ _schema = schema.list_images
+ if detail:
+ url += '/detail'
+ _schema = schema.list_images_details
+
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.validate_response(_schema, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_image(self, image_id):
+ """Return the details of a single image."""
+ resp, body = self.get("images/%s" % image_id)
+ body = json.loads(body)
+ self.validate_response(schema.get_image, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_image(self, image_id):
+ """Delete the provided image."""
+ resp, body = self.delete("images/%s" % image_id)
+ self.validate_response(schema.delete, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_image_metadata(self, image_id):
+ """List all metadata items for an image."""
+ resp, body = self.get("images/%s/metadata" % image_id)
+ body = json.loads(body)
+ self.validate_response(schema.image_metadata, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def set_image_metadata(self, image_id, meta):
+ """Set the metadata for an image.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#update-image-metadata
+ """
+ post_body = json.dumps({'metadata': meta})
+ resp, body = self.put('images/%s/metadata' % image_id, post_body)
+ body = json.loads(body)
+ self.validate_response(schema.image_metadata, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_image_metadata(self, image_id, meta):
+ """Update the metadata for an image.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#create-image-metadata
+ """
+ post_body = json.dumps({'metadata': meta})
+ resp, body = self.post('images/%s/metadata' % image_id, post_body)
+ body = json.loads(body)
+ self.validate_response(schema.image_metadata, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_image_metadata_item(self, image_id, key):
+ """Return the value for a specific image metadata key."""
+ resp, body = self.get("images/%s/metadata/%s" % (image_id, key))
+ body = json.loads(body)
+ self.validate_response(schema.image_meta_item, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def set_image_metadata_item(self, image_id, key, meta):
+ """Set the value for a specific image metadata key.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#create-or-update-image-metadata-item
+ """
+ post_body = json.dumps({'meta': meta})
+ resp, body = self.put('images/%s/metadata/%s' % (image_id, key),
+ post_body)
+ body = json.loads(body)
+ self.validate_response(schema.image_meta_item, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_image_metadata_item(self, image_id, key):
+ """Delete a single image metadata key/value pair."""
+ resp, body = self.delete("images/%s/metadata/%s" %
+ (image_id, key))
+ self.validate_response(schema.delete, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def is_resource_deleted(self, id):
+ # Added status check for user with admin role
+ try:
+ if self.show_image(id)['image']['status'] == 'DELETED':
+ return True
+ except lib_exc.NotFound:
+ return True
+ return False
+
+ @property
+ def resource_type(self):
+ """Return the primary type of resource this client works with."""
+ return 'image'
diff --git a/tempest/lib/services/compute/instance_usage_audit_log_client.py b/tempest/lib/services/compute/instance_usage_audit_log_client.py
new file mode 100644
index 0000000..1b94306
--- /dev/null
+++ b/tempest/lib/services/compute/instance_usage_audit_log_client.py
@@ -0,0 +1,39 @@
+# Copyright 2013 IBM 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.api_schema.response.compute.v2_1 import \
+ instance_usage_audit_logs as schema
+from tempest.lib.common import rest_client
+from tempest.lib.services.compute import base_compute_client
+
+
+class InstanceUsagesAuditLogClient(base_compute_client.BaseComputeClient):
+
+ def list_instance_usage_audit_logs(self):
+ url = 'os-instance_usage_audit_log'
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.validate_response(schema.list_instance_usage_audit_log,
+ resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_instance_usage_audit_log(self, time_before):
+ url = 'os-instance_usage_audit_log/%s' % time_before
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.validate_response(schema.get_instance_usage_audit_log, resp, body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/compute/interfaces_client.py b/tempest/lib/services/compute/interfaces_client.py
new file mode 100644
index 0000000..d7c3107
--- /dev/null
+++ b/tempest/lib/services/compute/interfaces_client.py
@@ -0,0 +1,57 @@
+# Copyright 2013 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.api_schema.response.compute.v2_1 import interfaces as schema
+from tempest.lib.common import rest_client
+from tempest.lib.services.compute import base_compute_client
+
+
+class InterfacesClient(base_compute_client.BaseComputeClient):
+
+ def list_interfaces(self, server_id):
+ resp, body = self.get('servers/%s/os-interface' % server_id)
+ body = json.loads(body)
+ self.validate_response(schema.list_interfaces, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def create_interface(self, server_id, **kwargs):
+ """Create an interface.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#create-interface
+ """
+ post_body = {'interfaceAttachment': kwargs}
+ post_body = json.dumps(post_body)
+ resp, body = self.post('servers/%s/os-interface' % server_id,
+ body=post_body)
+ body = json.loads(body)
+ self.validate_response(schema.get_create_interfaces, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_interface(self, server_id, port_id):
+ resp, body = self.get('servers/%s/os-interface/%s' % (server_id,
+ port_id))
+ body = json.loads(body)
+ self.validate_response(schema.get_create_interfaces, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_interface(self, server_id, port_id):
+ resp, body = self.delete('servers/%s/os-interface/%s' % (server_id,
+ port_id))
+ self.validate_response(schema.delete_interface, resp, body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/compute/keypairs_client.py b/tempest/lib/services/compute/keypairs_client.py
new file mode 100644
index 0000000..5215fca
--- /dev/null
+++ b/tempest/lib/services/compute/keypairs_client.py
@@ -0,0 +1,89 @@
+# 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.api_schema.response.compute.v2_1 import keypairs as schemav21
+from tempest.lib.api_schema.response.compute.v2_2 import keypairs as schemav22
+from tempest.lib.common import rest_client
+from tempest.lib.services.compute import base_compute_client
+
+
+class KeyPairsClient(base_compute_client.BaseComputeClient):
+
+ schema_versions_info = [{'min': None, 'max': '2.1', 'schema': schemav21},
+ {'min': '2.2', 'max': None, 'schema': schemav22}]
+
+ def list_keypairs(self, **params):
+ """Lists keypairs that are associated with the account.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#list-keypairs
+ """
+ url = 'os-keypairs'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+ resp, body = self.get(url)
+ body = json.loads(body)
+ schema = self.get_schema(self.schema_versions_info)
+ self.validate_response(schema.list_keypairs, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_keypair(self, keypair_name, **params):
+ """Shows details for a keypair that is associated with the account.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#show-keypair-details
+ """
+ url = "os-keypairs/%s" % keypair_name
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+ resp, body = self.get(url)
+ body = json.loads(body)
+ schema = self.get_schema(self.schema_versions_info)
+ self.validate_response(schema.get_keypair, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def create_keypair(self, **kwargs):
+ """Create a keypair.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#create-or-import-keypair
+ """
+ post_body = json.dumps({'keypair': kwargs})
+ resp, body = self.post("os-keypairs", body=post_body)
+ body = json.loads(body)
+ schema = self.get_schema(self.schema_versions_info)
+ self.validate_response(schema.create_keypair, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_keypair(self, keypair_name, **params):
+ """Deletes a keypair.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#delete-keypair
+ """
+ url = "os-keypairs/%s" % keypair_name
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+ resp, body = self.delete(url)
+ schema = self.get_schema(self.schema_versions_info)
+ self.validate_response(schema.delete_keypair, resp, body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/compute/limits_client.py b/tempest/lib/services/compute/limits_client.py
new file mode 100644
index 0000000..efe9889
--- /dev/null
+++ b/tempest/lib/services/compute/limits_client.py
@@ -0,0 +1,29 @@
+# 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.api_schema.response.compute.v2_1 import limits as schema
+from tempest.lib.common import rest_client
+from tempest.lib.services.compute import base_compute_client
+
+
+class LimitsClient(base_compute_client.BaseComputeClient):
+
+ def show_limits(self):
+ resp, body = self.get("limits")
+ body = json.loads(body)
+ self.validate_response(schema.get_limit, resp, body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/compute/migrations_client.py b/tempest/lib/services/compute/migrations_client.py
new file mode 100644
index 0000000..68c8f3f
--- /dev/null
+++ b/tempest/lib/services/compute/migrations_client.py
@@ -0,0 +1,46 @@
+# Copyright 2014 NEC 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 oslo_serialization import jsonutils as json
+from six.moves.urllib import parse as urllib
+
+from tempest.lib.api_schema.response.compute.v2_1 import migrations as schema
+from tempest.lib.api_schema.response.compute.v2_23 import migrations \
+ as schemav223
+from tempest.lib.common import rest_client
+from tempest.lib.services.compute import base_compute_client
+
+
+class MigrationsClient(base_compute_client.BaseComputeClient):
+ schema_versions_info = [
+ {'min': None, 'max': '2.22', 'schema': schema},
+ {'min': '2.23', 'max': None, 'schema': schemav223}]
+
+ def list_migrations(self, **params):
+ """List all migrations.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#list-migrations
+ """
+
+ url = 'os-migrations'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ schema = self.get_schema(self.schema_versions_info)
+ self.validate_response(schema.list_migrations, resp, body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/compute/networks_client.py b/tempest/lib/services/compute/networks_client.py
new file mode 100644
index 0000000..6c8c943
--- /dev/null
+++ b/tempest/lib/services/compute/networks_client.py
@@ -0,0 +1,34 @@
+# 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.services.compute import base_compute_client
+
+
+class NetworksClient(base_compute_client.BaseComputeClient):
+
+ def list_networks(self):
+ resp, body = self.get("os-networks")
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_network(self, network_id):
+ resp, body = self.get("os-networks/%s" % network_id)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/compute/quota_classes_client.py b/tempest/lib/services/compute/quota_classes_client.py
new file mode 100644
index 0000000..64e06f4
--- /dev/null
+++ b/tempest/lib/services/compute/quota_classes_client.py
@@ -0,0 +1,50 @@
+# Copyright 2012 NTT Data
+# 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.api_schema.response.compute.v2_1\
+ import quota_classes as classes_schema
+from tempest.lib.common import rest_client
+from tempest.lib.services.compute import base_compute_client
+
+
+class QuotaClassesClient(base_compute_client.BaseComputeClient):
+
+ def show_quota_class_set(self, quota_class_id):
+ """List the quota class set for a quota class."""
+
+ url = 'os-quota-class-sets/%s' % quota_class_id
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.validate_response(classes_schema.get_quota_class_set, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_quota_class_set(self, quota_class_id, **kwargs):
+ """Update the quota class's limits for one or more resources.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#create-or-update-quotas-for-quota-class
+ """
+ post_body = json.dumps({'quota_class_set': kwargs})
+
+ resp, body = self.put('os-quota-class-sets/%s' % quota_class_id,
+ post_body)
+
+ body = json.loads(body)
+ self.validate_response(classes_schema.update_quota_class_set,
+ resp, body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/compute/quotas_client.py b/tempest/lib/services/compute/quotas_client.py
new file mode 100644
index 0000000..12df895
--- /dev/null
+++ b/tempest/lib/services/compute/quotas_client.py
@@ -0,0 +1,91 @@
+# Copyright 2012 NTT Data
+# 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.api_schema.response.compute.v2_1 import quotas as schema
+from tempest.lib.common import rest_client
+from tempest.lib.services.compute import base_compute_client
+
+
+class QuotasClient(base_compute_client.BaseComputeClient):
+
+ def show_quota_set(self, tenant_id, user_id=None, detail=False):
+ """List the quota set for a tenant.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#show-a-quota
+ https://developer.openstack.org/api-ref/compute/#show-the-detail-of-quota
+ """
+
+ params = {}
+ url = 'os-quota-sets/%s' % tenant_id
+ if detail:
+ url += '/detail'
+ if user_id:
+ params.update({'user_id': user_id})
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+ resp, body = self.get(url)
+ body = json.loads(body)
+ if detail:
+ self.validate_response(schema.get_quota_set_details, resp, body)
+ else:
+ self.validate_response(schema.get_quota_set, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_default_quota_set(self, tenant_id):
+ """List the default quota set for a tenant.
+
+ https://developer.openstack.org/api-ref/compute/#list-default-quotas-for-tenant
+ """
+
+ url = 'os-quota-sets/%s/defaults' % tenant_id
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.validate_response(schema.get_quota_set, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_quota_set(self, tenant_id, user_id=None, **kwargs):
+ """Updates the tenant's quota limits for one or more resources.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#update-quotas
+ """
+
+ post_body = json.dumps({'quota_set': kwargs})
+
+ if user_id:
+ resp, body = self.put('os-quota-sets/%s?user_id=%s' %
+ (tenant_id, user_id), post_body)
+ else:
+ resp, body = self.put('os-quota-sets/%s' % tenant_id,
+ post_body)
+
+ body = json.loads(body)
+ self.validate_response(schema.update_quota_set, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_quota_set(self, tenant_id):
+ """Delete the tenant's quota set.
+
+ https://developer.openstack.org/api-ref/compute/#revert-quotas-to-defaults
+ """
+ resp, body = self.delete('os-quota-sets/%s' % tenant_id)
+ self.validate_response(schema.delete_quota, resp, body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/compute/security_group_default_rules_client.py b/tempest/lib/services/compute/security_group_default_rules_client.py
new file mode 100644
index 0000000..70cab88
--- /dev/null
+++ b/tempest/lib/services/compute/security_group_default_rules_client.py
@@ -0,0 +1,65 @@
+# Copyright 2014 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 oslo_serialization import jsonutils as json
+
+from tempest.lib.api_schema.response.compute.v2_1 import \
+ security_group_default_rule as schema
+from tempest.lib.common import rest_client
+from tempest.lib.services.compute import base_compute_client
+
+
+class SecurityGroupDefaultRulesClient(base_compute_client.BaseComputeClient):
+
+ def create_security_default_group_rule(self, **kwargs):
+ """Create security group default rule.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#create-default-security-group-rule
+ """
+ post_body = json.dumps({'security_group_default_rule': kwargs})
+ url = 'os-security-group-default-rules'
+ resp, body = self.post(url, post_body)
+ body = json.loads(body)
+ self.validate_response(schema.create_get_security_group_default_rule,
+ resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_security_group_default_rule(self,
+ security_group_default_rule_id):
+ """Delete the provided Security Group default rule."""
+ resp, body = self.delete('os-security-group-default-rules/%s' % (
+ security_group_default_rule_id))
+ self.validate_response(schema.delete_security_group_default_rule,
+ resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_security_group_default_rules(self):
+ """List all Security Group default rules."""
+ resp, body = self.get('os-security-group-default-rules')
+ body = json.loads(body)
+ self.validate_response(schema.list_security_group_default_rules,
+ resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_security_group_default_rule(self, security_group_default_rule_id):
+ """Return the details of provided Security Group default rule."""
+ resp, body = self.get('os-security-group-default-rules/%s' %
+ security_group_default_rule_id)
+ body = json.loads(body)
+ self.validate_response(schema.create_get_security_group_default_rule,
+ resp, body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/compute/security_group_rules_client.py b/tempest/lib/services/compute/security_group_rules_client.py
new file mode 100644
index 0000000..710bfab
--- /dev/null
+++ b/tempest/lib/services/compute/security_group_rules_client.py
@@ -0,0 +1,45 @@
+# 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.api_schema.response.compute.v2_1 import \
+ security_groups as schema
+from tempest.lib.common import rest_client
+from tempest.lib.services.compute import base_compute_client
+
+
+class SecurityGroupRulesClient(base_compute_client.BaseComputeClient):
+
+ def create_security_group_rule(self, **kwargs):
+ """Create a new security group rule.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#create-security-group-rule
+ """
+ post_body = json.dumps({'security_group_rule': kwargs})
+ url = 'os-security-group-rules'
+ resp, body = self.post(url, post_body)
+ body = json.loads(body)
+ self.validate_response(schema.create_security_group_rule, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_security_group_rule(self, group_rule_id):
+ """Deletes the provided Security Group rule."""
+ resp, body = self.delete('os-security-group-rules/%s' %
+ group_rule_id)
+ self.validate_response(schema.delete_security_group_rule, resp, body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/compute/security_groups_client.py b/tempest/lib/services/compute/security_groups_client.py
new file mode 100644
index 0000000..b525f68
--- /dev/null
+++ b/tempest/lib/services/compute/security_groups_client.py
@@ -0,0 +1,107 @@
+# 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.api_schema.response.compute.v2_1 import \
+ security_groups as schema
+from tempest.lib.common import rest_client
+from tempest.lib import exceptions as lib_exc
+from tempest.lib.services.compute import base_compute_client
+
+
+class SecurityGroupsClient(base_compute_client.BaseComputeClient):
+
+ def list_security_groups(self, **params):
+ """List all security groups for a user.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#list-security-groups
+ """
+
+ url = 'os-security-groups'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.validate_response(schema.list_security_groups, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_security_group(self, security_group_id):
+ """Get the details of a Security Group.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#show-security-group-details
+ """
+ url = "os-security-groups/%s" % security_group_id
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.validate_response(schema.get_security_group, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def create_security_group(self, **kwargs):
+ """Create a new security group.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#create-security-group
+ """
+ post_body = json.dumps({'security_group': kwargs})
+ resp, body = self.post('os-security-groups', post_body)
+ body = json.loads(body)
+ self.validate_response(schema.get_security_group, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_security_group(self, security_group_id, **kwargs):
+ """Update a security group.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#update-security-group
+ """
+ post_body = json.dumps({'security_group': kwargs})
+ resp, body = self.put('os-security-groups/%s' % security_group_id,
+ post_body)
+ body = json.loads(body)
+ self.validate_response(schema.update_security_group, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_security_group(self, security_group_id):
+ """Delete the provided Security Group.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#delete-security-group
+ """
+ resp, body = self.delete(
+ 'os-security-groups/%s' % security_group_id)
+ self.validate_response(schema.delete_security_group, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def is_resource_deleted(self, id):
+ try:
+ self.show_security_group(id)
+ except lib_exc.NotFound:
+ return True
+ return False
+
+ @property
+ def resource_type(self):
+ """Return the primary type of resource this client works with."""
+ return 'security_group'
diff --git a/tempest/lib/services/compute/server_groups_client.py b/tempest/lib/services/compute/server_groups_client.py
new file mode 100644
index 0000000..03cd645
--- /dev/null
+++ b/tempest/lib/services/compute/server_groups_client.py
@@ -0,0 +1,67 @@
+# Copyright 2012 OpenStack Foundation
+# Copyright 2013 Hewlett-Packard Development Company, L.P.
+# 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.api_schema.response.compute.v2_1 import servers as schema
+from tempest.lib.api_schema.response.compute.v2_13 import servers as schemav213
+from tempest.lib.common import rest_client
+from tempest.lib.services.compute import base_compute_client
+
+
+class ServerGroupsClient(base_compute_client.BaseComputeClient):
+
+ schema_versions_info = [
+ {'min': None, 'max': '2.12', 'schema': schema},
+ {'min': '2.13', 'max': None, 'schema': schemav213}]
+
+ def create_server_group(self, **kwargs):
+ """Create the server group.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#create-server-group
+ """
+ post_body = json.dumps({'server_group': kwargs})
+ resp, body = self.post('os-server-groups', post_body)
+
+ body = json.loads(body)
+ schema = self.get_schema(self.schema_versions_info)
+ self.validate_response(schema.create_show_server_group, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_server_group(self, server_group_id):
+ """Delete the given server-group."""
+ resp, body = self.delete("os-server-groups/%s" % server_group_id)
+ schema = self.get_schema(self.schema_versions_info)
+ self.validate_response(schema.delete_server_group, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_server_groups(self):
+ """List the server-groups."""
+ resp, body = self.get("os-server-groups")
+ body = json.loads(body)
+ schema = self.get_schema(self.schema_versions_info)
+ self.validate_response(schema.list_server_groups, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_server_group(self, server_group_id):
+ """Get the details of given server_group."""
+ resp, body = self.get("os-server-groups/%s" % server_group_id)
+ body = json.loads(body)
+ schema = self.get_schema(self.schema_versions_info)
+ self.validate_response(schema.create_show_server_group, resp, body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/compute/servers_client.py b/tempest/lib/services/compute/servers_client.py
new file mode 100644
index 0000000..0314356
--- /dev/null
+++ b/tempest/lib/services/compute/servers_client.py
@@ -0,0 +1,868 @@
+# Copyright 2012 OpenStack Foundation
+# Copyright 2013 Hewlett-Packard Development Company, L.P.
+# Copyright 2017 AT&T 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.
+
+import copy
+
+from oslo_serialization import jsonutils as json
+from six.moves.urllib import parse as urllib
+
+from tempest.lib.api_schema.response.compute.v2_1 import \
+ security_groups as security_groups_schema
+from tempest.lib.api_schema.response.compute.v2_1 import servers as schema
+from tempest.lib.api_schema.response.compute.v2_16 import servers as schemav216
+from tempest.lib.api_schema.response.compute.v2_19 import servers as schemav219
+from tempest.lib.api_schema.response.compute.v2_26 import servers as schemav226
+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_57 import servers as schemav257
+from tempest.lib.api_schema.response.compute.v2_6 import servers as schemav26
+from tempest.lib.api_schema.response.compute.v2_63 import servers as schemav263
+from tempest.lib.api_schema.response.compute.v2_9 import servers as schemav29
+from tempest.lib.common import rest_client
+from tempest.lib.services.compute import base_compute_client
+
+
+class ServersClient(base_compute_client.BaseComputeClient):
+ """Service client for the resource /servers"""
+
+ schema_versions_info = [
+ {'min': None, 'max': '2.2', 'schema': schema},
+ {'min': '2.3', 'max': '2.5', 'schema': schemav23},
+ {'min': '2.6', 'max': '2.8', 'schema': schemav26},
+ {'min': '2.9', 'max': '2.15', 'schema': schemav29},
+ {'min': '2.16', 'max': '2.18', 'schema': schemav216},
+ {'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': '2.53', 'schema': schemav248},
+ {'min': '2.54', 'max': '2.56', 'schema': schemav254},
+ {'min': '2.57', 'max': '2.62', 'schema': schemav257},
+ {'min': '2.63', 'max': None, 'schema': schemav263}]
+
+ def __init__(self, auth_provider, service, region,
+ enable_instance_password=True, **kwargs):
+ super(ServersClient, self).__init__(
+ auth_provider, service, region, **kwargs)
+ self.enable_instance_password = enable_instance_password
+
+ def create_server(self, **kwargs):
+ """Create server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/compute/#create-server
+
+ :param name: Server name
+ :param imageRef: Image reference (UUID)
+ :param flavorRef: Flavor reference (UUID or full URL)
+
+ Most parameters except the following are passed to the API without
+ any changes.
+ :param disk_config: The name is changed to OS-DCF:diskConfig
+ :param scheduler_hints: The name is changed to os:scheduler_hints and
+ the parameter is set in the same level as the parameter 'server'.
+ """
+ body = copy.deepcopy(kwargs)
+ if body.get('disk_config'):
+ body['OS-DCF:diskConfig'] = body.pop('disk_config')
+
+ hints = None
+ if body.get('scheduler_hints'):
+ hints = {'os:scheduler_hints': body.pop('scheduler_hints')}
+
+ post_body = {'server': body}
+
+ if hints:
+ post_body.update(hints)
+
+ post_body = json.dumps(post_body)
+ resp, body = self.post('servers', post_body)
+
+ body = json.loads(body)
+ # NOTE(maurosr): this deals with the case of multiple server create
+ # with return reservation id set True
+ if 'reservation_id' in body:
+ return rest_client.ResponseBody(resp, body)
+ if self.enable_instance_password:
+ create_schema = schema.create_server_with_admin_pass
+ else:
+ create_schema = schema.create_server
+ self.validate_response(create_schema, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_server(self, server_id, **kwargs):
+ """Update server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#update-server
+
+ Most parameters except the following are passed to the API without
+ any changes.
+ :param disk_config: The name is changed to OS-DCF:diskConfig
+ """
+ if 'disk_config' in kwargs:
+ kwargs['OS-DCF:diskConfig'] = kwargs.pop('disk_config')
+
+ post_body = json.dumps({'server': kwargs})
+ resp, body = self.put("servers/%s" % server_id, post_body)
+ body = json.loads(body)
+ schema = self.get_schema(self.schema_versions_info)
+ self.validate_response(schema.update_server, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_server(self, server_id):
+ """Get server details.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#show-server-details
+ """
+ resp, body = self.get("servers/%s" % server_id)
+ body = json.loads(body)
+ schema = self.get_schema(self.schema_versions_info)
+ self.validate_response(schema.get_server, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_server(self, server_id):
+ """Delete server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#delete-server
+ """
+ resp, body = self.delete("servers/%s" % server_id)
+ self.validate_response(schema.delete_server, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_servers(self, detail=False, **params):
+ """List servers.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#list-servers
+ https://developer.openstack.org/api-ref/compute/#list-servers-detailed
+ """
+
+ url = 'servers'
+ schema = self.get_schema(self.schema_versions_info)
+ if detail:
+ url += '/detail'
+ _schema = schema.list_servers_detail
+ else:
+ _schema = schema.list_servers
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.validate_response(_schema, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_addresses(self, server_id):
+ """Lists all addresses for a server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#list-ips
+ """
+ resp, body = self.get("servers/%s/ips" % server_id)
+ body = json.loads(body)
+ self.validate_response(schema.list_addresses, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_addresses_by_network(self, server_id, network_id):
+ """Lists all addresses of a specific network type for a server."""
+ resp, body = self.get("servers/%s/ips/%s" %
+ (server_id, network_id))
+ body = json.loads(body)
+ self.validate_response(schema.list_addresses_by_network, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def action(self, server_id, action_name,
+ schema=schema.server_actions_common_schema,
+ **kwargs):
+ post_body = json.dumps({action_name: kwargs})
+ resp, body = self.post('servers/%s/action' % server_id,
+ post_body)
+ if body:
+ body = json.loads(body)
+ self.validate_response(schema, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def create_backup(self, server_id, **kwargs):
+ """Backup a server instance.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#create-server-back-up-createbackup-action
+ """
+ return self.action(server_id, "createBackup", **kwargs)
+
+ def change_password(self, server_id, **kwargs):
+ """Change the root password for the server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#change-administrative-password-changepassword-action
+ """
+ return self.action(server_id, 'changePassword', **kwargs)
+
+ def show_password(self, server_id):
+ resp, body = self.get("servers/%s/os-server-password" %
+ server_id)
+ body = json.loads(body)
+ self.validate_response(schema.show_password, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_password(self, server_id):
+ """Removes the encrypted server password from the metadata server
+
+ Note that this does not actually change the instance server
+ password.
+ """
+ resp, body = self.delete("servers/%s/os-server-password" %
+ server_id)
+ self.validate_response(schema.server_actions_delete_password,
+ resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def reboot_server(self, server_id, **kwargs):
+ """Reboot a server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#reboot-server-reboot-action
+ """
+ return self.action(server_id, 'reboot', **kwargs)
+
+ def rebuild_server(self, server_id, image_ref, **kwargs):
+ """Rebuild a server with a new image.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#rebuild-server-rebuild-action
+
+ Most parameters except the following are passed to the API without
+ any changes.
+ :param disk_config: The name is changed to OS-DCF:diskConfig
+ """
+ kwargs['imageRef'] = image_ref
+ if 'disk_config' in kwargs:
+ kwargs['OS-DCF:diskConfig'] = kwargs.pop('disk_config')
+ schema = self.get_schema(self.schema_versions_info)
+ if self.enable_instance_password:
+ rebuild_schema = schema.rebuild_server_with_admin_pass
+ else:
+ rebuild_schema = schema.rebuild_server
+ return self.action(server_id, 'rebuild',
+ rebuild_schema, **kwargs)
+
+ def resize_server(self, server_id, flavor_ref, **kwargs):
+ """Change the flavor of a server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#resize-server-resize-action
+
+ Most parameters except the following are passed to the API without
+ any changes.
+ :param disk_config: The name is changed to OS-DCF:diskConfig
+ """
+ kwargs['flavorRef'] = flavor_ref
+ if 'disk_config' in kwargs:
+ kwargs['OS-DCF:diskConfig'] = kwargs.pop('disk_config')
+ return self.action(server_id, 'resize', **kwargs)
+
+ def confirm_resize_server(self, server_id, **kwargs):
+ """Confirm the flavor change for a server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#confirm-resized-server-confirmresize-action
+ """
+ return self.action(server_id, 'confirmResize',
+ schema.server_actions_confirm_resize,
+ **kwargs)
+
+ def revert_resize_server(self, server_id, **kwargs):
+ """Revert a server back to its original flavor.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#revert-resized-server-revertresize-action
+ """
+ return self.action(server_id, 'revertResize', **kwargs)
+
+ def list_server_metadata(self, server_id):
+ """Lists all metadata for a server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#list-all-metadata
+ """
+ resp, body = self.get("servers/%s/metadata" % server_id)
+ body = json.loads(body)
+ self.validate_response(schema.list_server_metadata, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def set_server_metadata(self, server_id, meta, no_metadata_field=False):
+ """Sets one or more metadata items for a server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#replace-metadata-items
+ """
+ if no_metadata_field:
+ post_body = ""
+ else:
+ post_body = json.dumps({'metadata': meta})
+ resp, body = self.put('servers/%s/metadata' % server_id,
+ post_body)
+ body = json.loads(body)
+ self.validate_response(schema.set_server_metadata, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_server_metadata(self, server_id, meta):
+ """Updates one or more metadata items for a server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#create-or-update-metadata-items
+ """
+ post_body = json.dumps({'metadata': meta})
+ resp, body = self.post('servers/%s/metadata' % server_id,
+ post_body)
+ body = json.loads(body)
+ self.validate_response(schema.update_server_metadata,
+ resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_server_metadata_item(self, server_id, key):
+ """Shows details for a metadata item, by key, for a server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#show-metadata-item-details
+ """
+ resp, body = self.get("servers/%s/metadata/%s" % (server_id, key))
+ body = json.loads(body)
+ self.validate_response(schema.set_show_server_metadata_item,
+ resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def set_server_metadata_item(self, server_id, key, meta):
+ """Sets a metadata item, by key, for a server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#create-or-update-metadata-item
+ """
+ post_body = json.dumps({'meta': meta})
+ resp, body = self.put('servers/%s/metadata/%s' % (server_id, key),
+ post_body)
+ body = json.loads(body)
+ self.validate_response(schema.set_show_server_metadata_item,
+ resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_server_metadata_item(self, server_id, key):
+ """Deletes a metadata item, by key, from a server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#delete-metadata-item
+ """
+ resp, body = self.delete("servers/%s/metadata/%s" %
+ (server_id, key))
+ self.validate_response(schema.delete_server_metadata_item,
+ resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def stop_server(self, server_id, **kwargs):
+ """Stops a running server and changes its status to SHUTOFF.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#stop-server-os-stop-action
+ """
+ return self.action(server_id, 'os-stop', **kwargs)
+
+ def start_server(self, server_id, **kwargs):
+ """Starts a stopped server and changes its status to ACTIVE.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#start-server-os-start-action
+ """
+ return self.action(server_id, 'os-start', **kwargs)
+
+ def attach_volume(self, server_id, **kwargs):
+ """Attaches a volume to a server instance.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#attach-a-volume-to-an-instance
+ """
+ post_body = json.dumps({'volumeAttachment': kwargs})
+ resp, body = self.post('servers/%s/os-volume_attachments' % server_id,
+ post_body)
+ body = json.loads(body)
+ self.validate_response(schema.attach_volume, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_attached_volume(self, server_id, attachment_id, **kwargs):
+ """Swaps a volume attached to an instance for another volume"""
+ post_body = json.dumps({'volumeAttachment': kwargs})
+ resp, body = self.put('servers/%s/os-volume_attachments/%s' %
+ (server_id, attachment_id),
+ post_body)
+ self.validate_response(schema.update_attached_volume, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def detach_volume(self, server_id, volume_id): # noqa
+ """Detaches a volume from a server instance.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#detach-a-volume-from-an-instance
+ """
+ resp, body = self.delete('servers/%s/os-volume_attachments/%s' %
+ (server_id, volume_id))
+ self.validate_response(schema.detach_volume, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_volume_attachment(self, server_id, volume_id):
+ """Return details about the given volume attachment.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#show-a-detail-of-a-volume-attachment
+ """
+ resp, body = self.get('servers/%s/os-volume_attachments/%s' % (
+ server_id, volume_id))
+ body = json.loads(body)
+ self.validate_response(schema.show_volume_attachment, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_volume_attachments(self, server_id):
+ """Returns the list of volume attachments for a given instance.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#list-volume-attachments-for-an-instance
+ """
+ resp, body = self.get('servers/%s/os-volume_attachments' % (
+ server_id))
+ body = json.loads(body)
+ self.validate_response(schema.list_volume_attachments, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def add_security_group(self, server_id, **kwargs):
+ """Add a security group to the server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#add-security-group-to-a-server-addsecuritygroup-action
+ """
+ return self.action(server_id, 'addSecurityGroup', **kwargs)
+
+ def remove_security_group(self, server_id, **kwargs):
+ """Remove a security group from the server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#remove-security-group-from-a-server-removesecuritygroup-action
+ """
+ return self.action(server_id, 'removeSecurityGroup', **kwargs)
+
+ def live_migrate_server(self, server_id, **kwargs):
+ """This should be called with administrator privileges.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#live-migrate-server-os-migratelive-action
+ """
+ return self.action(server_id, 'os-migrateLive', **kwargs)
+
+ def migrate_server(self, server_id, **kwargs):
+ """Migrate a server to a new host.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#migrate-server-migrate-action
+ """
+ return self.action(server_id, 'migrate', **kwargs)
+
+ def lock_server(self, server_id, **kwargs):
+ """Lock the given server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#lock-server-lock-action
+ """
+ return self.action(server_id, 'lock', **kwargs)
+
+ def unlock_server(self, server_id, **kwargs):
+ """UNlock the given server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#unlock-server-unlock-action
+ """
+ return self.action(server_id, 'unlock', **kwargs)
+
+ def suspend_server(self, server_id, **kwargs):
+ """Suspend the provided server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#suspend-server-suspend-action
+ """
+ return self.action(server_id, 'suspend', **kwargs)
+
+ def resume_server(self, server_id, **kwargs):
+ """Un-suspend the provided server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#resume-suspended-server-resume-action
+ """
+ return self.action(server_id, 'resume', **kwargs)
+
+ def pause_server(self, server_id, **kwargs):
+ """Pause the provided server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#pause-server-pause-action
+ """
+ return self.action(server_id, 'pause', **kwargs)
+
+ def unpause_server(self, server_id, **kwargs):
+ """Un-pause the provided server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#unpause-server-unpause-action
+ """
+ return self.action(server_id, 'unpause', **kwargs)
+
+ def reset_state(self, server_id, **kwargs):
+ """Reset the state of a server to active/error.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#reset-server-state-os-resetstate-action
+ """
+ return self.action(server_id, 'os-resetState', **kwargs)
+
+ def shelve_server(self, server_id, **kwargs):
+ """Shelve the provided server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#shelve-server-shelve-action
+ """
+ return self.action(server_id, 'shelve', **kwargs)
+
+ def unshelve_server(self, server_id, **kwargs):
+ """Un-shelve the provided server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#unshelve-restore-shelved-server-unshelve-action
+ """
+ return self.action(server_id, 'unshelve', **kwargs)
+
+ def shelve_offload_server(self, server_id, **kwargs):
+ """Shelve-offload the provided server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#shelf-offload-remove-server-shelveoffload-action
+ """
+ return self.action(server_id, 'shelveOffload', **kwargs)
+
+ def get_console_output(self, server_id, **kwargs):
+ """Get console output.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#show-console-output-os-getconsoleoutput-action
+ """
+ return self.action(server_id, 'os-getConsoleOutput',
+ schema.get_console_output, **kwargs)
+
+ def get_remote_console(self, server_id, console_type, protocol, **kwargs):
+ """Get a remote console.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#create-remote-console
+ """
+ param = {
+ 'remote_console': {
+ 'type': console_type,
+ 'protocol': protocol,
+ }
+ }
+ post_body = json.dumps(param)
+ resp, body = self.post("servers/%s/remote-consoles" % server_id,
+ post_body)
+ body = json.loads(body)
+ schema = self.get_schema(self.schema_versions_info)
+ self.validate_response(schema.get_remote_consoles, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_virtual_interfaces(self, server_id):
+ """List the virtual interfaces used in an instance."""
+ resp, body = self.get('/'.join(['servers', server_id,
+ 'os-virtual-interfaces']))
+ body = json.loads(body)
+ self.validate_response(schema.list_virtual_interfaces, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def rescue_server(self, server_id, **kwargs):
+ """Rescue the provided server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#rescue-server-rescue-action
+ """
+ if self.enable_instance_password:
+ rescue_schema = schema.rescue_server_with_admin_pass
+ else:
+ rescue_schema = schema.rescue_server
+ return self.action(server_id, 'rescue', rescue_schema, **kwargs)
+
+ def unrescue_server(self, server_id):
+ """Unrescue the provided server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#unrescue-server-unrescue-action
+ """
+ return self.action(server_id, 'unrescue')
+
+ def show_server_diagnostics(self, server_id):
+ """Get the usage data for a server."""
+ resp, body = self.get("servers/%s/diagnostics" % server_id)
+ body = json.loads(body)
+ schema = self.get_schema(self.schema_versions_info)
+ self.validate_response(schema.show_server_diagnostics, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_instance_actions(self, server_id):
+ """List the provided server action."""
+ resp, body = self.get("servers/%s/os-instance-actions" %
+ server_id)
+ body = json.loads(body)
+ self.validate_response(schema.list_instance_actions, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_instance_action(self, server_id, request_id):
+ """Returns the action details of the provided server."""
+ resp, body = self.get("servers/%s/os-instance-actions/%s" %
+ (server_id, request_id))
+ body = json.loads(body)
+ self.validate_response(schema.show_instance_action, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def force_delete_server(self, server_id, **kwargs):
+ """Force delete a server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#force-delete-server-forcedelete-action
+ """
+ return self.action(server_id, 'forceDelete', **kwargs)
+
+ def restore_soft_deleted_server(self, server_id, **kwargs):
+ """Restore a soft-deleted server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#restore-soft-deleted-instance-restore-action
+ """
+ return self.action(server_id, 'restore', **kwargs)
+
+ def reset_network(self, server_id, **kwargs):
+ """Reset the Network of a server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#reset-networking-on-a-server-resetnetwork-action
+ """
+ return self.action(server_id, 'resetNetwork', **kwargs)
+
+ def inject_network_info(self, server_id, **kwargs):
+ """Inject the Network Info into server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#inject-network-information-injectnetworkinfo-action
+ """
+ return self.action(server_id, 'injectNetworkInfo', **kwargs)
+
+ def get_vnc_console(self, server_id, **kwargs):
+ """Get URL of VNC console.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#get-vnc-console-os-getvncconsole-action-deprecated
+ """
+ return self.action(server_id, "os-getVNCConsole",
+ schema.get_vnc_console, **kwargs)
+
+ def add_fixed_ip(self, server_id, **kwargs):
+ """Add a fixed IP to server instance.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#add-associate-fixed-ip-addfixedip-action-deprecated
+ """
+ return self.action(server_id, 'addFixedIp', **kwargs)
+
+ def remove_fixed_ip(self, server_id, **kwargs):
+ """Remove input fixed IP from input server instance.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#remove-disassociate-fixed-ip-removefixedip-action-deprecated
+ """
+ return self.action(server_id, 'removeFixedIp', **kwargs)
+
+ def list_security_groups_by_server(self, server_id):
+ """Lists security groups for a server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#list-security-groups-by-server
+ """
+ resp, body = self.get("servers/%s/os-security-groups" % server_id)
+ body = json.loads(body)
+ self.validate_response(security_groups_schema.list_security_groups,
+ resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_tags(self, server_id):
+ """Lists all tags for a server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#list-tags
+ """
+ url = 'servers/%s/tags' % server_id
+ resp, body = self.get(url)
+ body = json.loads(body)
+ schema = self.get_schema(self.schema_versions_info)
+ self.validate_response(schema.list_tags, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_all_tags(self, server_id, tags):
+ """Replaces all tags on specified server with the new set of tags.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#replace-tags
+
+ :param tags: List of tags to replace current server tags with.
+ """
+ url = 'servers/%s/tags' % server_id
+ put_body = {'tags': tags}
+ resp, body = self.put(url, json.dumps(put_body))
+ body = json.loads(body)
+ schema = self.get_schema(self.schema_versions_info)
+ self.validate_response(schema.update_all_tags, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_all_tags(self, server_id):
+ """Deletes all tags from the specified server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#delete-all-tags
+ """
+ url = 'servers/%s/tags' % server_id
+ resp, body = self.delete(url)
+ schema = self.get_schema(self.schema_versions_info)
+ self.validate_response(schema.delete_all_tags, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def check_tag_existence(self, server_id, tag):
+ """Checks tag existence on the server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#check-tag-existence
+
+ :param tag: Check for existence of tag on specified server.
+ """
+ url = 'servers/%s/tags/%s' % (server_id, tag)
+ resp, body = self.get(url)
+ schema = self.get_schema(self.schema_versions_info)
+ self.validate_response(schema.check_tag_existence, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_tag(self, server_id, tag):
+ """Adds a single tag to the server if server has no specified tag.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#add-a-single-tag
+
+ :param tag: Tag to be added to the specified server.
+ """
+ url = 'servers/%s/tags/%s' % (server_id, tag)
+ resp, body = self.put(url, None)
+ schema = self.get_schema(self.schema_versions_info)
+ self.validate_response(schema.update_tag, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_tag(self, server_id, tag):
+ """Deletes a single tag from the specified server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#delete-a-single-tag
+
+ :param tag: Tag to be removed from the specified server.
+ """
+ url = 'servers/%s/tags/%s' % (server_id, tag)
+ resp, body = self.delete(url)
+ schema = self.get_schema(self.schema_versions_info)
+ self.validate_response(schema.delete_tag, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def evacuate_server(self, server_id, **kwargs):
+ """Evacuate the given server.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#evacuate-server-evacuate-action
+ """
+ if self.enable_instance_password:
+ evacuate_schema = schema.evacuate_server_with_admin_pass
+ else:
+ evacuate_schema = schema.evacuate_server
+
+ return self.action(server_id, 'evacuate',
+ evacuate_schema,
+ **kwargs)
diff --git a/tempest/lib/services/compute/services_client.py b/tempest/lib/services/compute/services_client.py
new file mode 100644
index 0000000..b046c35
--- /dev/null
+++ b/tempest/lib/services/compute/services_client.py
@@ -0,0 +1,102 @@
+# Copyright 2013 NEC Corporation
+# Copyright 2013 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 six.moves.urllib import parse as urllib
+
+from tempest.lib.api_schema.response.compute.v2_1 import services as schema
+from tempest.lib.api_schema.response.compute.v2_11 import services \
+ as schemav211
+from tempest.lib.common import rest_client
+from tempest.lib.services.compute import base_compute_client
+
+
+class ServicesClient(base_compute_client.BaseComputeClient):
+
+ schema_versions_info = [
+ {'min': None, 'max': '2.10', 'schema': schema},
+ {'min': '2.11', 'max': None, 'schema': schemav211}]
+
+ def list_services(self, **params):
+ """Lists all running Compute services for a tenant.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#list-compute-services
+ """
+ url = 'os-services'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ _schema = self.get_schema(self.schema_versions_info)
+ self.validate_response(_schema.list_services, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def enable_service(self, **kwargs):
+ """Enable service on a host.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#enable-scheduling-for-a-compute-service
+ """
+ post_body = json.dumps(kwargs)
+ resp, body = self.put('os-services/enable', post_body)
+ body = json.loads(body)
+ self.validate_response(schema.enable_disable_service, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def disable_service(self, **kwargs):
+ """Disable service on a host.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#disable-scheduling-for-a-compute-service
+ """
+ post_body = json.dumps(kwargs)
+ resp, body = self.put('os-services/disable', post_body)
+ body = json.loads(body)
+ self.validate_response(schema.enable_disable_service, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def disable_log_reason(self, **kwargs):
+ """Disables scheduling for a Compute service and logs reason.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#disable-scheduling-for-a-compute-service-and-log-disabled-reason
+ """
+ post_body = json.dumps(kwargs)
+ resp, body = self.put('os-services/disable-log-reason', post_body)
+ body = json.loads(body)
+ self.validate_response(schema.disable_log_reason, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_forced_down(self, **kwargs):
+ """Set or unset ``forced_down`` flag for the service.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#update-forced-down
+ """
+ post_body = json.dumps(kwargs)
+ resp, body = self.put('os-services/force-down', post_body)
+ body = json.loads(body)
+ # NOTE: Use schemav211.update_forced_down directly because there is no
+ # update_forced_down schema for <2.11.
+ self.validate_response(schemav211.update_forced_down, resp, body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/compute/snapshots_client.py b/tempest/lib/services/compute/snapshots_client.py
new file mode 100644
index 0000000..df8d6fb
--- /dev/null
+++ b/tempest/lib/services/compute/snapshots_client.py
@@ -0,0 +1,84 @@
+# Copyright 2015 Fujitsu(fnst) 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 six.moves.urllib import parse as urllib
+
+from tempest.lib.api_schema.response.compute.v2_1 import snapshots as schema
+from tempest.lib.common import rest_client
+from tempest.lib import exceptions as lib_exc
+from tempest.lib.services.compute import base_compute_client
+
+
+class SnapshotsClient(base_compute_client.BaseComputeClient):
+
+ def create_snapshot(self, volume_id, **kwargs):
+ """Create a snapshot.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#create-snapshot
+ """
+ post_body = {
+ 'volume_id': volume_id
+ }
+ post_body.update(kwargs)
+ post_body = json.dumps({'snapshot': post_body})
+ resp, body = self.post('os-snapshots', post_body)
+ body = json.loads(body)
+ self.validate_response(schema.create_get_snapshot, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_snapshot(self, snapshot_id):
+ url = "os-snapshots/%s" % snapshot_id
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.validate_response(schema.create_get_snapshot, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_snapshots(self, detail=False, params=None):
+ """List snapshots.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#list-snapshots
+ """
+ url = 'os-snapshots'
+
+ if detail:
+ url += '/detail'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.validate_response(schema.list_snapshots, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_snapshot(self, snapshot_id):
+ resp, body = self.delete("os-snapshots/%s" % snapshot_id)
+ self.validate_response(schema.delete_snapshot, resp, body)
+ 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):
+ """Return the primary type of resource this client works with."""
+ return 'snapshot'
diff --git a/tempest/lib/services/compute/tenant_networks_client.py b/tempest/lib/services/compute/tenant_networks_client.py
new file mode 100644
index 0000000..04d8bab
--- /dev/null
+++ b/tempest/lib/services/compute/tenant_networks_client.py
@@ -0,0 +1,35 @@
+# Copyright 2015 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 oslo_serialization import jsonutils as json
+
+from tempest.lib.api_schema.response.compute.v2_1 import tenant_networks
+from tempest.lib.common import rest_client
+from tempest.lib.services.compute import base_compute_client
+
+
+class TenantNetworksClient(base_compute_client.BaseComputeClient):
+
+ def list_tenant_networks(self):
+ resp, body = self.get("os-tenant-networks")
+ body = json.loads(body)
+ self.validate_response(tenant_networks.list_tenant_networks, resp,
+ body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_tenant_network(self, network_id):
+ resp, body = self.get("os-tenant-networks/%s" % network_id)
+ body = json.loads(body)
+ self.validate_response(tenant_networks.get_tenant_network, resp, body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/compute/tenant_usages_client.py b/tempest/lib/services/compute/tenant_usages_client.py
new file mode 100644
index 0000000..ade60e5
--- /dev/null
+++ b/tempest/lib/services/compute/tenant_usages_client.py
@@ -0,0 +1,56 @@
+# Copyright 2013 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 oslo_serialization import jsonutils as json
+from six.moves.urllib import parse as urllib
+
+from tempest.lib.api_schema.response.compute.v2_1 import tenant_usages
+from tempest.lib.common import rest_client
+from tempest.lib.services.compute import base_compute_client
+
+
+class TenantUsagesClient(base_compute_client.BaseComputeClient):
+
+ def list_tenant_usages(self, **params):
+ """List Tenant Usage For All Tenants.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#list-tenant-usage-statistics-for-all-tenants
+ """
+ url = 'os-simple-tenant-usage'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.validate_response(tenant_usages.list_tenant_usage, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_tenant_usage(self, tenant_id, **params):
+ """Show Usage Details For Tenant.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#show-usage-statistics-for-tenant
+ """
+ url = 'os-simple-tenant-usage/%s' % tenant_id
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.validate_response(tenant_usages.get_tenant_usage, resp, body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/compute/versions_client.py b/tempest/lib/services/compute/versions_client.py
new file mode 100644
index 0000000..8fbb136
--- /dev/null
+++ b/tempest/lib/services/compute/versions_client.py
@@ -0,0 +1,55 @@
+# Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
+#
+# 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 time
+
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.api_schema.response.compute.v2_1 import versions as schema
+from tempest.lib.common import rest_client
+from tempest.lib.services.compute import base_compute_client
+
+
+class VersionsClient(base_compute_client.BaseComputeClient):
+
+ def list_versions(self):
+ version_url = self._get_base_version_url()
+
+ start = time.time()
+ resp, body = self.raw_request(version_url, 'GET')
+ end = time.time()
+ self._log_request('GET', version_url, resp, secs=(end - start),
+ resp_body=body)
+
+ self._error_checker(resp, body)
+ body = json.loads(body)
+ self.validate_response(schema.list_versions, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def get_version_by_url(self, version_url):
+ """Get the version document by url.
+
+ This gets the version document for a url, useful in testing
+ the contents of things like /v2/ or /v2.1/ in Nova. That
+ controller needs authenticated access, so we have to get
+ ourselves a token before making the request.
+
+ """
+ # we need a token for this request
+ resp, body = self.raw_request(version_url, 'GET',
+ {'X-Auth-Token': self.token})
+ self._error_checker(resp, body)
+ body = json.loads(body)
+ self.validate_response(schema.get_one_version, resp, body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/compute/volumes_client.py b/tempest/lib/services/compute/volumes_client.py
new file mode 100644
index 0000000..95cdd53
--- /dev/null
+++ b/tempest/lib/services/compute/volumes_client.py
@@ -0,0 +1,94 @@
+# 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.api_schema.response.compute.v2_1 import volumes as schema
+from tempest.lib.common import rest_client
+from tempest.lib import exceptions as lib_exc
+from tempest.lib.services.compute import base_compute_client
+
+
+class VolumesClient(base_compute_client.BaseComputeClient):
+
+ def list_volumes(self, detail=False, **params):
+ """List all the volumes created.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#list-volumes
+ https://developer.openstack.org/api-ref/compute/#list-volumes-with-details
+ """
+ url = 'os-volumes'
+
+ if detail:
+ url += '/detail'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.validate_response(schema.list_volumes, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_volume(self, volume_id):
+ """Return the details of a single volume.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#show-volume-details
+ """
+ url = "os-volumes/%s" % volume_id
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.validate_response(schema.create_get_volume, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def create_volume(self, **kwargs):
+ """Create a new Volume.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#create-volume
+ """
+ post_body = json.dumps({'volume': kwargs})
+ resp, body = self.post('os-volumes', post_body)
+ body = json.loads(body)
+ self.validate_response(schema.create_get_volume, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_volume(self, volume_id):
+ """Delete the Specified Volume.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#delete-volume
+ """
+ resp, body = self.delete("os-volumes/%s" % volume_id)
+ self.validate_response(schema.delete_volume, resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def is_resource_deleted(self, id):
+ try:
+ self.show_volume(id)
+ except lib_exc.NotFound:
+ return True
+ return False
+
+ @property
+ def resource_type(self):
+ """Return the primary type of resource this client works with."""
+ return 'volume'
diff --git a/tempest/lib/services/identity/__init__.py b/tempest/lib/services/identity/__init__.py
new file mode 100644
index 0000000..941a10e
--- /dev/null
+++ b/tempest/lib/services/identity/__init__.py
@@ -0,0 +1,18 @@
+# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P.
+#
+# 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 import v2
+from tempest.lib.services.identity import v3
+
+__all__ = ['v2', 'v3']
diff --git a/tempest/lib/services/identity/v2/__init__.py b/tempest/lib/services/identity/v2/__init__.py
new file mode 100644
index 0000000..b7d3c74
--- /dev/null
+++ b/tempest/lib/services/identity/v2/__init__.py
@@ -0,0 +1,24 @@
+# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P.
+#
+# 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.v2.endpoints_client import EndpointsClient
+from tempest.lib.services.identity.v2.identity_client import IdentityClient
+from tempest.lib.services.identity.v2.roles_client import RolesClient
+from tempest.lib.services.identity.v2.services_client import ServicesClient
+from tempest.lib.services.identity.v2.tenants_client import TenantsClient
+from tempest.lib.services.identity.v2.token_client import TokenClient
+from tempest.lib.services.identity.v2.users_client import UsersClient
+
+__all__ = ['EndpointsClient', 'IdentityClient', 'RolesClient',
+ 'ServicesClient', 'TenantsClient', 'TokenClient', 'UsersClient']
diff --git a/tempest/lib/services/identity/v2/endpoints_client.py b/tempest/lib/services/identity/v2/endpoints_client.py
new file mode 100644
index 0000000..db8d7cc
--- /dev/null
+++ b/tempest/lib/services/identity/v2/endpoints_client.py
@@ -0,0 +1,49 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# 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 EndpointsClient(rest_client.RestClient):
+ api_version = "v2.0"
+
+ def create_endpoint(self, **kwargs):
+ """Create an endpoint for service.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/identity/v2-admin/index.html#create-endpoint-template
+ """
+
+ post_body = json.dumps({'endpoint': kwargs})
+ resp, body = self.post('/endpoints', post_body)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_endpoints(self):
+ """List Endpoints - Returns Endpoints."""
+ resp, body = self.get('/endpoints')
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_endpoint(self, endpoint_id):
+ """Delete an endpoint."""
+ url = '/endpoints/%s' % endpoint_id
+ resp, body = self.delete(url)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/identity/v2/identity_client.py b/tempest/lib/services/identity/v2/identity_client.py
new file mode 100644
index 0000000..c610d65
--- /dev/null
+++ b/tempest/lib/services/identity/v2/identity_client.py
@@ -0,0 +1,69 @@
+# 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 IdentityClient(rest_client.RestClient):
+ api_version = "v2.0"
+
+ def show_api_description(self):
+ """Retrieves info about the v2.0 Identity API"""
+ url = ''
+ resp, body = self.get(url)
+ self.expected_success([200, 203], resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_token(self, token_id):
+ """Get token details."""
+ resp, body = self.get("tokens/%s" % token_id)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_token(self, token_id):
+ """Delete a token."""
+ resp, body = self.delete("tokens/%s" % token_id)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_extensions(self):
+ """List all the extensions."""
+ resp, body = self.get('/extensions')
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_endpoints_for_token(self, token_id):
+ """List endpoints for a token """
+ resp, body = self.get("tokens/%s/endpoints" % token_id)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def check_token_existence(self, token_id, **params):
+ """Validates a token and confirms that it belongs to a tenant.
+
+ For a full list of available parameters, please refer to the
+ official API reference:
+ https://developer.openstack.org/api-ref/identity/v2-admin/#validate-token
+ """
+ url = "tokens/%s" % token_id
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+ resp, body = self.head(url)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/identity/v2/roles_client.py b/tempest/lib/services/identity/v2/roles_client.py
new file mode 100644
index 0000000..9e841dd
--- /dev/null
+++ b/tempest/lib/services/identity/v2/roles_client.py
@@ -0,0 +1,109 @@
+# 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 RolesClient(rest_client.RestClient):
+ api_version = "v2.0"
+
+ def create_role(self, **kwargs):
+ """Create a role.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/identity/v2-ext/index.html#create-a-role
+ """
+ post_body = json.dumps({'role': kwargs})
+ resp, body = self.post('OS-KSADM/roles', post_body)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_role(self, role_id_or_name):
+ """Get a role by its id or name.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/identity/v2-ext/index.html#show-a-role
+ OR
+ https://developer.openstack.org/api-ref/identity/v2-ext/index.html#show-role-information-by-name
+ """
+ resp, body = self.get('OS-KSADM/roles/%s' % role_id_or_name)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_roles(self, **params):
+ """Returns roles.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/identity/v2-ext/index.html#list-all-roles
+ """
+ url = 'OS-KSADM/roles'
+ 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_role(self, role_id):
+ """Delete a role.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/identity/v2-ext/index.html#delete-a-role
+ """
+ resp, body = self.delete('OS-KSADM/roles/%s' % role_id)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def create_user_role_on_project(self, tenant_id, user_id, role_id):
+ """Add roles to a user on a tenant.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/identity/v2-ext/index.html#grant-roles-to-user-on-tenant
+ """
+ resp, body = self.put('/tenants/%s/users/%s/roles/OS-KSADM/%s' %
+ (tenant_id, user_id, role_id), "")
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_user_roles_on_project(self, tenant_id, user_id, **params):
+ """Returns a list of roles assigned to a user for a tenant."""
+ # TODO(gmann): Need to write API-ref link, Bug# 1592711
+ url = '/tenants/%s/users/%s/roles' % (tenant_id, 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_role_from_user_on_project(self, tenant_id, user_id, role_id):
+ """Removes a role assignment for a user on a tenant.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/identity/v2-ext/index.html#revoke-role-from-user-on-tenant
+ """
+ resp, body = self.delete('/tenants/%s/users/%s/roles/OS-KSADM/%s' %
+ (tenant_id, user_id, role_id))
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/identity/v2/services_client.py b/tempest/lib/services/identity/v2/services_client.py
new file mode 100644
index 0000000..47398db
--- /dev/null
+++ b/tempest/lib/services/identity/v2/services_client.py
@@ -0,0 +1,65 @@
+# Copyright 2015 Red Hat, Inc.
+#
+# 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):
+ api_version = "v2.0"
+
+ def create_service(self, **kwargs):
+ """Create a service.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v2-ext/#create-service-admin-extension
+ """
+ post_body = json.dumps({'OS-KSADM:service': kwargs})
+ resp, body = self.post('/OS-KSADM/services', post_body)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_service(self, service_id):
+ """Get Service."""
+ url = '/OS-KSADM/services/%s' % service_id
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_services(self, **params):
+ """List Service - Returns Services.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v2-ext/#list-services-admin-extension
+ """
+ url = '/OS-KSADM/services'
+ 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_service(self, service_id):
+ """Delete Service."""
+ url = '/OS-KSADM/services/%s' % service_id
+ resp, body = self.delete(url)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/identity/v2/tenants_client.py b/tempest/lib/services/identity/v2/tenants_client.py
new file mode 100644
index 0000000..026db64
--- /dev/null
+++ b/tempest/lib/services/identity/v2/tenants_client.py
@@ -0,0 +1,103 @@
+# Copyright 2015 Red Hat, Inc.
+#
+# 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 TenantsClient(rest_client.RestClient):
+ api_version = "v2.0"
+
+ def create_tenant(self, **kwargs):
+ """Create a tenant
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v2-admin/index.html#create-tenant
+ """
+ post_body = json.dumps({'tenant': kwargs})
+ resp, body = self.post('tenants', post_body)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_tenant(self, tenant_id):
+ """Delete a tenant.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/identity/v2-admin/index.html#delete-tenant
+ """
+ resp, body = self.delete('tenants/%s' % str(tenant_id))
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_tenant(self, tenant_id):
+ """Get tenant details.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/identity/v2-admin/index.html#show-tenant-details-by-id
+ """
+ resp, body = self.get('tenants/%s' % str(tenant_id))
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_tenants(self, **params):
+ """Returns tenants.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v2-admin/index.html#list-tenants-admin-endpoint
+ """
+ url = 'tenants'
+ 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 update_tenant(self, tenant_id, **kwargs):
+ """Updates a tenant.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v2-admin/index.html#update-tenant
+ """
+ if 'id' not in kwargs:
+ kwargs['id'] = tenant_id
+ post_body = json.dumps({'tenant': kwargs})
+ resp, body = self.post('tenants/%s' % tenant_id, post_body)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_tenant_users(self, tenant_id, **params):
+ """List users for a Tenant.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v2-admin/index.html#list-users-on-a-tenant
+ """
+ url = '/tenants/%s/users' % tenant_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)
diff --git a/tempest/lib/services/identity/v2/token_client.py b/tempest/lib/services/identity/v2/token_client.py
new file mode 100644
index 0000000..458c862
--- /dev/null
+++ b/tempest/lib/services/identity/v2/token_client.py
@@ -0,0 +1,140 @@
+# Copyright 2015 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 oslo_log import log as logging
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+from tempest.lib import exceptions
+
+
+class TokenClient(rest_client.RestClient):
+
+ def __init__(self, auth_url, disable_ssl_certificate_validation=None,
+ ca_certs=None, trace_requests=None, **kwargs):
+ """Initialises the Token client
+
+ :param auth_url: URL to which the token request is sent
+ :param disable_ssl_certificate_validation: pass-through to rest client
+ :param ca_certs: pass-through to rest client
+ :param trace_requests: pass-through to rest client
+ :param kwargs: any extra parameter to pass through the rest client.
+ region, service and auth_provider will be ignored, if passed,
+ as they are not meaningful for token client
+ """
+ dscv = disable_ssl_certificate_validation
+ # NOTE(andreaf) region, service and auth_provider are passed
+ # positionally with None. Having them in kwargs would raise a
+ # "multiple values for keyword arguments" error
+ for unwanted_kwargs in ['region', 'service', 'auth_provider']:
+ kwargs.pop(unwanted_kwargs, None)
+ super(TokenClient, self).__init__(
+ None, None, None, disable_ssl_certificate_validation=dscv,
+ ca_certs=ca_certs, trace_requests=trace_requests, **kwargs)
+
+ if auth_url is None:
+ raise exceptions.IdentityError("Couldn't determine auth_url")
+
+ # Normalize URI to ensure /tokens is in it.
+ if 'tokens' not in auth_url:
+ auth_url = auth_url.rstrip('/') + '/tokens'
+
+ self.auth_url = auth_url
+
+ def auth(self, user, password, tenant=None):
+ creds = {
+ 'auth': {
+ 'passwordCredentials': {
+ 'username': user,
+ 'password': password,
+ },
+ }
+ }
+
+ if tenant:
+ creds['auth']['tenantName'] = tenant
+
+ body = json.dumps(creds, sort_keys=True)
+ resp, body = self.post(self.auth_url, body=body)
+ self.expected_success(200, resp.status)
+
+ return rest_client.ResponseBody(resp, body['access'])
+
+ def auth_token(self, token_id, tenant=None):
+ creds = {
+ 'auth': {
+ 'token': {
+ 'id': token_id,
+ },
+ }
+ }
+
+ if tenant:
+ creds['auth']['tenantName'] = tenant
+
+ body = json.dumps(creds)
+ resp, body = self.post(self.auth_url, body=body)
+ self.expected_success(200, resp.status)
+
+ return rest_client.ResponseBody(resp, body['access'])
+
+ def request(self, method, url, extra_headers=False, headers=None,
+ body=None, chunked=False):
+ """A simple HTTP request interface.
+
+ Note: this overloads the `request` method from the parent class and
+ thus must implement the same method signature.
+ """
+ if headers is None:
+ headers = self.get_headers(accept_type="json")
+ elif extra_headers:
+ try:
+ headers.update(self.get_headers(accept_type="json"))
+ except (ValueError, TypeError):
+ headers = self.get_headers(accept_type="json")
+
+ resp, resp_body = self.raw_request(url, method,
+ headers=headers, body=body)
+ self._log_request(method, url, resp, req_headers=headers,
+ req_body='<omitted>', resp_body=resp_body)
+
+ if resp.status in [401, 403]:
+ resp_body = json.loads(resp_body)
+ raise exceptions.Unauthorized(resp_body['error']['message'])
+ elif resp.status not in [200, 201]:
+ raise exceptions.IdentityError(
+ 'Unexpected status code {0}'.format(resp.status))
+
+ return resp, json.loads(resp_body)
+
+ def get_token(self, user, password, tenant, auth_data=False):
+ """Returns (token id, token data) for supplied credentials."""
+ body = self.auth(user, password, tenant)
+
+ if auth_data:
+ return body['token']['id'], body
+ else:
+ return body['token']['id']
+
+
+class TokenClientJSON(TokenClient):
+ LOG = logging.getLogger(__name__)
+
+ def _warn(self):
+ self.LOG.warning("%s class was deprecated and renamed to %s",
+ self.__class__.__name__, 'TokenClient')
+
+ def __init__(self, *args, **kwargs):
+ self._warn()
+ super(TokenClientJSON, self).__init__(*args, **kwargs)
diff --git a/tempest/lib/services/identity/v2/users_client.py b/tempest/lib/services/identity/v2/users_client.py
new file mode 100644
index 0000000..44bb5fd
--- /dev/null
+++ b/tempest/lib/services/identity/v2/users_client.py
@@ -0,0 +1,158 @@
+# 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 UsersClient(rest_client.RestClient):
+ api_version = "v2.0"
+
+ def create_user(self, **kwargs):
+ """Create a user.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v2-admin/index.html#create-user-admin-endpoint
+ """
+ post_body = json.dumps({'user': kwargs})
+ resp, body = self.post('users', post_body)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_user(self, user_id, **kwargs):
+ """Updates a user.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v2-admin/index.html#update-user-admin-endpoint
+ """
+ put_body = json.dumps({'user': kwargs})
+ resp, body = self.put('users/%s' % user_id, put_body)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_user(self, user_id):
+ """GET a user.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/identity/v2-admin/index.html#show-user-details-admin-endpoint
+ """
+ resp, body = self.get("users/%s" % user_id)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_user(self, user_id):
+ """Delete a user.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/identity/v2-admin/index.html#delete-user-admin-endpoint
+ """
+ resp, body = self.delete("users/%s" % user_id)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_users(self, **params):
+ """Get the list of users.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v2-admin/index.html#list-users-admin-endpoint
+ """
+ url = "users"
+ 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 update_user_enabled(self, user_id, **kwargs):
+ """Enables or disables a user.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/identity/v2-ext/index.html#enable-disable-user
+ """
+ # NOTE: The URL (users/<id>/enabled) is different from the api-site
+ # one (users/<id>/OS-KSADM/enabled) , but they are the same API
+ # because of the fact that in keystone/contrib/admin_crud/core.py
+ # both api use same action='set_user_enabled'
+ put_body = json.dumps({'user': kwargs})
+ resp, body = self.put('users/%s/enabled' % user_id, put_body)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_user_password(self, user_id, **kwargs):
+ """Update User Password."""
+ # 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/1524147
+ put_body = json.dumps({'user': kwargs})
+ resp, body = self.put('users/%s/OS-KSADM/password' % user_id, put_body)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_user_own_password(self, user_id, **kwargs):
+ """User updates own password"""
+ # 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/1524153
+ # NOTE: This API is used for updating user password by itself.
+ # Ref: http://lists.openstack.org/pipermail/openstack-dev/2015-December
+ # /081803.html
+ patch_body = json.dumps({'user': kwargs})
+ resp, body = self.patch('OS-KSCRUD/users/%s' % user_id, patch_body)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def create_user_ec2_credential(self, user_id, **kwargs):
+ # 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.
+ post_body = json.dumps(kwargs)
+ resp, body = self.post('/users/%s/credentials/OS-EC2' % user_id,
+ post_body)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_user_ec2_credential(self, user_id, access):
+ resp, body = self.delete('/users/%s/credentials/OS-EC2/%s' %
+ (user_id, access))
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_user_ec2_credentials(self, user_id):
+ resp, body = self.get('/users/%s/credentials/OS-EC2' % user_id)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_user_ec2_credential(self, user_id, access):
+ resp, body = self.get('/users/%s/credentials/OS-EC2/%s' %
+ (user_id, access))
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/identity/v3/__init__.py b/tempest/lib/services/identity/v3/__init__.py
new file mode 100644
index 0000000..da1c51c
--- /dev/null
+++ b/tempest/lib/services/identity/v3/__init__.py
@@ -0,0 +1,58 @@
+# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P.
+#
+# 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.application_credentials_client import \
+ ApplicationCredentialsClient
+from tempest.lib.services.identity.v3.catalog_client import \
+ CatalogClient
+from tempest.lib.services.identity.v3.credentials_client import \
+ CredentialsClient
+from tempest.lib.services.identity.v3.domain_configuration_client \
+ import DomainConfigurationClient
+from tempest.lib.services.identity.v3.domains_client import DomainsClient
+from tempest.lib.services.identity.v3.endpoint_filter_client import \
+ EndPointsFilterClient
+from tempest.lib.services.identity.v3.endpoint_groups_client import \
+ EndPointGroupsClient
+from tempest.lib.services.identity.v3.endpoints_client import EndPointsClient
+from tempest.lib.services.identity.v3.groups_client import GroupsClient
+from tempest.lib.services.identity.v3.identity_client import IdentityClient
+from tempest.lib.services.identity.v3.inherited_roles_client import \
+ InheritedRolesClient
+from tempest.lib.services.identity.v3.oauth_consumers_client import \
+ OAUTHConsumerClient
+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 \
+ RoleAssignmentsClient
+from tempest.lib.services.identity.v3.roles_client import RolesClient
+from tempest.lib.services.identity.v3.services_client import ServicesClient
+from tempest.lib.services.identity.v3.token_client import V3TokenClient
+from tempest.lib.services.identity.v3.trusts_client import TrustsClient
+from tempest.lib.services.identity.v3.users_client import UsersClient
+from tempest.lib.services.identity.v3.versions_client import 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/catalog_client.py b/tempest/lib/services/identity/v3/catalog_client.py
new file mode 100644
index 0000000..232b85a
--- /dev/null
+++ b/tempest/lib/services/identity/v3/catalog_client.py
@@ -0,0 +1,29 @@
+# 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#get-service-catalog
+"""
+
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+
+
+class CatalogClient(rest_client.RestClient):
+ api_version = "v3"
+
+ def show_catalog(self):
+ resp, body = self.get('auth/catalog')
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/identity/v3/credentials_client.py b/tempest/lib/services/identity/v3/credentials_client.py
new file mode 100644
index 0000000..6e5fd31
--- /dev/null
+++ b/tempest/lib/services/identity/v3/credentials_client.py
@@ -0,0 +1,91 @@
+# Copyright 2013 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.
+
+"""
+http://developer.openstack.org/api-ref/identity/v3/index.html#credentials
+"""
+
+from oslo_serialization import jsonutils as json
+from six.moves.urllib import parse as urllib
+
+from tempest.lib.common import rest_client
+
+
+class CredentialsClient(rest_client.RestClient):
+ api_version = "v3"
+
+ def create_credential(self, **kwargs):
+ """Creates a credential.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#create-credential
+ """
+ post_body = json.dumps({'credential': kwargs})
+ resp, body = self.post('credentials', post_body)
+ self.expected_success(201, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_credential(self, credential_id, **kwargs):
+ """Updates a credential.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#update-credential
+ """
+ post_body = json.dumps({'credential': kwargs})
+ resp, body = self.patch('credentials/%s' % credential_id, post_body)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_credential(self, credential_id):
+ """To GET Details of a credential.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#show-credential-details
+ """
+ resp, body = self.get('credentials/%s' % credential_id)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_credentials(self, **params):
+ """Lists out all the available credentials.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/#list-credentials
+ """
+ url = 'credentials'
+ 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_credential(self, credential_id):
+ """Deletes a credential.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/#delete-credential
+ """
+ resp, body = self.delete('credentials/%s' % credential_id)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/identity/v3/domain_configuration_client.py b/tempest/lib/services/identity/v3/domain_configuration_client.py
new file mode 100644
index 0000000..d57f2d4
--- /dev/null
+++ b/tempest/lib/services/identity/v3/domain_configuration_client.py
@@ -0,0 +1,188 @@
+# Copyright 2017 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 oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+
+
+class DomainConfigurationClient(rest_client.RestClient):
+ api_version = "v3"
+
+ def show_default_config_settings(self):
+ """Show default configuration settings.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#show-default-configuration-settings
+ """
+ url = 'domains/config/default'
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_default_group_config(self, group):
+ """Show default configuration for a group.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#show-default-configuration-for-a-group
+ """
+ url = 'domains/config/%s/default' % group
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_default_group_option(self, group, option):
+ """Show default option for a group.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#show-default-option-for-a-group
+ """
+ url = 'domains/config/%s/%s/default' % (group, option)
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_domain_group_option_config(self, domain_id, group, option):
+ """Show domain group option configuration.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#show-domain-group-option-configuration
+ """
+ url = 'domains/%s/config/%s/%s' % (domain_id, group, option)
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_domain_group_option_config(self, domain_id, group, option,
+ **kwargs):
+ """Update domain group option configuration.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#update-domain-group-option-configuration
+ """
+ url = 'domains/%s/config/%s/%s' % (domain_id, group, option)
+ resp, body = self.patch(url, json.dumps({'config': kwargs}))
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_domain_group_option_config(self, domain_id, group, option):
+ """Delete domain group option configuration.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#delete-domain-group-option-configuration
+ """
+ url = 'domains/%s/config/%s/%s' % (domain_id, group, option)
+ resp, body = self.delete(url)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_domain_group_config(self, domain_id, group):
+ """Shows details for a domain group configuration.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#show-domain-group-configuration
+ """
+ url = 'domains/%s/config/%s' % (domain_id, group)
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_domain_group_config(self, domain_id, group, **kwargs):
+ """Update domain group configuration.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#update-domain-group-configuration
+ """
+ url = 'domains/%s/config/%s' % (domain_id, group)
+ resp, body = self.patch(url, json.dumps({'config': kwargs}))
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_domain_group_config(self, domain_id, group):
+ """Delete domain group configuration.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#delete-domain-group-configuration
+ """
+ url = 'domains/%s/config/%s' % (domain_id, group)
+ resp, body = self.delete(url)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def create_domain_config(self, domain_id, **kwargs):
+ """Create domain configuration.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#create-domain-configuration
+ """
+ url = 'domains/%s/config' % domain_id
+ resp, body = self.put(url, json.dumps({'config': kwargs}))
+ self.expected_success([200, 201], resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_domain_config(self, domain_id):
+ """Show domain configuration.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#show-domain-configuration
+ """
+ url = 'domains/%s/config' % domain_id
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_domain_config(self, domain_id, **kwargs):
+ """Update domain configuration.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#update-domain-configuration
+ """
+ url = 'domains/%s/config' % domain_id
+ resp, body = self.patch(url, json.dumps({'config': kwargs}))
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_domain_config(self, domain_id):
+ """Delete domain configuration.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#delete-domain-configuration
+ """
+ url = 'domains/%s/config' % domain_id
+ resp, body = self.delete(url)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/identity/v3/domains_client.py b/tempest/lib/services/identity/v3/domains_client.py
new file mode 100644
index 0000000..43cb62c
--- /dev/null
+++ b/tempest/lib/services/identity/v3/domains_client.py
@@ -0,0 +1,84 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# 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 DomainsClient(rest_client.RestClient):
+ api_version = "v3"
+
+ def create_domain(self, **kwargs):
+ """Creates a domain.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#create-domain
+ """
+ post_body = json.dumps({'domain': kwargs})
+ resp, body = self.post('domains', post_body)
+ self.expected_success(201, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_domain(self, domain_id):
+ """Deletes a domain.
+
+ For APi details, please refer to the official API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#delete-domain
+ """
+ resp, body = self.delete('domains/%s' % domain_id)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_domains(self, **params):
+ """List Domains.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#list-domains
+ """
+ url = 'domains'
+ 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 update_domain(self, domain_id, **kwargs):
+ """Updates a domain.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#update-domain
+ """
+ post_body = json.dumps({'domain': kwargs})
+ resp, body = self.patch('domains/%s' % domain_id, post_body)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_domain(self, domain_id):
+ """Get Domain details.
+
+ For API details, please refer to the official API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#show-domain-details
+ """
+ resp, body = self.get('domains/%s' % domain_id)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/identity/v3/endpoint_filter_client.py b/tempest/lib/services/identity/v3/endpoint_filter_client.py
new file mode 100644
index 0000000..a8cd722
--- /dev/null
+++ b/tempest/lib/services/identity/v3/endpoint_filter_client.py
@@ -0,0 +1,68 @@
+# Copyright 2017 AT&T 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.
+
+"""
+https://developer.openstack.org/api-ref/identity/v3-ext/#os-ep-filter-api
+"""
+
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+
+
+class EndPointsFilterClient(rest_client.RestClient):
+ api_version = "v3"
+ ep_filter = "OS-EP-FILTER"
+
+ def list_projects_for_endpoint(self, endpoint_id):
+ """List all projects that are associated with the endpoint."""
+ resp, body = self.get(self.ep_filter + '/endpoints/%s/projects' %
+ endpoint_id)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def add_endpoint_to_project(self, project_id, endpoint_id):
+ """Add association between project and endpoint. """
+ body = None
+ resp, body = self.put(
+ self.ep_filter + '/projects/%s/endpoints/%s' %
+ (project_id, endpoint_id), body)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def check_endpoint_in_project(self, project_id, endpoint_id):
+ """Check association of Project with Endpoint."""
+ resp, body = self.head(
+ self.ep_filter + '/projects/%s/endpoints/%s' %
+ (project_id, endpoint_id), None)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_endpoints_in_project(self, project_id):
+ """List Endpoints associated with Project."""
+ resp, body = self.get(self.ep_filter + '/projects/%s/endpoints'
+ % project_id)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_endpoint_from_project(self, project_id, endpoint_id):
+ """Delete association between project and endpoint."""
+ resp, body = self.delete(
+ self.ep_filter + '/projects/%s/endpoints/%s'
+ % (project_id, endpoint_id))
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/identity/v3/endpoint_groups_client.py b/tempest/lib/services/identity/v3/endpoint_groups_client.py
new file mode 100644
index 0000000..ce99389
--- /dev/null
+++ b/tempest/lib/services/identity/v3/endpoint_groups_client.py
@@ -0,0 +1,78 @@
+# Copyright 2017 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 EndPointGroupsClient(rest_client.RestClient):
+ api_version = "v3"
+
+ def create_endpoint_group(self, **kwargs):
+ """Create endpoint group.
+
+ For a full list of available parameters, please refer to the
+ official API reference:
+ https://developer.openstack.org/api-ref/identity/v3-ext/#create-endpoint-group
+ """
+ post_body = json.dumps({'endpoint_group': kwargs})
+ resp, body = self.post('OS-EP-FILTER/endpoint_groups', post_body)
+ self.expected_success(201, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_endpoint_group(self, endpoint_group_id, **kwargs):
+ """Update endpoint group.
+
+ For a full list of available parameters, please refer to the
+ official API reference:
+ https://developer.openstack.org/api-ref/identity/v3-ext/#update-endpoint-group
+ """
+ post_body = json.dumps({'endpoint_group': kwargs})
+ resp, body = self.patch(
+ 'OS-EP-FILTER/endpoint_groups/%s' % endpoint_group_id, post_body)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_endpoint_group(self, endpoint_group_id):
+ """Delete endpoint group."""
+ resp_header, resp_body = self.delete(
+ 'OS-EP-FILTER/endpoint_groups/%s' % endpoint_group_id)
+ self.expected_success(204, resp_header.status)
+ return rest_client.ResponseBody(resp_header, resp_body)
+
+ def show_endpoint_group(self, endpoint_group_id):
+ """Get endpoint group."""
+ resp_header, resp_body = self.get(
+ 'OS-EP-FILTER/endpoint_groups/%s' % endpoint_group_id)
+ self.expected_success(200, resp_header.status)
+ resp_body = json.loads(resp_body)
+ return rest_client.ResponseBody(resp_header, resp_body)
+
+ def check_endpoint_group(self, endpoint_group_id):
+ """Check endpoint group."""
+ resp_header, resp_body = self.head(
+ 'OS-EP-FILTER/endpoint_groups/%s' % endpoint_group_id)
+ self.expected_success(200, resp_header.status)
+ return rest_client.ResponseBody(resp_header, resp_body)
+
+ def list_endpoint_groups(self):
+ """Get endpoint groups."""
+ resp_header, resp_body = self.get('OS-EP-FILTER/endpoint_groups')
+ self.expected_success(200, resp_header.status)
+ resp_body = json.loads(resp_body)
+ return rest_client.ResponseBody(resp_header, resp_body)
diff --git a/tempest/lib/services/identity/v3/endpoints_client.py b/tempest/lib/services/identity/v3/endpoints_client.py
new file mode 100644
index 0000000..e24dca7
--- /dev/null
+++ b/tempest/lib/services/identity/v3/endpoints_client.py
@@ -0,0 +1,81 @@
+# Copyright 2013 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.
+
+"""
+https://developer.openstack.org/api-ref/identity/v3/index.html#service-catalog-and-endpoints
+"""
+
+from oslo_serialization import jsonutils as json
+from six.moves.urllib import parse as urllib
+
+from tempest.lib.common import rest_client
+
+
+class EndPointsClient(rest_client.RestClient):
+ api_version = "v3"
+
+ def list_endpoints(self, **params):
+ """List endpoints.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/#list-endpoints
+ """
+ url = 'endpoints'
+ 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 create_endpoint(self, **kwargs):
+ """Create endpoint.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#create-endpoint
+ """
+ post_body = json.dumps({'endpoint': kwargs})
+ resp, body = self.post('endpoints', post_body)
+ self.expected_success(201, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_endpoint(self, endpoint_id, **kwargs):
+ """Updates an endpoint with given parameters.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#update-endpoint
+ """
+ post_body = json.dumps({'endpoint': kwargs})
+ resp, body = self.patch('endpoints/%s' % endpoint_id, post_body)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_endpoint(self, endpoint_id):
+ """Delete endpoint."""
+ resp_header, resp_body = self.delete('endpoints/%s' % endpoint_id)
+ self.expected_success(204, resp_header.status)
+ return rest_client.ResponseBody(resp_header, resp_body)
+
+ def show_endpoint(self, endpoint_id):
+ """Get endpoint."""
+ resp_header, resp_body = self.get('endpoints/%s' % endpoint_id)
+ self.expected_success(200, resp_header.status)
+ resp_body = json.loads(resp_body)
+ return rest_client.ResponseBody(resp_header, resp_body)
diff --git a/tempest/lib/services/identity/v3/groups_client.py b/tempest/lib/services/identity/v3/groups_client.py
new file mode 100644
index 0000000..bc6ead0
--- /dev/null
+++ b/tempest/lib/services/identity/v3/groups_client.py
@@ -0,0 +1,115 @@
+# Copyright 2013 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.
+
+"""
+https://developer.openstack.org/api-ref/identity/v3/index.html#groups
+"""
+
+from oslo_serialization import jsonutils as json
+from six.moves.urllib import parse as urllib
+
+from tempest.lib.common import rest_client
+
+
+class GroupsClient(rest_client.RestClient):
+ api_version = "v3"
+
+ def create_group(self, **kwargs):
+ """Creates a group.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#create-group
+ """
+ post_body = json.dumps({'group': kwargs})
+ resp, body = self.post('groups', post_body)
+ self.expected_success(201, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_group(self, group_id):
+ """Get group details."""
+ resp, body = self.get('groups/%s' % group_id)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_groups(self, **params):
+ """Lists the groups.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/#list-groups
+ """
+ url = 'groups'
+ 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 update_group(self, group_id, **kwargs):
+ """Updates a group.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#update-group
+ """
+ post_body = json.dumps({'group': kwargs})
+ resp, body = self.patch('groups/%s' % group_id, post_body)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_group(self, group_id):
+ """Delete a group."""
+ resp, body = self.delete('groups/%s' % group_id)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def add_group_user(self, group_id, user_id):
+ """Add user into group."""
+ resp, body = self.put('groups/%s/users/%s' % (group_id, user_id),
+ None)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_group_users(self, group_id, **params):
+ """List users in group.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/#list-users-in-group
+ """
+ url = 'groups/%s/users' % group_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_group_user(self, group_id, user_id):
+ """Delete user in group."""
+ resp, body = self.delete('groups/%s/users/%s' % (group_id, user_id))
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def check_group_user_existence(self, group_id, user_id):
+ """Check user in group."""
+ resp, body = self.head('groups/%s/users/%s' % (group_id, user_id))
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp)
diff --git a/tempest/lib/services/identity/v3/identity_client.py b/tempest/lib/services/identity/v3/identity_client.py
new file mode 100644
index 0000000..ad770bf
--- /dev/null
+++ b/tempest/lib/services/identity/v3/identity_client.py
@@ -0,0 +1,66 @@
+# Copyright 2013 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
+
+
+class IdentityClient(rest_client.RestClient):
+ api_version = "v3"
+
+ def show_api_description(self):
+ """Retrieves info about the v3 Identity API"""
+ url = ''
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_token(self, resp_token):
+ """Get token details."""
+ headers = {'X-Subject-Token': resp_token}
+ resp, body = self.get("auth/tokens", headers=headers)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_token(self, resp_token):
+ """Deletes token."""
+ headers = {'X-Subject-Token': resp_token}
+ resp, body = self.delete("auth/tokens", headers=headers)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def check_token_existence(self, resp_token):
+ """Validates a token."""
+ headers = {'X-Subject-Token': resp_token}
+ resp, body = self.head("auth/tokens", headers=headers)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_auth_projects(self):
+ """Get available project scopes."""
+ resp, body = self.get("auth/projects")
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_auth_domains(self):
+ """Get available domain scopes."""
+ resp, body = self.get("auth/domains")
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/identity/v3/inherited_roles_client.py b/tempest/lib/services/identity/v3/inherited_roles_client.py
new file mode 100644
index 0000000..691c7fd
--- /dev/null
+++ b/tempest/lib/services/identity/v3/inherited_roles_client.py
@@ -0,0 +1,151 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# 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 InheritedRolesClient(rest_client.RestClient):
+ api_version = "v3"
+
+ def create_inherited_role_on_domains_user(
+ self, domain_id, user_id, role_id):
+ """Assigns a role to a user on projects owned by a domain."""
+ resp, body = self.put(
+ "OS-INHERIT/domains/%s/users/%s/roles/%s/inherited_to_projects"
+ % (domain_id, user_id, role_id), None)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_inherited_role_from_user_on_domain(
+ self, domain_id, user_id, role_id):
+ """Revokes an inherited project role from a user on a domain."""
+ resp, body = self.delete(
+ "OS-INHERIT/domains/%s/users/%s/roles/%s/inherited_to_projects"
+ % (domain_id, user_id, role_id))
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_inherited_project_role_for_user_on_domain(
+ self, domain_id, user_id):
+ """Lists the inherited project roles on a domain for a user."""
+ resp, body = self.get(
+ "OS-INHERIT/domains/%s/users/%s/roles/inherited_to_projects"
+ % (domain_id, user_id))
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def check_user_inherited_project_role_on_domain(
+ self, domain_id, user_id, role_id):
+ """Checks whether a user has an inherited project role on a domain."""
+ resp, body = self.head(
+ "OS-INHERIT/domains/%s/users/%s/roles/%s/inherited_to_projects"
+ % (domain_id, user_id, role_id))
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp)
+
+ def create_inherited_role_on_domains_group(
+ self, domain_id, group_id, role_id):
+ """Assigns a role to a group on projects owned by a domain."""
+ resp, body = self.put(
+ "OS-INHERIT/domains/%s/groups/%s/roles/%s/inherited_to_projects"
+ % (domain_id, group_id, role_id), None)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_inherited_role_from_group_on_domain(
+ self, domain_id, group_id, role_id):
+ """Revokes an inherited project role from a group on a domain."""
+ resp, body = self.delete(
+ "OS-INHERIT/domains/%s/groups/%s/roles/%s/inherited_to_projects"
+ % (domain_id, group_id, role_id))
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_inherited_project_role_for_group_on_domain(
+ self, domain_id, group_id):
+ """Lists the inherited project roles on a domain for a group."""
+ resp, body = self.get(
+ "OS-INHERIT/domains/%s/groups/%s/roles/inherited_to_projects"
+ % (domain_id, group_id))
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def check_group_inherited_project_role_on_domain(
+ self, domain_id, group_id, role_id):
+ """Checks whether a group has an inherited project role on a domain."""
+ resp, body = self.head(
+ "OS-INHERIT/domains/%s/groups/%s/roles/%s/inherited_to_projects"
+ % (domain_id, group_id, role_id))
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp)
+
+ def create_inherited_role_on_projects_user(
+ self, project_id, user_id, role_id):
+ """Assigns a role to a user on projects in a subtree."""
+ resp, body = self.put(
+ "OS-INHERIT/projects/%s/users/%s/roles/%s/inherited_to_projects"
+ % (project_id, user_id, role_id), None)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_inherited_role_from_user_on_project(
+ self, project_id, user_id, role_id):
+ """Revokes an inherited role from a user on a project."""
+ resp, body = self.delete(
+ "OS-INHERIT/projects/%s/users/%s/roles/%s/inherited_to_projects"
+ % (project_id, user_id, role_id))
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def check_user_has_flag_on_inherited_to_project(
+ self, project_id, user_id, role_id):
+ """Checks whether a user has a role assignment"""
+ """with the inherited_to_projects flag on a project."""
+ resp, body = self.head(
+ "OS-INHERIT/projects/%s/users/%s/roles/%s/inherited_to_projects"
+ % (project_id, user_id, role_id))
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp)
+
+ def create_inherited_role_on_projects_group(
+ self, project_id, group_id, role_id):
+ """Assigns a role to a group on projects in a subtree."""
+ resp, body = self.put(
+ "OS-INHERIT/projects/%s/groups/%s/roles/%s/inherited_to_projects"
+ % (project_id, group_id, role_id), None)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_inherited_role_from_group_on_project(
+ self, project_id, group_id, role_id):
+ """Revokes an inherited role from a group on a project."""
+ resp, body = self.delete(
+ "OS-INHERIT/projects/%s/groups/%s/roles/%s/inherited_to_projects"
+ % (project_id, group_id, role_id))
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def check_group_has_flag_on_inherited_to_project(
+ self, project_id, group_id, role_id):
+ """Checks whether a group has a role assignment"""
+ """with the inherited_to_projects flag on a project."""
+ resp, body = self.head(
+ "OS-INHERIT/projects/%s/groups/%s/roles/%s/inherited_to_projects"
+ % (project_id, group_id, role_id))
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp)
diff --git a/tempest/lib/services/identity/v3/oauth_consumers_client.py b/tempest/lib/services/identity/v3/oauth_consumers_client.py
new file mode 100644
index 0000000..97fb141
--- /dev/null
+++ b/tempest/lib/services/identity/v3/oauth_consumers_client.py
@@ -0,0 +1,95 @@
+# Copyright 2017 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 OAUTHConsumerClient(rest_client.RestClient):
+ api_version = "v3"
+
+ def create_consumer(self, description=None):
+ """Creates a consumer.
+
+ :param str description: Optional field to add notes about the consumer
+
+ For more information, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3-ext/#create-consumer
+ """
+ post_body = {"description": description}
+ post_body = json.dumps({'consumer': post_body})
+ resp, body = self.post('OS-OAUTH1/consumers', post_body)
+ self.expected_success(201, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_consumer(self, consumer_id):
+ """Deletes a consumer.
+
+ :param str consumer_id: The ID of the consumer that will be deleted
+
+ For more information, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3-ext/#delete-consumer
+ """
+ resp, body = self.delete('OS-OAUTH1/consumers/%s' % consumer_id)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_consumer(self, consumer_id, description=None):
+ """Updates a consumer.
+
+ :param str consumer_id: The ID of the consumer that will be updated
+ :param str description: Optional field to add notes about the consumer
+
+ For more information, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/identity/v3-ext/#update-consumer
+ """
+ post_body = {"description": description}
+ post_body = json.dumps({'consumer': post_body})
+ resp, body = self.patch('OS-OAUTH1/consumers/%s' % consumer_id,
+ post_body)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_consumer(self, consumer_id):
+ """Show consumer details.
+
+ :param str consumer_id: The ID of the consumer that will be shown
+
+ For more information, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3-ext/#show-consumer-details
+ """
+ resp, body = self.get('OS-OAUTH1/consumers/%s' % consumer_id)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_consumers(self):
+ """List all consumers.
+
+ For more information, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3-ext/#list-consumers
+ """
+ resp, body = self.get('OS-OAUTH1/consumers')
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/identity/v3/oauth_token_client.py b/tempest/lib/services/identity/v3/oauth_token_client.py
new file mode 100644
index 0000000..b1d298b
--- /dev/null
+++ b/tempest/lib/services/identity/v3/oauth_token_client.py
@@ -0,0 +1,236 @@
+# Copyright 2017 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 binascii
+import hashlib
+import hmac
+import random
+import time
+
+import six
+from six.moves.urllib import parse as urlparse
+
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+
+
+class OAUTHTokenClient(rest_client.RestClient):
+ api_version = "v3"
+
+ def _escape(self, s):
+ """Escape a unicode string in an OAuth-compatible fashion."""
+ safe = b'~'
+ s = s.encode('utf-8') if isinstance(s, six.text_type) else s
+ s = urlparse.quote(s, safe)
+ if isinstance(s, six.binary_type):
+ s = s.decode('utf-8')
+ return s
+
+ def _generate_params_with_signature(self, client_key, uri,
+ client_secret=None,
+ resource_owner_key=None,
+ resource_owner_secret=None,
+ callback_uri=None,
+ verifier=None,
+ http_method='GET'):
+ """Generate OAUTH params along with signature."""
+ timestamp = six.text_type(int(time.time()))
+ nonce = six.text_type(random.getrandbits(64)) + timestamp
+ oauth_params = [
+ ('oauth_nonce', nonce),
+ ('oauth_timestamp', timestamp),
+ ('oauth_version', '1.0'),
+ ('oauth_signature_method', 'HMAC-SHA1'),
+ ('oauth_consumer_key', client_key),
+ ]
+ if resource_owner_key:
+ oauth_params.append(('oauth_token', resource_owner_key))
+ if callback_uri:
+ oauth_params.append(('oauth_callback', callback_uri))
+ if verifier:
+ oauth_params.append(('oauth_verifier', verifier))
+
+ # normalize_params
+ key_values = [(self._escape(k), self._escape(v))
+ for k, v in oauth_params]
+ key_values.sort()
+ parameter_parts = ['{0}={1}'.format(k, v) for k, v in key_values]
+ normalized_params = '&'.join(parameter_parts)
+
+ # normalize_uri
+ scheme, netloc, path, params, query, fragment = urlparse.urlparse(uri)
+ scheme = scheme.lower()
+ netloc = netloc.lower()
+ normalized_uri = urlparse.urlunparse((scheme, netloc, path,
+ params, '', ''))
+
+ # construct base string
+ base_string = self._escape(http_method.upper())
+ base_string += '&'
+ base_string += self._escape(normalized_uri)
+ base_string += '&'
+ base_string += self._escape(normalized_params)
+
+ # sign using hmac-sha1
+ key = self._escape(client_secret or '')
+ key += '&'
+ key += self._escape(resource_owner_secret or '')
+ key_utf8 = key.encode('utf-8')
+ text_utf8 = base_string.encode('utf-8')
+ signature = hmac.new(key_utf8, text_utf8, hashlib.sha1)
+ sig = binascii.b2a_base64(signature.digest())[:-1].decode('utf-8')
+
+ oauth_params.append(('oauth_signature', sig))
+ return oauth_params
+
+ def _generate_oauth_header(self, oauth_params):
+ authorization_header = {}
+ authorization_header_parameters_parts = []
+ for oauth_parameter_name, value in oauth_params:
+ escaped_name = self._escape(oauth_parameter_name)
+ escaped_value = self._escape(value)
+ part = '{0}="{1}"'.format(escaped_name, escaped_value)
+ authorization_header_parameters_parts.append(part)
+
+ authorization_header_parameters = ', '.join(
+ authorization_header_parameters_parts)
+ oauth_string = 'OAuth %s' % authorization_header_parameters
+ authorization_header['Authorization'] = oauth_string
+
+ return authorization_header
+
+ def create_request_token(self, consumer_key, consumer_secret, project_id):
+ """Create request token.
+
+ For more information, please refer to the official API reference:
+ http://developer.openstack.org/api-ref/identity/v3-ext/#create-request-token
+ """
+ endpoint = 'OS-OAUTH1/request_token'
+ headers = {'Requested-Project-Id': project_id}
+ oauth_params = self._generate_params_with_signature(
+ consumer_key,
+ self.base_url + '/' + endpoint,
+ client_secret=consumer_secret,
+ callback_uri='oob',
+ http_method='POST')
+ oauth_header = self._generate_oauth_header(oauth_params)
+ headers.update(oauth_header)
+ resp, body = self.post(endpoint,
+ body=None,
+ headers=headers)
+ self.expected_success(201, resp.status)
+ if not isinstance(body, str):
+ body = body.decode('utf-8')
+ body = dict(item.split("=") for item in body.split("&"))
+ return rest_client.ResponseBody(resp, body)
+
+ def authorize_request_token(self, request_token_id, role_ids):
+ """Authorize request token.
+
+ For more information, please refer to the official API reference:
+ http://developer.openstack.org/api-ref/identity/v3-ext/#authorize-request-token
+ """
+ roles = [{'id': role_id} for role_id in role_ids]
+ body = {'roles': roles}
+ post_body = json.dumps(body)
+ resp, body = self.put("OS-OAUTH1/authorize/%s" % request_token_id,
+ post_body)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def create_access_token(self, consumer_key, consumer_secret, request_key,
+ request_secret, oauth_verifier):
+ """Create access token.
+
+ For more information, please refer to the official API reference:
+ http://developer.openstack.org/api-ref/identity/v3-ext/#create-access-token
+ """
+ endpoint = 'OS-OAUTH1/access_token'
+ oauth_params = self._generate_params_with_signature(
+ consumer_key,
+ self.base_url + '/' + endpoint,
+ client_secret=consumer_secret,
+ resource_owner_key=request_key,
+ resource_owner_secret=request_secret,
+ verifier=oauth_verifier,
+ http_method='POST')
+ headers = self._generate_oauth_header(oauth_params)
+ resp, body = self.post(endpoint, body=None, headers=headers)
+ self.expected_success(201, resp.status)
+ if not isinstance(body, str):
+ body = body.decode('utf-8')
+ body = dict(item.split("=") for item in body.split("&"))
+ return rest_client.ResponseBody(resp, body)
+
+ def get_access_token(self, user_id, access_token_id):
+ """Get access token.
+
+ For more information, please refer to the official API reference:
+ http://developer.openstack.org/api-ref/identity/v3-ext/#get-access-token
+ """
+ resp, body = self.get("users/%s/OS-OAUTH1/access_tokens/%s"
+ % (user_id, access_token_id))
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def revoke_access_token(self, user_id, access_token_id):
+ """Revoke access token.
+
+ For more information, please refer to the official API reference:
+ http://developer.openstack.org/api-ref/identity/v3-ext/#revoke-access-token
+ """
+ resp, body = self.delete("users/%s/OS-OAUTH1/access_tokens/%s"
+ % (user_id, access_token_id))
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_access_tokens(self, user_id):
+ """List access tokens.
+
+ For more information, please refer to the official API reference:
+ http://developer.openstack.org/api-ref/identity/v3-ext/#list-access-tokens
+ """
+ resp, body = self.get("users/%s/OS-OAUTH1/access_tokens"
+ % (user_id))
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_access_token_roles(self, user_id, access_token_id):
+ """List roles for an access token.
+
+ For more information, please refer to the official API reference:
+ http://developer.openstack.org/api-ref/identity/v3-ext/#list-roles-for-an-access-token
+ """
+ resp, body = self.get("users/%s/OS-OAUTH1/access_tokens/%s/roles"
+ % (user_id, access_token_id))
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def get_access_token_role(self, user_id, access_token_id, role_id):
+ """Show role details for an access token.
+
+ For more information, please refer to the official API reference:
+ http://developer.openstack.org/api-ref/identity/v3-ext/#show-role-details-for-an-access-token
+ """
+ resp, body = self.get("users/%s/OS-OAUTH1/access_tokens/%s/roles/%s"
+ % (user_id, access_token_id, role_id))
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/identity/v3/policies_client.py b/tempest/lib/services/identity/v3/policies_client.py
new file mode 100644
index 0000000..ca8dbbd
--- /dev/null
+++ b/tempest/lib/services/identity/v3/policies_client.py
@@ -0,0 +1,187 @@
+# Copyright 2013 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.
+
+"""
+https://developer.openstack.org/api-ref/identity/v3/index.html#policies
+"""
+
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+
+
+class PoliciesClient(rest_client.RestClient):
+ api_version = "v3"
+
+ def create_policy(self, **kwargs):
+ """Creates a Policy.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#create-policy
+ """
+ post_body = json.dumps({'policy': kwargs})
+ resp, body = self.post('policies', post_body)
+ self.expected_success(201, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_policies(self):
+ """Lists the policies."""
+ resp, body = self.get('policies')
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_policy(self, policy_id):
+ """Lists out the given policy."""
+ url = 'policies/%s' % policy_id
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_policy(self, policy_id, **kwargs):
+ """Updates a policy.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#update-policy
+ """
+ post_body = json.dumps({'policy': kwargs})
+ url = 'policies/%s' % policy_id
+ resp, body = self.patch(url, post_body)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_policy(self, policy_id):
+ """Deletes the policy."""
+ url = "policies/%s" % policy_id
+ resp, body = self.delete(url)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_policy_association_for_endpoint(self, policy_id, endpoint_id):
+ """Create policy association with endpoint.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/identity/v3-ext/index.html#associate-policy-and-endpoint
+ """
+ url = "policies/{0}/OS-ENDPOINT-POLICY/endpoints/{1}"\
+ .format(policy_id, endpoint_id)
+ resp, body = self.put(url, '{}')
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_policy_association_for_endpoint(self, policy_id, endpoint_id):
+ """Get policy association of endpoint.
+
+ API reference:
+ https://developer.openstack.org/api-ref/identity/v3-ext/index.html#verify-a-policy-and-endpoint-association
+ """
+ url = "policies/{0}/OS-ENDPOINT-POLICY/endpoints/{1}"\
+ .format(policy_id, endpoint_id)
+ resp, body = self.get(url)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_policy_association_for_endpoint(self, policy_id, endpoint_id):
+ """Delete policy association with endpoint.
+
+ API reference:
+ https://developer.openstack.org/api-ref/identity/v3-ext/index.html#delete-a-policy-and-endpoint-association
+ """
+ url = "policies/{0}/OS-ENDPOINT-POLICY/endpoints/{1}"\
+ .format(policy_id, endpoint_id)
+ resp, body = self.delete(url)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_policy_association_for_service(self, policy_id, service_id):
+ """Create policy association with service.
+
+ API reference:
+ https://developer.openstack.org/api-ref/identity/v3-ext/index.html#associate-policy-and-service-type-endpoint
+ """
+ url = "policies/{0}/OS-ENDPOINT-POLICY/services/{1}"\
+ .format(policy_id, service_id)
+ resp, body = self.put(url, '{}')
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_policy_association_for_service(self, policy_id, service_id):
+ """Get policy association of service.
+
+ API Reference:
+ https://developer.openstack.org/api-ref/identity/v3-ext/index.html#verify-a-policy-and-service-type-endpoint-association
+ """
+ url = "policies/{0}/OS-ENDPOINT-POLICY/services/{1}"\
+ .format(policy_id, service_id)
+ resp, body = self.get(url)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_policy_association_for_service(self, policy_id, service_id):
+ """Delete policy association with service.
+
+ API reference:
+ https://developer.openstack.org/api-ref/identity/v3-ext/index.html#delete-a-policy-and-service-type-endpoint-association
+ """
+ url = "policies/{0}/OS-ENDPOINT-POLICY/services/{1}"\
+ .format(policy_id, service_id)
+ resp, body = self.delete(url)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_policy_association_for_region_and_service(
+ self, policy_id, service_id, region_id):
+ """Create policy association with service and region.
+
+ API reference:
+ https://developer.openstack.org/api-ref/identity/v3-ext/index.html#associate-policy-and-service-type-endpoint-in-a-region
+ """
+ url = "policies/{0}/OS-ENDPOINT-POLICY/services/{1}/regions/{2}"\
+ .format(policy_id, service_id, region_id)
+ resp, body = self.put(url, '{}')
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_policy_association_for_region_and_service(
+ self, policy_id, service_id, region_id):
+ """Get policy association of service and region.
+
+ API reference:
+ https://developer.openstack.org/api-ref/identity/v3-ext/index.html#verify-a-policy-and-service-type-endpoint-in-a-region-association
+ """
+ url = "policies/{0}/OS-ENDPOINT-POLICY/services/{1}/regions/{2}"\
+ .format(policy_id, service_id, region_id)
+ resp, body = self.get(url)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_policy_association_for_region_and_service(
+ self, policy_id, service_id, region_id):
+ """Delete policy association with service and region.
+
+ API reference:
+ https://developer.openstack.org/api-ref/identity/v3-ext/index.html#delete-a-policy-and-service-type-endpoint-in-a-region-association
+ """
+ url = "policies/{0}/OS-ENDPOINT-POLICY/services/{1}/regions/{2}"\
+ .format(policy_id, service_id, region_id)
+ resp, body = self.delete(url)
+ 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/projects_client.py b/tempest/lib/services/identity/v3/projects_client.py
new file mode 100644
index 0000000..20787da
--- /dev/null
+++ b/tempest/lib/services/identity/v3/projects_client.py
@@ -0,0 +1,75 @@
+# Copyright 2013 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 ProjectsClient(rest_client.RestClient):
+ api_version = "v3"
+
+ def create_project(self, name, **kwargs):
+ """Create a Project.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#create-project
+
+ """
+ # Include the project name to the kwargs parameters
+ kwargs['name'] = name
+ post_body = json.dumps({'project': kwargs})
+ resp, body = self.post('projects', post_body)
+ self.expected_success(201, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_projects(self, params=None):
+ url = "projects"
+ 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 update_project(self, project_id, **kwargs):
+ """Update a Project.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#update-project
+
+ """
+ post_body = json.dumps({'project': kwargs})
+ resp, body = self.patch('projects/%s' % project_id, post_body)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_project(self, project_id):
+ """GET a Project."""
+ resp, body = self.get("projects/%s" % project_id)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_project(self, project_id):
+ """Delete a project."""
+ resp, body = self.delete('projects/%s' % project_id)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/identity/v3/regions_client.py b/tempest/lib/services/identity/v3/regions_client.py
new file mode 100644
index 0000000..d7507cf
--- /dev/null
+++ b/tempest/lib/services/identity/v3/regions_client.py
@@ -0,0 +1,83 @@
+# Copyright 2014 Hewlett-Packard Development Company, L.P
+# 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#regions
+"""
+
+from oslo_serialization import jsonutils as json
+from six.moves.urllib import parse as urllib
+
+from tempest.lib.common import rest_client
+
+
+class RegionsClient(rest_client.RestClient):
+ api_version = "v3"
+
+ def create_region(self, region_id=None, **kwargs):
+ """Create region.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#create-region
+ """
+ if region_id is not None:
+ method = self.put
+ url = 'regions/%s' % region_id
+ else:
+ method = self.post
+ url = 'regions'
+ req_body = json.dumps({'region': kwargs})
+ resp, body = method(url, req_body)
+ self.expected_success(201, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_region(self, region_id, **kwargs):
+ """Updates a region.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#update-region
+ """
+ post_body = json.dumps({'region': kwargs})
+ resp, body = self.patch('regions/%s' % region_id, post_body)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_region(self, region_id):
+ """Get region."""
+ url = 'regions/%s' % region_id
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_regions(self, params=None):
+ """List regions."""
+ url = 'regions'
+ 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_region(self, region_id):
+ """Delete region."""
+ resp, body = self.delete('regions/%s' % region_id)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/identity/v3/role_assignments_client.py b/tempest/lib/services/identity/v3/role_assignments_client.py
new file mode 100644
index 0000000..a426e69
--- /dev/null
+++ b/tempest/lib/services/identity/v3/role_assignments_client.py
@@ -0,0 +1,48 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# 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 RoleAssignmentsClient(rest_client.RestClient):
+ api_version = "v3"
+
+ def list_role_assignments(self, effective=False, **kwargs):
+ """List role assignments.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/#list-role-assignments
+
+ :param effective: If True, returns the effective assignments, including
+ any assignments gained by virtue of group membership
+ or inherited roles.
+ """
+ url = 'role_assignments'
+ if kwargs:
+ # NOTE(rodrigods): "effective" is a key-only query parameter and
+ # is treated below.
+ if 'effective' in kwargs:
+ del kwargs['effective']
+ url += '?%s' % urllib.urlencode(kwargs)
+ if effective:
+ url += '&effective'
+
+ 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/identity/v3/roles_client.py b/tempest/lib/services/identity/v3/roles_client.py
new file mode 100644
index 0000000..43e3c01
--- /dev/null
+++ b/tempest/lib/services/identity/v3/roles_client.py
@@ -0,0 +1,241 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# 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 RolesClient(rest_client.RestClient):
+ api_version = "v3"
+
+ def create_role(self, **kwargs):
+ """Create a Role.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#create-role
+ """
+ post_body = json.dumps({'role': kwargs})
+ resp, body = self.post('roles', post_body)
+ self.expected_success(201, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_role(self, role_id):
+ """GET a Role."""
+ resp, body = self.get('roles/%s' % role_id)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_roles(self, **params):
+ """Get the list of Roles."""
+
+ url = 'roles'
+ 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 update_role(self, role_id, **kwargs):
+ """Update a Role.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#update-role
+ """
+ post_body = json.dumps({'role': kwargs})
+ resp, body = self.patch('roles/%s' % role_id, post_body)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_role(self, role_id):
+ """Delete a role."""
+ resp, body = self.delete('roles/%s' % role_id)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def create_user_role_on_project(self, project_id, user_id, role_id):
+ """Add roles to a user on a project."""
+ resp, body = self.put('projects/%s/users/%s/roles/%s' %
+ (project_id, user_id, role_id), None)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def create_user_role_on_domain(self, domain_id, user_id, role_id):
+ """Add roles to a user on a domain."""
+ resp, body = self.put('domains/%s/users/%s/roles/%s' %
+ (domain_id, user_id, role_id), None)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_user_roles_on_project(self, project_id, user_id):
+ """list roles of a user on a project."""
+ resp, body = self.get('projects/%s/users/%s/roles' %
+ (project_id, user_id))
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_user_roles_on_domain(self, domain_id, user_id):
+ """list roles of a user on a domain."""
+ resp, body = self.get('domains/%s/users/%s/roles' %
+ (domain_id, user_id))
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_role_from_user_on_project(self, project_id, user_id, role_id):
+ """Delete role of a user on a project."""
+ resp, body = self.delete('projects/%s/users/%s/roles/%s' %
+ (project_id, user_id, role_id))
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_role_from_user_on_domain(self, domain_id, user_id, role_id):
+ """Delete role of a user on a domain."""
+ resp, body = self.delete('domains/%s/users/%s/roles/%s' %
+ (domain_id, user_id, role_id))
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def check_user_role_existence_on_project(self, project_id,
+ user_id, role_id):
+ """Check role of a user on a project."""
+ resp, body = self.head('projects/%s/users/%s/roles/%s' %
+ (project_id, user_id, role_id))
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp)
+
+ def check_user_role_existence_on_domain(self, domain_id,
+ user_id, role_id):
+ """Check role of a user on a domain."""
+ resp, body = self.head('domains/%s/users/%s/roles/%s' %
+ (domain_id, user_id, role_id))
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp)
+
+ def create_group_role_on_project(self, project_id, group_id, role_id):
+ """Add roles to a group on a project."""
+ resp, body = self.put('projects/%s/groups/%s/roles/%s' %
+ (project_id, group_id, role_id), None)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def create_group_role_on_domain(self, domain_id, group_id, role_id):
+ """Add roles to a group on a domain."""
+ resp, body = self.put('domains/%s/groups/%s/roles/%s' %
+ (domain_id, group_id, role_id), None)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_group_roles_on_project(self, project_id, group_id):
+ """list roles of a group on a project."""
+ resp, body = self.get('projects/%s/groups/%s/roles' %
+ (project_id, group_id))
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_group_roles_on_domain(self, domain_id, group_id):
+ """list roles of a group on a domain."""
+ resp, body = self.get('domains/%s/groups/%s/roles' %
+ (domain_id, group_id))
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_role_from_group_on_project(self, project_id, group_id, role_id):
+ """Delete role of a group on a project."""
+ resp, body = self.delete('projects/%s/groups/%s/roles/%s' %
+ (project_id, group_id, role_id))
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_role_from_group_on_domain(self, domain_id, group_id, role_id):
+ """Delete role of a group on a domain."""
+ resp, body = self.delete('domains/%s/groups/%s/roles/%s' %
+ (domain_id, group_id, role_id))
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def check_role_from_group_on_project_existence(self, project_id,
+ group_id, role_id):
+ """Check role of a group on a project."""
+ resp, body = self.head('projects/%s/groups/%s/roles/%s' %
+ (project_id, group_id, role_id))
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp)
+
+ def check_role_from_group_on_domain_existence(self, domain_id,
+ group_id, role_id):
+ """Check role of a group on a domain."""
+ resp, body = self.head('domains/%s/groups/%s/roles/%s' %
+ (domain_id, group_id, role_id))
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp)
+
+ def create_role_inference_rule(self, prior_role, implies_role):
+ """Create a role inference rule."""
+ resp, body = self.put('roles/%s/implies/%s' %
+ (prior_role, implies_role), None)
+ self.expected_success(201, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_role_inference_rule(self, prior_role, implies_role):
+ """Get a role inference rule."""
+ resp, body = self.get('roles/%s/implies/%s' %
+ (prior_role, implies_role))
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_role_inferences_rules(self, prior_role):
+ """List the inferences rules from a role."""
+ resp, body = self.get('roles/%s/implies' % prior_role)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_all_role_inference_rules(self):
+ """Lists all role inference rules.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#list-all-role-inference-rules
+ """
+ resp, body = self.get('role_inferences')
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def check_role_inference_rule(self, prior_role, implies_role):
+ """Check a role inference rule."""
+ resp, body = self.head('roles/%s/implies/%s' %
+ (prior_role, implies_role))
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp)
+
+ def delete_role_inference_rule(self, prior_role, implies_role):
+ """Delete a role inference rule."""
+ resp, body = self.delete('roles/%s/implies/%s' %
+ (prior_role, implies_role))
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp)
diff --git a/tempest/lib/services/identity/v3/services_client.py b/tempest/lib/services/identity/v3/services_client.py
new file mode 100644
index 0000000..7bbe850
--- /dev/null
+++ b/tempest/lib/services/identity/v3/services_client.py
@@ -0,0 +1,82 @@
+# Copyright 2013 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.
+
+"""
+https://developer.openstack.org/api-ref/identity/v3/index.html#service-catalog-and-endpoints
+"""
+
+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):
+ api_version = "v3"
+
+ def update_service(self, service_id, **kwargs):
+ """Updates a service.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#update-service
+ """
+ patch_body = json.dumps({'service': kwargs})
+ resp, body = self.patch('services/%s' % service_id, patch_body)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_service(self, service_id):
+ """Get Service."""
+ url = 'services/%s' % service_id
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def create_service(self, **kwargs):
+ """Creates a service.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#create-service
+ """
+ body = json.dumps({'service': kwargs})
+ resp, body = self.post("services", body)
+ self.expected_success(201, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_service(self, service_id):
+ url = "services/" + service_id
+ resp, body = self.delete(url)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_services(self, **params):
+ """List services.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/#list-services
+ """
+ url = 'services'
+ 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)
diff --git a/tempest/lib/services/identity/v3/token_client.py b/tempest/lib/services/identity/v3/token_client.py
new file mode 100644
index 0000000..d591f03
--- /dev/null
+++ b/tempest/lib/services/identity/v3/token_client.py
@@ -0,0 +1,207 @@
+# Copyright 2015 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 oslo_log import log as logging
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+from tempest.lib import exceptions
+
+
+class V3TokenClient(rest_client.RestClient):
+
+ def __init__(self, auth_url, disable_ssl_certificate_validation=None,
+ ca_certs=None, trace_requests=None, **kwargs):
+ """Initialises the Token client
+
+ :param auth_url: URL to which the token request is sent
+ :param disable_ssl_certificate_validation: pass-through to rest client
+ :param ca_certs: pass-through to rest client
+ :param trace_requests: pass-through to rest client
+ :param kwargs: any extra parameter to pass through the rest client.
+ Three kwargs are forbidden: region, service and auth_provider
+ as they are not meaningful for token client
+ """
+ dscv = disable_ssl_certificate_validation
+ for unwanted_kwargs in ['region', 'service', 'auth_provider']:
+ kwargs.pop(unwanted_kwargs, None)
+ super(V3TokenClient, self).__init__(
+ None, None, None, disable_ssl_certificate_validation=dscv,
+ ca_certs=ca_certs, trace_requests=trace_requests, **kwargs)
+
+ if auth_url is None:
+ raise exceptions.IdentityError("Couldn't determine auth_url")
+
+ if 'auth/tokens' not in auth_url:
+ auth_url = auth_url.rstrip('/') + '/auth/tokens'
+
+ self.auth_url = auth_url
+
+ 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, app_cred_id=None,
+ app_cred_secret=None):
+ """Obtains a token from the authentication service
+
+ :param user_id: user id
+ :param username: user name
+ :param user_domain_id: the user domain id
+ :param user_domain_name: the user domain name
+ :param project_domain_id: the project domain id
+ :param project_domain_name: the project domain name
+ :param domain_id: a domain id to scope to
+ :param domain_name: a domain name to scope to
+ :param project_id: a project id to scope to
+ :param project_name: a project name to scope to
+ :param token: a token to re-scope.
+
+ Accepts different combinations of credentials.
+ Sample sample valid combinations:
+ - token
+ - token, project_name, project_domain_id
+ - user_id, password
+ - username, password, user_domain_id
+ - username, password, project_name, user_domain_id, project_domain_id
+ Validation is left to the server side.
+ """
+ creds = {
+ 'auth': {
+ 'identity': {
+ 'methods': [],
+ }
+ }
+ }
+ id_obj = creds['auth']['identity']
+ if token:
+ id_obj['methods'].append('token')
+ id_obj['token'] = {
+ 'id': token
+ }
+
+ if (user_id or username) and password:
+ id_obj['methods'].append('password')
+ id_obj['password'] = {
+ 'user': {
+ 'password': password,
+ }
+ }
+ if user_id:
+ id_obj['password']['user']['id'] = user_id
+ else:
+ id_obj['password']['user']['name'] = username
+
+ _domain = None
+ if user_domain_id is not None:
+ _domain = dict(id=user_domain_id)
+ elif user_domain_name is not None:
+ _domain = dict(name=user_domain_name)
+ 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()
+
+ if project_id:
+ _project['id'] = project_id
+ elif project_name:
+ _project['name'] = project_name
+
+ if project_domain_id is not None:
+ _project['domain'] = {'id': project_domain_id}
+ elif project_domain_name is not None:
+ _project['domain'] = {'name': project_domain_name}
+
+ creds['auth']['scope'] = dict(project=_project)
+ elif domain_id:
+ creds['auth']['scope'] = dict(domain={'id': domain_id})
+ elif domain_name:
+ creds['auth']['scope'] = dict(domain={'name': domain_name})
+
+ body = json.dumps(creds, sort_keys=True)
+ resp, body = self.post(self.auth_url, body=body)
+ self.expected_success(201, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def request(self, method, url, extra_headers=False, headers=None,
+ body=None, chunked=False):
+ """A simple HTTP request interface.
+
+ Note: this overloads the `request` method from the parent class and
+ thus must implement the same method signature.
+ """
+ if headers is None:
+ # Always accept 'json', for xml token client too.
+ # Because XML response is not easily
+ # converted to the corresponding JSON one
+ headers = self.get_headers(accept_type="json")
+ elif extra_headers:
+ try:
+ headers.update(self.get_headers(accept_type="json"))
+ except (ValueError, TypeError):
+ headers = self.get_headers(accept_type="json")
+
+ resp, resp_body = self.raw_request(url, method,
+ headers=headers, body=body)
+ self._log_request(method, url, resp, req_headers=headers,
+ req_body='<omitted>', resp_body=resp_body)
+
+ if resp.status in [401, 403]:
+ resp_body = json.loads(resp_body)
+ raise exceptions.Unauthorized(resp_body['error']['message'])
+ elif resp.status not in [200, 201, 204]:
+ raise exceptions.IdentityError(
+ 'Unexpected status code {0}'.format(resp.status))
+
+ return resp, json.loads(resp_body)
+
+ def get_token(self, **kwargs):
+ """Returns (token id, token data) for supplied credentials"""
+
+ auth_data = kwargs.pop('auth_data', False)
+
+ if not (kwargs.get('user_domain_id') or
+ kwargs.get('user_domain_name')):
+ kwargs['user_domain_name'] = 'Default'
+
+ if not (kwargs.get('project_domain_id') or
+ kwargs.get('project_domain_name')):
+ kwargs['project_domain_name'] = 'Default'
+
+ body = self.auth(**kwargs)
+
+ token = body.response.get('x-subject-token')
+ if auth_data:
+ return token, body['token']
+ else:
+ return token
+
+
+class V3TokenClientJSON(V3TokenClient):
+ LOG = logging.getLogger(__name__)
+
+ def _warn(self):
+ self.LOG.warning("%s class was deprecated and renamed to %s",
+ self.__class__.__name__, 'V3TokenClient')
+
+ def __init__(self, *args, **kwargs):
+ self._warn()
+ super(V3TokenClientJSON, self).__init__(*args, **kwargs)
diff --git a/tempest/lib/services/identity/v3/trusts_client.py b/tempest/lib/services/identity/v3/trusts_client.py
new file mode 100644
index 0000000..d113905
--- /dev/null
+++ b/tempest/lib/services/identity/v3/trusts_client.py
@@ -0,0 +1,85 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# 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 TrustsClient(rest_client.RestClient):
+ api_version = "v3"
+
+ def create_trust(self, **kwargs):
+ """Creates a trust.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3-ext/index.html#create-trust
+ """
+ post_body = json.dumps({'trust': kwargs})
+ resp, body = self.post('OS-TRUST/trusts', post_body)
+ self.expected_success(201, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_trust(self, trust_id):
+ """Deletes a trust."""
+ resp, body = self.delete("OS-TRUST/trusts/%s" % trust_id)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_trusts(self, **params):
+ """Returns trusts
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3-ext/index.html#list-trusts
+ """
+ url = "OS-TRUST/trusts/"
+ 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 show_trust(self, trust_id):
+ """GET trust."""
+ resp, body = self.get("OS-TRUST/trusts/%s" % trust_id)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_trust_roles(self, trust_id):
+ """GET roles delegated by a trust."""
+ resp, body = self.get("OS-TRUST/trusts/%s/roles" % trust_id)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_trust_role(self, trust_id, role_id):
+ """GET role delegated by a trust."""
+ resp, body = self.get("OS-TRUST/trusts/%s/roles/%s"
+ % (trust_id, role_id))
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def check_trust_role(self, trust_id, role_id):
+ """HEAD Check if role is delegated by a trust."""
+ resp, body = self.head("OS-TRUST/trusts/%s/roles/%s"
+ % (trust_id, role_id))
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/identity/v3/users_client.py b/tempest/lib/services/identity/v3/users_client.py
new file mode 100644
index 0000000..e99a971
--- /dev/null
+++ b/tempest/lib/services/identity/v3/users_client.py
@@ -0,0 +1,120 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# 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 UsersClient(rest_client.RestClient):
+ api_version = "v3"
+
+ def create_user(self, **kwargs):
+ """Creates a user.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/#create-user
+ """
+ post_body = json.dumps({'user': kwargs})
+ resp, body = self.post('users', post_body)
+ self.expected_success(201, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_user(self, user_id, **kwargs):
+ """Updates a user.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/#update-user
+ """
+ if 'id' not in kwargs:
+ kwargs['id'] = user_id
+ post_body = json.dumps({'user': kwargs})
+ resp, body = self.patch('users/%s' % user_id, post_body)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_user_password(self, user_id, **kwargs):
+ """Update a user password
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/index.html#change-password-for-user
+ """
+ update_user = json.dumps({'user': kwargs})
+ resp, _ = self.post('users/%s/password' % user_id, update_user)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp)
+
+ def list_user_projects(self, user_id, **params):
+ """Lists the projects on which a user has roles assigned.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/#list-projects-for-user
+ """
+ url = 'users/%s/projects' % 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 list_users(self, **params):
+ """Get the list of users.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/#list-users
+ """
+ url = 'users'
+ 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 show_user(self, user_id):
+ """GET a user."""
+ resp, body = self.get("users/%s" % user_id)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_user(self, user_id):
+ """Deletes a User."""
+ resp, body = self.delete("users/%s" % user_id)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_user_groups(self, user_id, **params):
+ """Lists groups which a user belongs to.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/identity/v3/#list-groups-to-which-a-user-belongs
+ """
+ url = 'users/%s/groups' % 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)
diff --git a/tempest/lib/services/identity/v3/versions_client.py b/tempest/lib/services/identity/v3/versions_client.py
new file mode 100644
index 0000000..441ee0d
--- /dev/null
+++ b/tempest/lib/services/identity/v3/versions_client.py
@@ -0,0 +1,38 @@
+# Copyright 2017 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.
+
+import time
+
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+
+
+class VersionsClient(rest_client.RestClient):
+ api_version = "v3"
+
+ def list_versions(self):
+ """List API versions"""
+ version_url = self._get_base_version_url()
+
+ start = time.time()
+ resp, body = self.raw_request(version_url, 'GET')
+ end = time.time()
+ self._log_request('GET', version_url, resp, secs=(end - start),
+ resp_body=body)
+ self._error_checker(resp, body)
+
+ self.expected_success(300, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/image/__init__.py b/tempest/lib/services/image/__init__.py
new file mode 100644
index 0000000..4b01663
--- /dev/null
+++ b/tempest/lib/services/image/__init__.py
@@ -0,0 +1,18 @@
+# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P.
+#
+# 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.image import v1
+from tempest.lib.services.image import v2
+
+__all__ = ['v1', 'v2']
diff --git a/tempest/lib/services/image/v1/__init__.py b/tempest/lib/services/image/v1/__init__.py
new file mode 100644
index 0000000..9bd8262
--- /dev/null
+++ b/tempest/lib/services/image/v1/__init__.py
@@ -0,0 +1,19 @@
+# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P.
+#
+# 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.image.v1.image_members_client import \
+ ImageMembersClient
+from tempest.lib.services.image.v1.images_client import ImagesClient
+
+__all__ = ['ImageMembersClient', 'ImagesClient']
diff --git a/tempest/lib/services/image/v1/image_members_client.py b/tempest/lib/services/image/v1/image_members_client.py
new file mode 100644
index 0000000..2318087
--- /dev/null
+++ b/tempest/lib/services/image/v1/image_members_client.py
@@ -0,0 +1,66 @@
+# 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 ImageMembersClient(rest_client.RestClient):
+ api_version = "v1"
+
+ def list_image_members(self, image_id):
+ """List all members of an image."""
+ url = 'images/%s/members' % image_id
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_shared_images(self, tenant_id):
+ """List image memberships for the given tenant.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v1/#list-shared-images
+ """
+
+ url = 'shared-images/%s' % tenant_id
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def create_image_member(self, image_id, member_id, **kwargs):
+ """Add a member to an image.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v1/#add-member-to-image
+ """
+ url = 'images/%s/members/%s' % (image_id, member_id)
+ body = json.dumps({'member': kwargs})
+ resp, __ = self.put(url, body)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp)
+
+ def delete_image_member(self, image_id, member_id):
+ """Removes a membership from the image.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v1/#remove-member
+ """
+ url = 'images/%s/members/%s' % (image_id, member_id)
+ resp, __ = self.delete(url)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp)
diff --git a/tempest/lib/services/image/v1/images_client.py b/tempest/lib/services/image/v1/images_client.py
new file mode 100644
index 0000000..42f8cf2
--- /dev/null
+++ b/tempest/lib/services/image/v1/images_client.py
@@ -0,0 +1,155 @@
+# Copyright 2013 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.
+
+import functools
+
+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
+
+CHUNKSIZE = 1024 * 64 # 64kB
+
+
+class ImagesClient(rest_client.RestClient):
+ api_version = "v1"
+
+ def _create_with_data(self, headers, data):
+ # We are going to do chunked transfert, so split the input data
+ # info fixed-sized chunks.
+ headers['Content-Type'] = 'application/octet-stream'
+ data = iter(functools.partial(data.read, CHUNKSIZE), b'')
+ resp, body = self.request('POST', 'images',
+ headers=headers, body=data, chunked=True)
+ self._error_checker(resp, body)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def _update_with_data(self, image_id, headers, data):
+ # We are going to do chunked transfert, so split the input data
+ # info fixed-sized chunks.
+ headers['Content-Type'] = 'application/octet-stream'
+ data = iter(functools.partial(data.read, CHUNKSIZE), b'')
+ url = 'images/%s' % image_id
+ resp, body = self.request('PUT', url, headers=headers,
+ body=data, chunked=True)
+ self._error_checker(resp, body)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ @property
+ def http(self):
+ if self._http is None:
+ self._http = self._get_http()
+ return self._http
+
+ def create_image(self, data=None, headers=None):
+ """Create an image.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/image/v1/index.html#create-image
+ """
+ if headers is None:
+ headers = {}
+
+ if data is not None:
+ return self._create_with_data(headers, data)
+
+ resp, body = self.post('images', None, headers)
+ self.expected_success(201, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_image(self, image_id, data=None, headers=None):
+ """Update an image.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/image/v1/index.html#update-image
+ """
+ if headers is None:
+ headers = {}
+
+ if data is not None:
+ return self._update_with_data(image_id, headers, data)
+
+ url = 'images/%s' % image_id
+ resp, body = self.put(url, None, headers)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_image(self, image_id):
+ url = 'images/%s' % image_id
+ resp, body = self.delete(url)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_images(self, detail=False, **kwargs):
+ """Return a list of all images filtered by input parameters.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v1/#list-images
+
+ Most parameters except the following are passed to the API without
+ any changes.
+ :param changes_since: The name is changed to changes-since
+ """
+ url = 'images'
+
+ if detail:
+ url += '/detail'
+
+ if 'changes_since' in kwargs:
+ kwargs['changes-since'] = kwargs.pop('changes_since')
+
+ if kwargs:
+ url += '?%s' % urllib.urlencode(kwargs)
+
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def check_image(self, image_id):
+ """Check image metadata."""
+ url = 'images/%s' % image_id
+ resp, body = self.head(url)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_image(self, image_id):
+ """Get image details plus the image itself."""
+ url = 'images/%s' % image_id
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBodyData(resp, body)
+
+ def is_resource_deleted(self, id):
+ try:
+ resp = self.check_image(id)
+ if resp.response["x-image-meta-status"] == 'deleted':
+ 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 'image_meta'
diff --git a/tempest/lib/services/image/v2/__init__.py b/tempest/lib/services/image/v2/__init__.py
new file mode 100644
index 0000000..99a5321
--- /dev/null
+++ b/tempest/lib/services/image/v2/__init__.py
@@ -0,0 +1,33 @@
+# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P.
+#
+# 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.image.v2.image_members_client import \
+ ImageMembersClient
+from tempest.lib.services.image.v2.images_client import ImagesClient
+from tempest.lib.services.image.v2.namespace_objects_client import \
+ NamespaceObjectsClient
+from tempest.lib.services.image.v2.namespace_properties_client import \
+ NamespacePropertiesClient
+from tempest.lib.services.image.v2.namespace_tags_client import \
+ NamespaceTagsClient
+from tempest.lib.services.image.v2.namespaces_client import NamespacesClient
+from tempest.lib.services.image.v2.resource_types_client import \
+ ResourceTypesClient
+from tempest.lib.services.image.v2.schemas_client import SchemasClient
+from tempest.lib.services.image.v2.versions_client import VersionsClient
+
+__all__ = ['ImageMembersClient', 'ImagesClient', 'NamespaceObjectsClient',
+ 'NamespacePropertiesClient', 'NamespaceTagsClient',
+ 'NamespacesClient', 'ResourceTypesClient', 'SchemasClient',
+ 'VersionsClient']
diff --git a/tempest/lib/services/image/v2/image_members_client.py b/tempest/lib/services/image/v2/image_members_client.py
new file mode 100644
index 0000000..e5118a8
--- /dev/null
+++ b/tempest/lib/services/image/v2/image_members_client.py
@@ -0,0 +1,84 @@
+# 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 ImageMembersClient(rest_client.RestClient):
+ api_version = "v2"
+
+ def list_image_members(self, image_id):
+ """List image members.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/#list-image-members
+ """
+ url = 'images/%s/members' % image_id
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def create_image_member(self, image_id, **kwargs):
+ """Create an image member.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/#create-image-member
+ """
+ url = 'images/%s/members' % image_id
+ data = json.dumps(kwargs)
+ resp, body = self.post(url, data)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_image_member(self, image_id, member_id, **kwargs):
+ """Update an image member.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/#update-image-member
+ """
+ url = 'images/%s/members/%s' % (image_id, member_id)
+ data = json.dumps(kwargs)
+ resp, body = self.put(url, data)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_image_member(self, image_id, member_id):
+ """Show an image member.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/#show-image-member-details
+ """
+ url = 'images/%s/members/%s' % (image_id, member_id)
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, json.loads(body))
+
+ def delete_image_member(self, image_id, member_id):
+ """Delete an image member.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/#delete-image-member
+ """
+ url = 'images/%s/members/%s' % (image_id, member_id)
+ resp, _ = self.delete(url)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp)
diff --git a/tempest/lib/services/image/v2/images_client.py b/tempest/lib/services/image/v2/images_client.py
new file mode 100644
index 0000000..ed6df47
--- /dev/null
+++ b/tempest/lib/services/image/v2/images_client.py
@@ -0,0 +1,189 @@
+# Copyright 2013 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.
+
+import functools
+
+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
+
+CHUNKSIZE = 1024 * 64 # 64kB
+
+
+class ImagesClient(rest_client.RestClient):
+ api_version = "v2"
+
+ def update_image(self, image_id, patch):
+ """Update an image.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/index.html#update-an-image
+ """
+ data = json.dumps(patch)
+ headers = {"Content-Type": "application/openstack-images-v2.0"
+ "-json-patch"}
+ resp, body = self.patch('images/%s' % image_id, data, headers)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def create_image(self, **kwargs):
+ """Create an image.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/index.html#create-an-image
+ """
+ data = json.dumps(kwargs)
+ resp, body = self.post('images', data)
+ self.expected_success(201, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def deactivate_image(self, image_id):
+ """Deactivate image.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/#deactivate-image
+ """
+ url = 'images/%s/actions/deactivate' % image_id
+ resp, body = self.post(url, None)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def reactivate_image(self, image_id):
+ """Reactivate image.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/#reactivate-image
+ """
+ url = 'images/%s/actions/reactivate' % image_id
+ resp, body = self.post(url, None)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_image(self, image_id):
+ """Delete image.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/#delete-an-image
+ """
+ url = 'images/%s' % image_id
+ resp, _ = self.delete(url)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp)
+
+ def list_images(self, params=None):
+ """List images.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/#show-images
+ """
+ url = 'images'
+
+ 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 show_image(self, image_id):
+ """Show image details.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/#show-image-details
+ """
+ url = 'images/%s' % image_id
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def is_resource_deleted(self, id):
+ try:
+ self.show_image(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 'image'
+
+ def store_image_file(self, image_id, data):
+ """Upload binary image data.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/#upload-binary-image-data
+ """
+ url = 'images/%s/file' % image_id
+
+ # We are going to do chunked transfert, so split the input data
+ # info fixed-sized chunks.
+ headers = {'Content-Type': 'application/octet-stream'}
+ data = iter(functools.partial(data.read, CHUNKSIZE), b'')
+
+ resp, body = self.request('PUT', url, headers=headers,
+ body=data, chunked=True)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_image_file(self, image_id):
+ """Download binary image data.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/#download-binary-image-data
+ """
+ url = 'images/%s/file' % image_id
+ resp, body = self.get(url)
+ self.expected_success([200, 204, 206], resp.status)
+ return rest_client.ResponseBodyData(resp, body)
+
+ def add_image_tag(self, image_id, tag):
+ """Add an image tag.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/#add-image-tag
+ """
+ url = 'images/%s/tags/%s' % (image_id, tag)
+ resp, body = self.put(url, body=None)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_image_tag(self, image_id, tag):
+ """Delete an image tag.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/#delete-image-tag
+ """
+ url = 'images/%s/tags/%s' % (image_id, tag)
+ resp, _ = self.delete(url)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp)
diff --git a/tempest/lib/services/image/v2/namespace_objects_client.py b/tempest/lib/services/image/v2/namespace_objects_client.py
new file mode 100644
index 0000000..ac2e63e
--- /dev/null
+++ b/tempest/lib/services/image/v2/namespace_objects_client.py
@@ -0,0 +1,91 @@
+# Copyright 2016 EasyStack.
+# 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 NamespaceObjectsClient(rest_client.RestClient):
+ api_version = "v2"
+
+ def list_namespace_objects(self, namespace, **kwargs):
+ """Lists all namespace objects.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/metadefs-index.html#list-objects
+ """
+ url = 'metadefs/namespaces/%s/objects' % namespace
+ if kwargs:
+ url += '?%s' % urllib.urlencode(kwargs)
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def create_namespace_object(self, namespace, **kwargs):
+ """Create a namespace object
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/metadefs-index.html#create-object
+ """
+ url = 'metadefs/namespaces/%s/objects' % namespace
+ data = json.dumps(kwargs)
+ resp, body = self.post(url, data)
+ self.expected_success(201, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_namespace_object(self, namespace, object_name, **kwargs):
+ """Update a namespace object
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/metadefs-index.html#update-object
+ """
+ url = 'metadefs/namespaces/%s/objects/%s' % (namespace, object_name)
+ data = json.dumps(kwargs)
+ resp, body = self.put(url, data)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_namespace_object(self, namespace, object_name):
+ """Show a namespace object
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/metadefs-index.html#show-object
+ """
+ url = 'metadefs/namespaces/%s/objects/%s' % (namespace, object_name)
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_namespace_object(self, namespace, object_name):
+ """Delete a namespace object
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/metadefs-index.html#delete-object
+ """
+ url = 'metadefs/namespaces/%s/objects/%s' % (namespace, object_name)
+ resp, _ = self.delete(url)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp)
diff --git a/tempest/lib/services/image/v2/namespace_properties_client.py b/tempest/lib/services/image/v2/namespace_properties_client.py
new file mode 100644
index 0000000..1236b2b
--- /dev/null
+++ b/tempest/lib/services/image/v2/namespace_properties_client.py
@@ -0,0 +1,91 @@
+# Copyright 2016 EasyStack.
+# 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 NamespacePropertiesClient(rest_client.RestClient):
+ api_version = "v2"
+
+ def list_namespace_properties(self, namespace):
+ """Lists property definitions in a namespace.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/metadefs-index.html#list-properties
+ """
+ url = 'metadefs/namespaces/%s/properties' % namespace
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def create_namespace_property(self, namespace, **kwargs):
+ """Creates a property definition in a namespace.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/metadefs-index.html#create-property
+ """
+ url = 'metadefs/namespaces/%s/properties' % namespace
+ data = json.dumps(kwargs)
+ resp, body = self.post(url, data)
+ self.expected_success(201, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_namespace_properties(self, namespace, property_name):
+ """Shows the definition for a property.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/metadefs-index.html#show-property-definition
+ """
+ url = 'metadefs/namespaces/%s/properties/%s' % (namespace,
+ property_name)
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_namespace_properties(self, namespace, property_name, **kwargs):
+ """Updates a property definition.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/metadefs-index.html#update-property-definition
+ """
+ url = 'metadefs/namespaces/%s/properties/%s' % (namespace,
+ property_name)
+ data = json.dumps(kwargs)
+ resp, body = self.put(url, data)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_namespace_property(self, namespace, property_name):
+ """Removes a property definition from a namespace.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/metadefs-index.html#remove-property-definition
+ """
+ url = 'metadefs/namespaces/%s/properties/%s' % (namespace,
+ property_name)
+ resp, _ = self.delete(url)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp)
diff --git a/tempest/lib/services/image/v2/namespace_tags_client.py b/tempest/lib/services/image/v2/namespace_tags_client.py
new file mode 100644
index 0000000..61cc33d
--- /dev/null
+++ b/tempest/lib/services/image/v2/namespace_tags_client.py
@@ -0,0 +1,125 @@
+# Copyright 2016 EasyStack.
+# 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 NamespaceTagsClient(rest_client.RestClient):
+ api_version = "v2"
+
+ def create_namespace_tag(self, namespace, tag_name):
+ """Adds a tag to the list of namespace tag definitions.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/metadefs-index.html#create-tag-definition
+ """
+ url = 'metadefs/namespaces/%s/tags/%s' % (namespace,
+ tag_name)
+ resp, body = self.post(url, None)
+ self.expected_success(201, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_namespace_tag(self, namespace, tag_name):
+ """Gets a definition for a tag.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/image/v2/metadefs-index.html#get-tag-definition
+ """
+ url = 'metadefs/namespaces/%s/tags/%s' % (namespace,
+ tag_name)
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_namespace_tag(self, namespace, tag_name, **kwargs):
+ """Renames a tag definition.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/metadefs-index.html#update-tag-definition
+ """
+ url = 'metadefs/namespaces/%s/tags/%s' % (namespace,
+ tag_name)
+ data = json.dumps(kwargs)
+ resp, body = self.put(url, data)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_namespace_tag(self, namespace, tag_name):
+ """Deletes a tag definition within a namespace.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/metadefs-index.html#delete-tag-definition
+ """
+ url = 'metadefs/namespaces/%s/tags/%s' % (namespace, tag_name)
+ resp, _ = self.delete(url)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp)
+
+ def create_namespace_tags(self, namespace, **kwargs):
+ """Creates one or more tag definitions in a namespace.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/metadefs-index.html#create-tags
+ """
+ url = 'metadefs/namespaces/%s/tags' % namespace
+ data = json.dumps(kwargs)
+ resp, body = self.post(url, data)
+ self.expected_success(201, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_namespace_tags(self, namespace, **params):
+ """Lists the tag definitions within a namespace.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/metadefs-index.html#list-tags
+ """
+ url = 'metadefs/namespaces/%s/tags' % namespace
+ 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_namespace_tags(self, namespace):
+ """Deletes all tag definitions within a namespace.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/metadefs-index.html#delete-all-tag-definitions
+ """
+ url = 'metadefs/namespaces/%s/tags' % namespace
+ resp, _ = self.delete(url)
+
+ # NOTE(rosmaita): Bug 1656183 fixed the success response code for
+ # this call to make it consistent with the other metadefs delete
+ # calls. Accept both codes in case tempest is being run against
+ # an old Glance.
+ self.expected_success([200, 204], resp.status)
+
+ return rest_client.ResponseBody(resp)
diff --git a/tempest/lib/services/image/v2/namespaces_client.py b/tempest/lib/services/image/v2/namespaces_client.py
new file mode 100644
index 0000000..b00de89
--- /dev/null
+++ b/tempest/lib/services/image/v2/namespaces_client.py
@@ -0,0 +1,91 @@
+# Copyright 2013 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 NamespacesClient(rest_client.RestClient):
+ api_version = "v2"
+
+ def create_namespace(self, **kwargs):
+ """Create a namespace.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/metadefs-index.html#create-namespace
+ """
+ data = json.dumps(kwargs)
+ resp, body = self.post('metadefs/namespaces', data)
+ self.expected_success(201, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_namespaces(self):
+ """List namespaces
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/metadefs-index.html#list-namespaces
+ """
+ url = 'metadefs/namespaces'
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_namespace(self, namespace):
+ """Show namespace details.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/metadefs-index.html#get-namespace-details
+ """
+ url = 'metadefs/namespaces/%s' % namespace
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_namespace(self, namespace, **kwargs):
+ """Update a namespace.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/metadefs-index.html#update-namespace
+ """
+ # NOTE: On Glance API, we need to pass namespace on both URI
+ # and a request body.
+ params = {'namespace': namespace}
+ params.update(kwargs)
+ data = json.dumps(params)
+ url = 'metadefs/namespaces/%s' % namespace
+ resp, body = self.put(url, body=data)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_namespace(self, namespace):
+ """Delete a namespace.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/metadefs-index.html#delete-namespace
+ """
+ url = 'metadefs/namespaces/%s' % namespace
+ resp, _ = self.delete(url)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp)
diff --git a/tempest/lib/services/image/v2/resource_types_client.py b/tempest/lib/services/image/v2/resource_types_client.py
new file mode 100644
index 0000000..13259d1
--- /dev/null
+++ b/tempest/lib/services/image/v2/resource_types_client.py
@@ -0,0 +1,75 @@
+# Copyright 2013 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 ResourceTypesClient(rest_client.RestClient):
+ api_version = "v2"
+
+ def list_resource_types(self):
+ """Lists all resource types.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/metadefs-index.html#list-resource-types
+ """
+ url = 'metadefs/resource_types'
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def create_resource_type_association(self, namespace_id, **kwargs):
+ """Creates a resource type association in given namespace.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/metadefs-index.html#create-resource-type-association
+ """
+ url = 'metadefs/namespaces/%s/resource_types' % namespace_id
+ data = json.dumps(kwargs)
+ resp, body = self.post(url, data)
+ self.expected_success(201, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_resource_type_association(self, namespace_id):
+ """Lists resource type associations in given namespace.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/metadefs-index.html#list-resource-type-associations
+ """
+ url = 'metadefs/namespaces/%s/resource_types' % namespace_id
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_resource_type_association(self, namespace_id, resource_name):
+ """Removes resource type association in given namespace.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/image/v2/metadefs-index.html#remove-resource-type-association
+ """
+ url = 'metadefs/namespaces/%s/resource_types/%s' % (namespace_id,
+ resource_name)
+ resp, _ = self.delete(url)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp)
diff --git a/tempest/lib/services/image/v2/schemas_client.py b/tempest/lib/services/image/v2/schemas_client.py
new file mode 100644
index 0000000..0c9db40
--- /dev/null
+++ b/tempest/lib/services/image/v2/schemas_client.py
@@ -0,0 +1,29 @@
+# Copyright 2013 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 SchemasClient(rest_client.RestClient):
+ api_version = "v2"
+
+ def show_schema(self, schema):
+ url = 'schemas/%s' % schema
+ 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/image/v2/versions_client.py b/tempest/lib/services/image/v2/versions_client.py
new file mode 100644
index 0000000..1adc466
--- /dev/null
+++ b/tempest/lib/services/image/v2/versions_client.py
@@ -0,0 +1,38 @@
+# Copyright 2017 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.
+
+import time
+
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+
+
+class VersionsClient(rest_client.RestClient):
+ api_version = "v2"
+
+ def list_versions(self):
+ """List API versions"""
+ version_url = self._get_base_version_url()
+
+ start = time.time()
+ resp, body = self.raw_request(version_url, 'GET')
+ end = time.time()
+ self._log_request('GET', version_url, resp, secs=(end - start),
+ resp_body=body)
+ self._error_checker(resp, body)
+
+ self.expected_success(300, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/network/__init__.py b/tempest/lib/services/network/__init__.py
new file mode 100644
index 0000000..419e593
--- /dev/null
+++ b/tempest/lib/services/network/__init__.py
@@ -0,0 +1,42 @@
+# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P.
+#
+# 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.network.agents_client import AgentsClient
+from tempest.lib.services.network.extensions_client import ExtensionsClient
+from tempest.lib.services.network.floating_ips_client import FloatingIPsClient
+from tempest.lib.services.network.metering_label_rules_client import \
+ MeteringLabelRulesClient
+from tempest.lib.services.network.metering_labels_client import \
+ MeteringLabelsClient
+from tempest.lib.services.network.networks_client import NetworksClient
+from tempest.lib.services.network.ports_client import PortsClient
+from tempest.lib.services.network.quotas_client import QuotasClient
+from tempest.lib.services.network.routers_client import RoutersClient
+from tempest.lib.services.network.security_group_rules_client import \
+ SecurityGroupRulesClient
+from tempest.lib.services.network.security_groups_client import \
+ SecurityGroupsClient
+from tempest.lib.services.network.service_providers_client import \
+ ServiceProvidersClient
+from tempest.lib.services.network.subnetpools_client import SubnetpoolsClient
+from tempest.lib.services.network.subnets_client import SubnetsClient
+from tempest.lib.services.network.tags_client import TagsClient
+from tempest.lib.services.network.versions_client import NetworkVersionsClient
+
+__all__ = ['AgentsClient', 'ExtensionsClient', 'FloatingIPsClient',
+ 'MeteringLabelRulesClient', 'MeteringLabelsClient',
+ 'NetworksClient', 'NetworkVersionsClient', 'PortsClient',
+ 'QuotasClient', 'RoutersClient', 'SecurityGroupRulesClient',
+ 'SecurityGroupsClient', 'ServiceProvidersClient',
+ 'SubnetpoolsClient', 'SubnetsClient', 'TagsClient']
diff --git a/tempest/lib/services/network/agents_client.py b/tempest/lib/services/network/agents_client.py
new file mode 100644
index 0000000..a0f832e
--- /dev/null
+++ b/tempest/lib/services/network/agents_client.py
@@ -0,0 +1,95 @@
+# Copyright 2015 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.services.network import base
+
+
+class AgentsClient(base.BaseNetworkClient):
+
+ def update_agent(self, agent_id, **kwargs):
+ """Update an agent.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/network/v2/#update-agent
+ """
+ uri = '/agents/%s' % agent_id
+ return self.update_resource(uri, kwargs)
+
+ def show_agent(self, agent_id, **fields):
+ """Show details for an agent.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/network/v2/#show-agent-details
+ """
+ uri = '/agents/%s' % agent_id
+ return self.show_resource(uri, **fields)
+
+ def list_agents(self, **filters):
+ """List all agents.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/network/v2/#list-all-agents
+ """
+ uri = '/agents'
+ return self.list_resources(uri, **filters)
+
+ def list_routers_on_l3_agent(self, agent_id):
+ """List routers that an l3 agent hosts.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/network/v2/#list-routers-hosted-by-an-l3-agent
+ """
+ uri = '/agents/%s/l3-routers' % agent_id
+ return self.list_resources(uri)
+
+ def create_router_on_l3_agent(self, agent_id, **kwargs):
+ """Add a router to an l3 agent.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/network/v2/#schedule-router-to-an-l3-agent
+ """
+ uri = '/agents/%s/l3-routers' % agent_id
+ return self.create_resource(uri, kwargs, expect_empty_body=True)
+
+ def delete_router_from_l3_agent(self, agent_id, router_id):
+ """Remove a router to an l3 agent.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/network/v2/#remove-l3-router-from-an-l3-agent
+ """
+ uri = '/agents/%s/l3-routers/%s' % (agent_id, router_id)
+ return self.delete_resource(uri)
+
+ def list_networks_hosted_by_one_dhcp_agent(self, agent_id):
+ uri = '/agents/%s/dhcp-networks' % agent_id
+ return self.list_resources(uri)
+
+ def delete_network_from_dhcp_agent(self, agent_id, network_id):
+ uri = '/agents/%s/dhcp-networks/%s' % (agent_id,
+ network_id)
+ return self.delete_resource(uri)
+
+ def add_dhcp_agent_to_network(self, agent_id, **kwargs):
+ # 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/1526212
+ uri = '/agents/%s/dhcp-networks' % agent_id
+ return self.create_resource(uri, kwargs, expect_empty_body=True)
diff --git a/tempest/lib/services/network/base.py b/tempest/lib/services/network/base.py
new file mode 100644
index 0000000..fe8b244
--- /dev/null
+++ b/tempest/lib/services/network/base.py
@@ -0,0 +1,85 @@
+# 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 BaseNetworkClient(rest_client.RestClient):
+
+ """Base class for Tempest REST clients for Neutron.
+
+ Child classes use v2 of the Neutron API, since the V1 API has been
+ removed from the code base.
+ """
+
+ version = '2.0'
+ uri_prefix = "v2.0"
+
+ def list_resources(self, uri, **filters):
+ req_uri = self.uri_prefix + uri
+ if filters:
+ req_uri += '?' + urllib.urlencode(filters, doseq=1)
+ resp, body = self.get(req_uri)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_resource(self, uri):
+ req_uri = self.uri_prefix + uri
+ resp, body = self.delete(req_uri)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_resource(self, uri, **fields):
+ # fields is a dict which key is 'fields' and value is a
+ # list of field's name. An example:
+ # {'fields': ['id', 'name']}
+ req_uri = self.uri_prefix + uri
+ if fields:
+ req_uri += '?' + urllib.urlencode(fields, doseq=1)
+ resp, body = self.get(req_uri)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def create_resource(self, uri, post_data, expect_empty_body=False,
+ expect_response_code=201):
+ req_uri = self.uri_prefix + uri
+ req_post_data = json.dumps(post_data)
+ resp, body = self.post(req_uri, req_post_data)
+ # NOTE: RFC allows both a valid non-empty body and an empty body for
+ # response of POST API. If a body is expected not empty, we decode the
+ # body. Otherwise we returns the body as it is.
+ if not expect_empty_body:
+ body = json.loads(body)
+ else:
+ body = None
+ self.expected_success(expect_response_code, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_resource(self, uri, post_data, expect_empty_body=False,
+ expect_response_code=200):
+ req_uri = self.uri_prefix + uri
+ req_post_data = json.dumps(post_data)
+ resp, body = self.put(req_uri, req_post_data)
+ # NOTE: RFC allows both a valid non-empty body and an empty body for
+ # response of PUT API. If a body is expected not empty, we decode the
+ # body. Otherwise we returns the body as it is.
+ if not expect_empty_body:
+ body = json.loads(body)
+ else:
+ body = None
+ self.expected_success(expect_response_code, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/network/extensions_client.py b/tempest/lib/services/network/extensions_client.py
new file mode 100644
index 0000000..3910c84
--- /dev/null
+++ b/tempest/lib/services/network/extensions_client.py
@@ -0,0 +1,24 @@
+# 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.network import base
+
+
+class ExtensionsClient(base.BaseNetworkClient):
+
+ def show_extension(self, ext_alias, **fields):
+ uri = '/extensions/%s' % ext_alias
+ return self.show_resource(uri, **fields)
+
+ def list_extensions(self, **filters):
+ uri = '/extensions'
+ return self.list_resources(uri, **filters)
diff --git a/tempest/lib/services/network/floating_ips_client.py b/tempest/lib/services/network/floating_ips_client.py
new file mode 100644
index 0000000..2bb18e0
--- /dev/null
+++ b/tempest/lib/services/network/floating_ips_client.py
@@ -0,0 +1,65 @@
+# 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.network import base
+
+
+class FloatingIPsClient(base.BaseNetworkClient):
+
+ def create_floatingip(self, **kwargs):
+ """Creates a floating IP.
+
+ If you specify port information, associates the floating IP with an
+ internal port.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#create-floating-ip
+ """
+ uri = '/floatingips'
+ post_data = {'floatingip': kwargs}
+ return self.create_resource(uri, post_data)
+
+ def update_floatingip(self, floatingip_id, **kwargs):
+ """Updates a floating IP and its association with an internal port.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#update-floating-ip
+ """
+ uri = '/floatingips/%s' % floatingip_id
+ post_data = {'floatingip': kwargs}
+ return self.update_resource(uri, post_data)
+
+ def show_floatingip(self, floatingip_id, **fields):
+ """Shows details for a floating IP.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#show-floating-ip-details
+ """
+ uri = '/floatingips/%s' % floatingip_id
+ return self.show_resource(uri, **fields)
+
+ def delete_floatingip(self, floatingip_id):
+ uri = '/floatingips/%s' % floatingip_id
+ return self.delete_resource(uri)
+
+ def list_floatingips(self, **filters):
+ """Lists floating IPs.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#list-floating-ips
+ """
+ uri = '/floatingips'
+ return self.list_resources(uri, **filters)
diff --git a/tempest/lib/services/network/metering_label_rules_client.py b/tempest/lib/services/network/metering_label_rules_client.py
new file mode 100644
index 0000000..9542e8f
--- /dev/null
+++ b/tempest/lib/services/network/metering_label_rules_client.py
@@ -0,0 +1,45 @@
+# 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.network import base
+
+
+class MeteringLabelRulesClient(base.BaseNetworkClient):
+
+ def create_metering_label_rule(self, **kwargs):
+ """Create metering label rule.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/network/v2/index.html#create-metering-label-rule
+ """
+ uri = '/metering/metering-label-rules'
+ post_data = {'metering_label_rule': kwargs}
+ return self.create_resource(uri, post_data)
+
+ def show_metering_label_rule(self, metering_label_rule_id, **fields):
+ uri = '/metering/metering-label-rules/%s' % metering_label_rule_id
+ return self.show_resource(uri, **fields)
+
+ def delete_metering_label_rule(self, metering_label_rule_id):
+ uri = '/metering/metering-label-rules/%s' % metering_label_rule_id
+ return self.delete_resource(uri)
+
+ def list_metering_label_rules(self, **filters):
+ """List metering label rules.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/network/v2/index.html#list-metering-label-rules
+ """
+ uri = '/metering/metering-label-rules'
+ return self.list_resources(uri, **filters)
diff --git a/tempest/lib/services/network/metering_labels_client.py b/tempest/lib/services/network/metering_labels_client.py
new file mode 100644
index 0000000..411da1f
--- /dev/null
+++ b/tempest/lib/services/network/metering_labels_client.py
@@ -0,0 +1,57 @@
+# 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.network import base
+
+
+class MeteringLabelsClient(base.BaseNetworkClient):
+
+ def create_metering_label(self, **kwargs):
+ """Creates an L3 metering label.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#create-metering-label
+ """
+ uri = '/metering/metering-labels'
+ post_data = {'metering_label': kwargs}
+ return self.create_resource(uri, post_data)
+
+ def show_metering_label(self, metering_label_id, **fields):
+ """Shows details for a metering label.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#show-metering-label-details
+ """
+ uri = '/metering/metering-labels/%s' % metering_label_id
+ return self.show_resource(uri, **fields)
+
+ def delete_metering_label(self, metering_label_id):
+ """Deletes an L3 metering label.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#delete-metering-label
+ """
+ uri = '/metering/metering-labels/%s' % metering_label_id
+ return self.delete_resource(uri)
+
+ def list_metering_labels(self, **filters):
+ """Lists all L3 metering labels that belong to the tenant.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#list-metering-labels
+ """
+ uri = '/metering/metering-labels'
+ return self.list_resources(uri, **filters)
diff --git a/tempest/lib/services/network/networks_client.py b/tempest/lib/services/network/networks_client.py
new file mode 100644
index 0000000..77d4823
--- /dev/null
+++ b/tempest/lib/services/network/networks_client.py
@@ -0,0 +1,76 @@
+# 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.network import base
+
+
+class NetworksClient(base.BaseNetworkClient):
+
+ def create_network(self, **kwargs):
+ """Creates a network.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#create-network
+ """
+ uri = '/networks'
+ post_data = {'network': kwargs}
+ return self.create_resource(uri, post_data)
+
+ def update_network(self, network_id, **kwargs):
+ """Updates a network.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#update-network
+ """
+ uri = '/networks/%s' % network_id
+ post_data = {'network': kwargs}
+ return self.update_resource(uri, post_data)
+
+ def show_network(self, network_id, **fields):
+ """Shows details for a network.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#show-network-details
+ """
+ uri = '/networks/%s' % network_id
+ return self.show_resource(uri, **fields)
+
+ def delete_network(self, network_id):
+ uri = '/networks/%s' % network_id
+ return self.delete_resource(uri)
+
+ def list_networks(self, **filters):
+ """Lists networks to which the tenant has access.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#list-networks
+ """
+ uri = '/networks'
+ return self.list_resources(uri, **filters)
+
+ def create_bulk_networks(self, **kwargs):
+ """Create multiple networks in a single request.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#bulk-create-networks
+ """
+ uri = '/networks'
+ return self.create_resource(uri, kwargs)
+
+ def list_dhcp_agents_on_hosting_network(self, network_id):
+ uri = '/networks/%s/dhcp-agents' % network_id
+ return self.list_resources(uri)
diff --git a/tempest/lib/services/network/ports_client.py b/tempest/lib/services/network/ports_client.py
new file mode 100644
index 0000000..daa15d7
--- /dev/null
+++ b/tempest/lib/services/network/ports_client.py
@@ -0,0 +1,86 @@
+# 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 import exceptions as lib_exc
+from tempest.lib.services.network import base
+
+
+class PortsClient(base.BaseNetworkClient):
+
+ def create_port(self, **kwargs):
+ """Creates a port on a network.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#create-port
+ """
+ uri = '/ports'
+ post_data = {'port': kwargs}
+ return self.create_resource(uri, post_data)
+
+ def update_port(self, port_id, **kwargs):
+ """Updates a port.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#update-port
+ """
+ uri = '/ports/%s' % port_id
+ post_data = {'port': kwargs}
+ return self.update_resource(uri, post_data)
+
+ def show_port(self, port_id, **fields):
+ """Shows details for a port.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#show-port-details
+ """
+ uri = '/ports/%s' % port_id
+ return self.show_resource(uri, **fields)
+
+ def delete_port(self, port_id):
+ """Deletes a port.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#delete-port
+ """
+ uri = '/ports/%s' % port_id
+ return self.delete_resource(uri)
+
+ def list_ports(self, **filters):
+ """Lists ports to which the tenant has access.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#list-ports
+ """
+ uri = '/ports'
+ return self.list_resources(uri, **filters)
+
+ def create_bulk_ports(self, **kwargs):
+ """Create multiple ports in a single request.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#bulk-create-ports
+ """
+ uri = '/ports'
+ return self.create_resource(uri, kwargs)
+
+ def is_resource_deleted(self, id):
+ try:
+ self.show_port(id)
+ except lib_exc.NotFound:
+ return True
+ return False
diff --git a/tempest/lib/services/network/quotas_client.py b/tempest/lib/services/network/quotas_client.py
new file mode 100644
index 0000000..e9666de
--- /dev/null
+++ b/tempest/lib/services/network/quotas_client.py
@@ -0,0 +1,53 @@
+# Copyright 2015 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.services.network import base
+
+
+class QuotasClient(base.BaseNetworkClient):
+
+ def update_quotas(self, tenant_id, **kwargs):
+ """Update quota for a project.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/network/v2/index.html#update-quota-for-a-project
+ """
+ put_body = {'quota': kwargs}
+ uri = '/quotas/%s' % tenant_id
+ return self.update_resource(uri, put_body)
+
+ def reset_quotas(self, tenant_id): # noqa
+ # NOTE: This noqa is for passing T111 check and we cannot rename
+ # to keep backwards compatibility.
+ uri = '/quotas/%s' % tenant_id
+ return self.delete_resource(uri)
+
+ def show_quotas(self, tenant_id, **fields):
+ uri = '/quotas/%s' % tenant_id
+ return self.show_resource(uri, **fields)
+
+ def list_quotas(self, **filters):
+ uri = '/quotas'
+ return self.list_resources(uri, **filters)
+
+ def show_default_quotas(self, tenant_id):
+ """List default quotas for a project."""
+ uri = '/quotas/%s/default' % tenant_id
+ return self.show_resource(uri)
+
+ def show_quota_details(self, tenant_id):
+ """Show quota details for a project."""
+ uri = '/quotas/%s/details.json' % tenant_id
+ return self.show_resource(uri)
diff --git a/tempest/lib/services/network/routers_client.py b/tempest/lib/services/network/routers_client.py
new file mode 100644
index 0000000..19b7627
--- /dev/null
+++ b/tempest/lib/services/network/routers_client.py
@@ -0,0 +1,86 @@
+# 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.network import base
+
+
+class RoutersClient(base.BaseNetworkClient):
+
+ def create_router(self, **kwargs):
+ """Create a router.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#create-router
+ """
+ post_body = {'router': kwargs}
+ uri = '/routers'
+ return self.create_resource(uri, post_body)
+
+ def update_router(self, router_id, **kwargs):
+ """Updates a logical router.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#update-router
+ """
+ uri = '/routers/%s' % router_id
+ update_body = {'router': kwargs}
+ return self.update_resource(uri, update_body)
+
+ def show_router(self, router_id, **fields):
+ """Shows details for a router.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#show-router-details
+ """
+ uri = '/routers/%s' % router_id
+ return self.show_resource(uri, **fields)
+
+ def delete_router(self, router_id):
+ uri = '/routers/%s' % router_id
+ return self.delete_resource(uri)
+
+ def list_routers(self, **filters):
+ """Lists logical routers.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#list-routers
+ """
+ uri = '/routers'
+ return self.list_resources(uri, **filters)
+
+ def add_router_interface(self, router_id, **kwargs):
+ """Add router interface.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#add-interface-to-router
+ """
+ uri = '/routers/%s/add_router_interface' % router_id
+ return self.update_resource(uri, kwargs)
+
+ def remove_router_interface(self, router_id, **kwargs):
+ """Remove router interface.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#remove-interface-from-router
+ """
+ uri = '/routers/%s/remove_router_interface' % router_id
+ return self.update_resource(uri, kwargs)
+
+ def list_l3_agents_hosting_router(self, router_id):
+ uri = '/routers/%s/l3-agents' % router_id
+ return self.list_resources(uri)
diff --git a/tempest/lib/services/network/security_group_rules_client.py b/tempest/lib/services/network/security_group_rules_client.py
new file mode 100644
index 0000000..d2bc4a9
--- /dev/null
+++ b/tempest/lib/services/network/security_group_rules_client.py
@@ -0,0 +1,51 @@
+# 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.network import base
+
+
+class SecurityGroupRulesClient(base.BaseNetworkClient):
+
+ def create_security_group_rule(self, **kwargs):
+ """Creates an OpenStack Networking security group rule.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#create-security-group-rule
+ """
+ uri = '/security-group-rules'
+ post_data = {'security_group_rule': kwargs}
+ return self.create_resource(uri, post_data)
+
+ def show_security_group_rule(self, security_group_rule_id, **fields):
+ """Shows detailed information for a security group rule.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#show-security-group-rule
+ """
+ uri = '/security-group-rules/%s' % security_group_rule_id
+ return self.show_resource(uri, **fields)
+
+ def delete_security_group_rule(self, security_group_rule_id):
+ uri = '/security-group-rules/%s' % security_group_rule_id
+ return self.delete_resource(uri)
+
+ def list_security_group_rules(self, **filters):
+ """Lists a summary of all OpenStack Networking security group rules.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#list-security-group-rules
+ """
+ uri = '/security-group-rules'
+ return self.list_resources(uri, **filters)
diff --git a/tempest/lib/services/network/security_groups_client.py b/tempest/lib/services/network/security_groups_client.py
new file mode 100644
index 0000000..d3ebf20
--- /dev/null
+++ b/tempest/lib/services/network/security_groups_client.py
@@ -0,0 +1,76 @@
+# 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 import exceptions as lib_exc
+from tempest.lib.services.network import base
+
+
+class SecurityGroupsClient(base.BaseNetworkClient):
+
+ def create_security_group(self, **kwargs):
+ """Creates an OpenStack Networking security group.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#create-security-group
+ """
+ uri = '/security-groups'
+ post_data = {'security_group': kwargs}
+ return self.create_resource(uri, post_data)
+
+ def update_security_group(self, security_group_id, **kwargs):
+ """Updates a security group.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#update-security-group
+ """
+ uri = '/security-groups/%s' % security_group_id
+ post_data = {'security_group': kwargs}
+ return self.update_resource(uri, post_data)
+
+ def show_security_group(self, security_group_id, **fields):
+ """Shows details for a security group.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#show-security-group
+ """
+ uri = '/security-groups/%s' % security_group_id
+ return self.show_resource(uri, **fields)
+
+ def delete_security_group(self, security_group_id):
+ """Deletes an OpenStack Networking security group.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#delete-security-group
+ """
+ uri = '/security-groups/%s' % security_group_id
+ return self.delete_resource(uri)
+
+ def list_security_groups(self, **filters):
+ """Lists OpenStack Networking security groups.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#list-security-groups
+ """
+ uri = '/security-groups'
+ return self.list_resources(uri, **filters)
+
+ def is_resource_deleted(self, id):
+ try:
+ self.show_security_group(id)
+ except lib_exc.NotFound:
+ return True
+ return False
diff --git a/tempest/lib/services/network/service_providers_client.py b/tempest/lib/services/network/service_providers_client.py
new file mode 100644
index 0000000..01313a0
--- /dev/null
+++ b/tempest/lib/services/network/service_providers_client.py
@@ -0,0 +1,26 @@
+# 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.network import base
+
+
+class ServiceProvidersClient(base.BaseNetworkClient):
+
+ def list_service_providers(self, **filters):
+ """Lists service providers.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/network/v2/index.html#list-service-providers
+ """
+ uri = '/service-providers'
+ return self.list_resources(uri, **filters)
diff --git a/tempest/lib/services/network/subnetpools_client.py b/tempest/lib/services/network/subnetpools_client.py
new file mode 100644
index 0000000..7e77e30
--- /dev/null
+++ b/tempest/lib/services/network/subnetpools_client.py
@@ -0,0 +1,64 @@
+# Copyright 2015 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.services.network import base
+
+
+class SubnetpoolsClient(base.BaseNetworkClient):
+
+ def list_subnetpools(self, **filters):
+ """Lists subnet pools to which the tenant has access.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#list-subnet-pools
+ """
+ uri = '/subnetpools'
+ return self.list_resources(uri, **filters)
+
+ def create_subnetpool(self, **kwargs):
+ """Creates a subnet pool.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#create-subnet-pool
+ """
+ uri = '/subnetpools'
+ post_data = {'subnetpool': kwargs}
+ return self.create_resource(uri, post_data)
+
+ def show_subnetpool(self, subnetpool_id, **fields):
+ """Shows information for a subnet pool.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#show-subnet-pool
+ """
+ uri = '/subnetpools/%s' % subnetpool_id
+ return self.show_resource(uri, **fields)
+
+ def update_subnetpool(self, subnetpool_id, **kwargs):
+ """Updates a subnet pool.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#update-subnet-pool
+ """
+ uri = '/subnetpools/%s' % subnetpool_id
+ post_data = {'subnetpool': kwargs}
+ return self.update_resource(uri, post_data)
+
+ def delete_subnetpool(self, subnetpool_id):
+ uri = '/subnetpools/%s' % subnetpool_id
+ return self.delete_resource(uri)
diff --git a/tempest/lib/services/network/subnets_client.py b/tempest/lib/services/network/subnets_client.py
new file mode 100644
index 0000000..b843f84
--- /dev/null
+++ b/tempest/lib/services/network/subnets_client.py
@@ -0,0 +1,72 @@
+# 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.network import base
+
+
+class SubnetsClient(base.BaseNetworkClient):
+
+ def create_subnet(self, **kwargs):
+ """Creates a subnet on a network.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#create-subnet
+ """
+ uri = '/subnets'
+ post_data = {'subnet': kwargs}
+ return self.create_resource(uri, post_data)
+
+ def update_subnet(self, subnet_id, **kwargs):
+ """Updates a subnet.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#update-subnet
+ """
+ uri = '/subnets/%s' % subnet_id
+ post_data = {'subnet': kwargs}
+ return self.update_resource(uri, post_data)
+
+ def show_subnet(self, subnet_id, **fields):
+ """Shows details for a subnet.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#show-subnet-details
+ """
+ uri = '/subnets/%s' % subnet_id
+ return self.show_resource(uri, **fields)
+
+ def delete_subnet(self, subnet_id):
+ uri = '/subnets/%s' % subnet_id
+ return self.delete_resource(uri)
+
+ def list_subnets(self, **filters):
+ """Lists subnets to which the tenant has access.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#list-subnets
+ """
+ uri = '/subnets'
+ return self.list_resources(uri, **filters)
+
+ def create_bulk_subnets(self, **kwargs):
+ """Create multiple subnets in a single request.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#bulk-create-subnet
+ """
+ uri = '/subnets'
+ return self.create_resource(uri, kwargs)
diff --git a/tempest/lib/services/network/tags_client.py b/tempest/lib/services/network/tags_client.py
new file mode 100644
index 0000000..5d49a79
--- /dev/null
+++ b/tempest/lib/services/network/tags_client.py
@@ -0,0 +1,84 @@
+# Copyright 2017 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
+from tempest.lib.services.network import base
+
+
+class TagsClient(base.BaseNetworkClient):
+
+ def create_tag(self, resource_type, resource_id, tag):
+ """Adds a tag on the resource.
+
+ For more information, please refer to the official API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#add-a-tag
+ """
+ uri = '/%s/%s/tags/%s' % (resource_type, resource_id, tag)
+ return self.update_resource(
+ uri, json.dumps({}), expect_response_code=201,
+ expect_empty_body=True)
+
+ def check_tag_existence(self, resource_type, resource_id, tag):
+ """Confirm that a given tag is set on the resource.
+
+ For more information, please refer to the official API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#confirm-a-tag
+ """
+ # TODO(felipemonteiro): Use the "check_resource" method in
+ # ``BaseNetworkClient`` once it has been implemented.
+ uri = '%s/%s/%s/tags/%s' % (
+ self.uri_prefix, resource_type, resource_id, tag)
+ resp, _ = self.get(uri)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp)
+
+ def update_all_tags(self, resource_type, resource_id, tags):
+ """Replace all tags on the resource.
+
+ For more information, please refer to the official API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#replace-all-tags
+ """
+ uri = '/%s/%s/tags' % (resource_type, resource_id)
+ put_body = {"tags": tags}
+ return self.update_resource(uri, put_body)
+
+ def delete_tag(self, resource_type, resource_id, tag):
+ """Removes a tag on the resource.
+
+ For more information, please refer to the official API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#remove-a-tag
+ """
+ uri = '/%s/%s/tags/%s' % (resource_type, resource_id, tag)
+ return self.delete_resource(uri)
+
+ def delete_all_tags(self, resource_type, resource_id):
+ """Removes all tags on the resource.
+
+ For more information, please refer to the official API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#remove-all-tags
+ """
+ uri = '/%s/%s/tags' % (resource_type, resource_id)
+ return self.delete_resource(uri)
+
+ def list_tags(self, resource_type, resource_id):
+ """Retrieves the tags for a resource.
+
+ For more information, please refer to the official API reference:
+ http://developer.openstack.org/api-ref/networking/v2/index.html#obtain-tag-list
+ """
+ uri = '/%s/%s/tags' % (resource_type, resource_id)
+ return self.list_resources(uri)
diff --git a/tempest/lib/services/network/versions_client.py b/tempest/lib/services/network/versions_client.py
new file mode 100644
index 0000000..f87fe87
--- /dev/null
+++ b/tempest/lib/services/network/versions_client.py
@@ -0,0 +1,43 @@
+# Copyright 2016 VMware, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import time
+
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.services.network import base
+
+
+class NetworkVersionsClient(base.BaseNetworkClient):
+
+ def list_versions(self):
+ """Do a GET / to fetch available API version information."""
+
+ version_url = self._get_base_version_url()
+
+ # Note: we do a raw_request here because we want to use
+ # an unversioned URL, not "v2/$project_id/".
+ # Since raw_request doesn't log anything, we do that too.
+ start = time.time()
+ self._log_request_start('GET', version_url)
+ response, body = self.raw_request(version_url, 'GET')
+ self._error_checker(response, body)
+ end = time.time()
+ self._log_request('GET', version_url, response,
+ secs=(end - start), resp_body=body)
+
+ self.response_checker('GET', response, body)
+ self.expected_success(200, response.status)
+ body = json.loads(body)
+ return body
diff --git a/tempest/lib/services/object_storage/__init__.py b/tempest/lib/services/object_storage/__init__.py
new file mode 100644
index 0000000..4303d09
--- /dev/null
+++ b/tempest/lib/services/object_storage/__init__.py
@@ -0,0 +1,25 @@
+# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P.
+#
+# 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.object_storage.account_client import AccountClient
+from tempest.lib.services.object_storage.bulk_middleware_client import \
+ BulkMiddlewareClient
+from tempest.lib.services.object_storage.capabilities_client import \
+ CapabilitiesClient
+from tempest.lib.services.object_storage.container_client import \
+ ContainerClient
+from tempest.lib.services.object_storage.object_client import ObjectClient
+
+__all__ = ['AccountClient', 'BulkMiddlewareClient', 'CapabilitiesClient',
+ 'ContainerClient', 'ObjectClient']
diff --git a/tempest/lib/services/object_storage/account_client.py b/tempest/lib/services/object_storage/account_client.py
new file mode 100644
index 0000000..6b097c1
--- /dev/null
+++ b/tempest/lib/services/object_storage/account_client.py
@@ -0,0 +1,78 @@
+# 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 xml.etree import ElementTree as etree
+
+from oslo_serialization import jsonutils as json
+from six.moves.urllib import parse as urllib
+
+from tempest.lib.common import rest_client
+
+
+class AccountClient(rest_client.RestClient):
+
+ def create_update_or_delete_account_metadata(
+ self,
+ create_update_metadata=None,
+ delete_metadata=None,
+ create_update_metadata_prefix='X-Account-Meta-',
+ delete_metadata_prefix='X-Remove-Account-Meta-'):
+ """Creates, Updates or deletes an account metadata entry.
+
+ Account Metadata can be created, updated or deleted based on
+ metadata header or value. For detailed info, please refer to the
+ official API reference:
+ https://developer.openstack.org/api-ref/object-store/#create-update-or-delete-account-metadata
+ """
+ headers = {}
+ if create_update_metadata:
+ for key in create_update_metadata:
+ metadata_header_name = create_update_metadata_prefix + key
+ headers[metadata_header_name] = create_update_metadata[key]
+ if delete_metadata:
+ for key in delete_metadata:
+ headers[delete_metadata_prefix + key] = delete_metadata[key]
+
+ resp, body = self.post('', headers=headers, body=None)
+ self.expected_success([200, 204], resp.status)
+ return resp, body
+
+ def list_account_metadata(self):
+ """List all account metadata."""
+ resp, body = self.head('')
+ self.expected_success(204, resp.status)
+ return resp, body
+
+ def list_account_containers(self, params=None):
+ """List all containers for the account.
+
+ Given valid X-Auth-Token, returns a list of all containers for the
+ account.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/object-store/#show-account-details-and-list-containers
+ """
+ url = '?%s' % urllib.urlencode(params) if params else ''
+
+ resp, body = self.get(url, headers={})
+ if params and params.get('format') == 'json':
+ body = json.loads(body)
+ elif params and params.get('format') == 'xml':
+ body = etree.fromstring(body)
+ else:
+ body = body.strip().splitlines()
+ self.expected_success([200, 204], resp.status)
+ return resp, body
diff --git a/tempest/lib/services/object_storage/bulk_middleware_client.py b/tempest/lib/services/object_storage/bulk_middleware_client.py
new file mode 100644
index 0000000..c11a105
--- /dev/null
+++ b/tempest/lib/services/object_storage/bulk_middleware_client.py
@@ -0,0 +1,62 @@
+# 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 tempest.lib.common import rest_client
+
+
+class BulkMiddlewareClient(rest_client.RestClient):
+
+ def upload_archive(self, upload_path, data,
+ archive_file_format='tar', headers=None):
+ """Expand tar files into a Swift cluster.
+
+ To extract containers and objects on Swift cluster from
+ uploaded archived file. For More information please check:
+ https://docs.openstack.org/swift/latest/middleware.html#module-swift.common.middleware.bulk
+ """
+ url = '%s?extract-archive=%s' % (upload_path, archive_file_format)
+ if headers is None:
+ headers = {}
+ resp, body = self.put(url, data, headers)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBodyData(resp, body)
+
+ def delete_bulk_data(self, data=None, headers=None):
+ """Delete multiple objects or containers from their account.
+
+ For More information please check:
+ https://docs.openstack.org/swift/latest/middleware.html#module-swift.common.middleware.bulk
+ """
+ url = '?bulk-delete'
+
+ if headers is None:
+ headers = {}
+ resp, body = self.delete(url, headers, data)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBodyData(resp, body)
+
+ def delete_bulk_data_with_post(self, data=None, headers=None):
+ """Delete multiple objects or containers with POST request.
+
+ For More information please check:
+ https://docs.openstack.org/swift/latest/middleware.html#module-swift.common.middleware.bulk
+ """
+ url = '?bulk-delete'
+
+ if headers is None:
+ headers = {}
+ resp, body = self.post(url, data, headers)
+ self.expected_success([200, 204], resp.status)
+ return rest_client.ResponseBodyData(resp, body)
diff --git a/tempest/lib/services/object_storage/capabilities_client.py b/tempest/lib/services/object_storage/capabilities_client.py
new file mode 100644
index 0000000..d31bbc2
--- /dev/null
+++ b/tempest/lib/services/object_storage/capabilities_client.py
@@ -0,0 +1,31 @@
+# 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
+
+
+class CapabilitiesClient(rest_client.RestClient):
+
+ def list_capabilities(self):
+ self.skip_path()
+ try:
+ resp, body = self.get('info')
+ finally:
+ self.reset_path()
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/object_storage/container_client.py b/tempest/lib/services/object_storage/container_client.py
new file mode 100644
index 0000000..430e0d4
--- /dev/null
+++ b/tempest/lib/services/object_storage/container_client.py
@@ -0,0 +1,124 @@
+# 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 xml.etree import ElementTree as etree
+
+import debtcollector.moves
+from oslo_serialization import jsonutils as json
+from six.moves.urllib import parse as urllib
+
+from tempest.lib.common import rest_client
+
+
+class ContainerClient(rest_client.RestClient):
+
+ def update_container(self, container_name, **headers):
+ """Creates or Updates a container
+
+ with optional metadata passed in as a dictionary.
+ Full list of allowed headers or value, please refer to the
+ official API reference:
+ https://developer.openstack.org/api-ref/object-store/#create-container
+ """
+ url = str(container_name)
+
+ resp, body = self.put(url, body=None, headers=headers)
+ self.expected_success([201, 202], resp.status)
+ return resp, body
+
+ # NOTE: This alias is for the usability because PUT can be used for both
+ # updating/creating a resource and this PUT is mainly used for creating
+ # on Swift container API.
+ create_container = update_container
+
+ def delete_container(self, container_name):
+ """Deletes the container (if it's empty)."""
+ url = str(container_name)
+ resp, body = self.delete(url)
+ self.expected_success(204, resp.status)
+ return resp, body
+
+ def create_update_or_delete_container_metadata(
+ self, container_name,
+ create_update_metadata=None,
+ delete_metadata=None,
+ create_update_metadata_prefix='X-Container-Meta-',
+ delete_metadata_prefix='X-Remove-Container-Meta-'):
+ """Creates, Updates or deletes an containter metadata entry.
+
+ Container Metadata can be created, updated or deleted based on
+ metadata header or value. For detailed info, please refer to the
+ official API reference:
+ https://developer.openstack.org/api-ref/object-store/#create-update-or-delete-container-metadata
+ """
+ url = str(container_name)
+ headers = {}
+ if create_update_metadata:
+ for key in create_update_metadata:
+ metadata_header_name = create_update_metadata_prefix + key
+ headers[metadata_header_name] = create_update_metadata[key]
+ if delete_metadata:
+ for key in delete_metadata:
+ headers[delete_metadata_prefix + key] = delete_metadata[key]
+
+ resp, body = self.post(url, headers=headers, body=None)
+ self.expected_success(204, resp.status)
+ return resp, body
+
+ update_container_metadata = debtcollector.moves.moved_function(
+ create_update_or_delete_container_metadata,
+ 'update_container_metadata', __name__,
+ version='Queens', removal_version='Rocky')
+
+ def list_container_metadata(self, container_name):
+ """List all container metadata."""
+ url = str(container_name)
+ resp, body = self.head(url)
+ self.expected_success(204, resp.status)
+ return resp, body
+
+ def list_container_objects(self, container_name, params=None):
+ """List the objects in a container, given the container name
+
+ Returns the container object listing as a plain text list, or as
+ xml or json if that option is specified via the 'format' argument.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/object-store/#show-container-details-and-list-objects
+ """
+
+ url = str(container_name)
+ if params:
+ url += '?'
+ url += '&%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url, headers={})
+ if params and params.get('format') == 'json':
+ body = json.loads(body)
+ elif params and params.get('format') == 'xml':
+ body = etree.fromstring(body)
+ # Else the content-type is plain/text
+ else:
+ body = [
+ obj_name for obj_name in body.decode().split('\n') if obj_name
+ ]
+
+ self.expected_success([200, 204], resp.status)
+ return resp, body
+
+ list_container_contents = debtcollector.moves.moved_function(
+ list_container_objects, 'list_container_contents', __name__,
+ version='Queens', removal_version='Rocky')
diff --git a/tempest/lib/services/object_storage/object_client.py b/tempest/lib/services/object_storage/object_client.py
new file mode 100644
index 0000000..383aff6
--- /dev/null
+++ b/tempest/lib/services/object_storage/object_client.py
@@ -0,0 +1,165 @@
+# 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 six.moves import http_client as httplib
+from six.moves.urllib import parse as urlparse
+
+from tempest.lib.common import rest_client
+from tempest.lib import exceptions
+
+
+class ObjectClient(rest_client.RestClient):
+
+ def create_object(self, container, object_name, data,
+ params=None, metadata=None, headers=None,
+ chunked=False):
+ """Create storage object."""
+
+ if headers is None:
+ headers = self.get_headers()
+ if not data:
+ headers['content-length'] = '0'
+ if metadata:
+ for key in metadata:
+ headers[str(key)] = metadata[key]
+ url = "%s/%s" % (str(container), str(object_name))
+ if params:
+ url += '?%s' % urlparse.urlencode(params)
+
+ resp, body = self.put(url, data, headers, chunked=chunked)
+ self.expected_success(201, resp.status)
+ return resp, body
+
+ def delete_object(self, container, object_name, params=None):
+ """Delete storage object."""
+ url = "%s/%s" % (str(container), str(object_name))
+ if params:
+ url += '?%s' % urlparse.urlencode(params)
+ resp, body = self.delete(url, headers={})
+ self.expected_success([200, 204], resp.status)
+ return resp, body
+
+ def create_or_update_object_metadata(self, container, object_name,
+ headers=None):
+ """Add, remove, or change X-Object-Meta metadata for storage object."""
+
+ url = "%s/%s" % (str(container), str(object_name))
+ resp, body = self.post(url, None, headers=headers)
+ self.expected_success(202, resp.status)
+ return resp, body
+
+ def list_object_metadata(self, container, object_name,
+ params=None, headers=None):
+ """List all storage object X-Object-Meta- metadata."""
+
+ url = "%s/%s" % (str(container), str(object_name))
+ if params:
+ url += '?%s' % urlparse.urlencode(params)
+ resp, body = self.head(url, headers=headers)
+ self.expected_success(200, resp.status)
+ return resp, body
+
+ def get_object(self, container, object_name, metadata=None, params=None):
+ """Retrieve object's data."""
+
+ headers = {}
+ if metadata:
+ for key in metadata:
+ headers[str(key)] = metadata[key]
+
+ url = "{0}/{1}".format(container, object_name)
+ if params:
+ url += '?%s' % urlparse.urlencode(params)
+ resp, body = self.get(url, headers=headers)
+ self.expected_success([200, 206], resp.status)
+ return resp, body
+
+ def copy_object_2d_way(self, container, src_object_name, dest_object_name,
+ metadata=None):
+ """Copy storage object's data to the new object using COPY."""
+
+ url = "{0}/{1}".format(container, src_object_name)
+ headers = {}
+ headers['Destination'] = "%s/%s" % (str(container),
+ str(dest_object_name))
+ if metadata:
+ for key in metadata:
+ headers[str(key)] = metadata[key]
+
+ resp, body = self.copy(url, headers=headers)
+ self.expected_success(201, resp.status)
+ return resp, body
+
+ def create_object_continue(self, container, object_name,
+ data, metadata=None):
+ """Put an object using Expect:100-continue"""
+ headers = {}
+ if metadata:
+ for key in metadata:
+ headers[str(key)] = metadata[key]
+
+ headers['X-Auth-Token'] = self.token
+ headers['content-length'] = 0 if data is None else len(data)
+ headers['Expect'] = '100-continue'
+
+ parsed = urlparse.urlparse(self.base_url)
+ path = str(parsed.path) + "/"
+ path += "%s/%s" % (str(container), str(object_name))
+
+ conn = _create_connection(parsed)
+ # Send the PUT request and the headers including the "Expect" header
+ conn.putrequest('PUT', path)
+
+ for header, value in headers.items():
+ conn.putheader(header, value)
+ conn.endheaders()
+
+ # Read the 100 status prior to sending the data
+ response = conn.response_class(conn.sock,
+ method=conn._method)
+ _, status, _ = response._read_status()
+
+ # toss the CRLF at the end of the response
+ response._safe_read(2)
+
+ # Expecting a 100 here, if not close and throw an exception
+ if status != 100:
+ conn.close()
+ pattern = "%s %s" % (
+ """Unexpected http success status code {0}.""",
+ """The expected status code is {1}""")
+ details = pattern.format(status, 100)
+ raise exceptions.UnexpectedResponseCode(details)
+
+ # If a continue was received go ahead and send the data
+ # and get the final response
+ conn.send(data)
+
+ resp = conn.getresponse()
+
+ return resp.status, resp.reason
+
+
+def _create_connection(parsed_url):
+ """Helper function to create connection with httplib
+
+ :param parsed_url: parsed url of the remote location
+ """
+ if parsed_url.scheme == 'https':
+ conn = httplib.HTTPSConnection(parsed_url.netloc)
+ else:
+ conn = httplib.HTTPConnection(parsed_url.netloc)
+
+ return conn
diff --git a/tempest/lib/services/volume/__init__.py b/tempest/lib/services/volume/__init__.py
new file mode 100644
index 0000000..6855d8e
--- /dev/null
+++ b/tempest/lib/services/volume/__init__.py
@@ -0,0 +1,19 @@
+# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P.
+#
+# 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.volume import v1
+from tempest.lib.services.volume import v2
+from tempest.lib.services.volume import v3
+
+__all__ = ['v1', 'v2', 'v3']
diff --git a/tempest/lib/services/volume/base_client.py b/tempest/lib/services/volume/base_client.py
new file mode 100644
index 0000000..c7fb21a
--- /dev/null
+++ b/tempest/lib/services/volume/base_client.py
@@ -0,0 +1,45 @@
+# Copyright 2016 Andrew Kerr
+# 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.common import api_version_utils
+from tempest.lib.common import rest_client
+
+VOLUME_MICROVERSION = None
+
+
+class BaseClient(rest_client.RestClient):
+ """Base volume service clients class to support microversion."""
+ api_microversion_header_name = 'Openstack-Api-Version'
+
+ def get_headers(self, accept_type=None, send_type=None):
+ headers = super(BaseClient, self).get_headers(
+ accept_type=accept_type, send_type=send_type)
+ if VOLUME_MICROVERSION:
+ headers[self.api_microversion_header_name] = ('volume %s' %
+ VOLUME_MICROVERSION)
+ return headers
+
+ def request(self, method, url, extra_headers=False, headers=None,
+ body=None, chunked=False):
+
+ resp, resp_body = super(BaseClient, self).request(
+ method, url, extra_headers, headers, body, chunked)
+ if (VOLUME_MICROVERSION and
+ VOLUME_MICROVERSION != api_version_utils.LATEST_MICROVERSION):
+ api_version_utils.assert_version_header_matches_request(
+ self.api_microversion_header_name,
+ 'volume %s' % VOLUME_MICROVERSION,
+ resp)
+ return resp, resp_body
diff --git a/tempest/lib/services/volume/v1/__init__.py b/tempest/lib/services/volume/v1/__init__.py
new file mode 100644
index 0000000..7b5991f
--- /dev/null
+++ b/tempest/lib/services/volume/v1/__init__.py
@@ -0,0 +1,33 @@
+# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P.
+#
+# 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.volume.v1.availability_zone_client \
+ import AvailabilityZoneClient
+from tempest.lib.services.volume.v1.backups_client import BackupsClient
+from tempest.lib.services.volume.v1.encryption_types_client import \
+ EncryptionTypesClient
+from tempest.lib.services.volume.v1.extensions_client import ExtensionsClient
+from tempest.lib.services.volume.v1.hosts_client import HostsClient
+from tempest.lib.services.volume.v1.limits_client import LimitsClient
+from tempest.lib.services.volume.v1.qos_client import QosSpecsClient
+from tempest.lib.services.volume.v1.quotas_client import QuotasClient
+from tempest.lib.services.volume.v1.services_client import ServicesClient
+from tempest.lib.services.volume.v1.snapshots_client import SnapshotsClient
+from tempest.lib.services.volume.v1.types_client import TypesClient
+from tempest.lib.services.volume.v1.volumes_client import VolumesClient
+
+__all__ = ['AvailabilityZoneClient', 'BackupsClient', 'EncryptionTypesClient',
+ 'ExtensionsClient', 'HostsClient', 'QosSpecsClient', 'QuotasClient',
+ 'ServicesClient', 'SnapshotsClient', 'TypesClient', 'VolumesClient',
+ 'LimitsClient']
diff --git a/tempest/lib/services/volume/v1/availability_zone_client.py b/tempest/lib/services/volume/v1/availability_zone_client.py
new file mode 100644
index 0000000..be4f539
--- /dev/null
+++ b/tempest/lib/services/volume/v1/availability_zone_client.py
@@ -0,0 +1,28 @@
+# Copyright 2014 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 oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+
+
+class AvailabilityZoneClient(rest_client.RestClient):
+ """Volume V1 availability zone client."""
+
+ 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/v1/backups_client.py b/tempest/lib/services/volume/v1/backups_client.py
new file mode 100644
index 0000000..77c40b3
--- /dev/null
+++ b/tempest/lib/services/volume/v1/backups_client.py
@@ -0,0 +1,109 @@
+# 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 tempest.lib.common import rest_client
+from tempest.lib import exceptions as lib_exc
+
+
+class BackupsClient(rest_client.RestClient):
+ """Volume V1 Backups client"""
+ api_version = "v1"
+
+ def create_backup(self, **kwargs):
+ """Creates a backup of 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-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:
+ http://developer.openstack.org/api-ref/block-storage/v2/#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):
+ """Information for all the tenant's backups."""
+ url = "backups"
+ if detail:
+ url += "/detail"
+ 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/v1/encryption_types_client.py b/tempest/lib/services/volume/v1/encryption_types_client.py
new file mode 100644
index 0000000..1fde79f
--- /dev/null
+++ b/tempest/lib/services/volume/v1/encryption_types_client.py
@@ -0,0 +1,68 @@
+# 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.
+
+ :param 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 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)
diff --git a/tempest/lib/services/volume/v1/extensions_client.py b/tempest/lib/services/volume/v1/extensions_client.py
new file mode 100644
index 0000000..7b849a8
--- /dev/null
+++ b/tempest/lib/services/volume/v1/extensions_client.py
@@ -0,0 +1,29 @@
+# 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
+
+
+class ExtensionsClient(rest_client.RestClient):
+ """Volume V1 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/v1/hosts_client.py b/tempest/lib/services/volume/v1/hosts_client.py
new file mode 100644
index 0000000..9b19b84
--- /dev/null
+++ b/tempest/lib/services/volume/v1/hosts_client.py
@@ -0,0 +1,39 @@
+# Copyright 2013 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 Host API V1 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/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)
diff --git a/tempest/lib/services/volume/v1/limits_client.py b/tempest/lib/services/volume/v1/limits_client.py
new file mode 100644
index 0000000..e14b2dc
--- /dev/null
+++ b/tempest/lib/services/volume/v1/limits_client.py
@@ -0,0 +1,32 @@
+# 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 V1 limits client."""
+
+ api_version = "v1"
+
+ 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/v1/qos_client.py b/tempest/lib/services/volume/v1/qos_client.py
new file mode 100644
index 0000000..593bddd
--- /dev/null
+++ b/tempest/lib/services/volume/v1/qos_client.py
@@ -0,0 +1,133 @@
+# 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 V1 QoS client.
+
+ Client class to send CRUD QoS API requests
+ """
+
+ api_version = "v1"
+
+ 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:
+ https://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)
diff --git a/tempest/lib/services/volume/v1/quotas_client.py b/tempest/lib/services/volume/v1/quotas_client.py
new file mode 100644
index 0000000..84f34f2
--- /dev/null
+++ b/tempest/lib/services/volume/v1/quotas_client.py
@@ -0,0 +1,62 @@
+# Copyright (C) 2014 eNovance SAS <licensing@enovance.com>
+#
+# 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 V1 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/v2/#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)
diff --git a/tempest/lib/services/volume/v1/services_client.py b/tempest/lib/services/volume/v1/services_client.py
new file mode 100644
index 0000000..d438a34
--- /dev/null
+++ b/tempest/lib/services/volume/v1/services_client.py
@@ -0,0 +1,33 @@
+# Copyright 2014 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 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):
+ """Volume V1 volume services client"""
+
+ 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/v1/snapshots_client.py b/tempest/lib/services/volume/v1/snapshots_client.py
new file mode 100644
index 0000000..51f7b9b
--- /dev/null
+++ b/tempest/lib/services/volume/v1/snapshots_client.py
@@ -0,0 +1,187 @@
+# 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 SnapshotsClient(rest_client.RestClient):
+ """Client class to send CRUD Volume V1 API requests."""
+
+ create_resp = 200
+
+ 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/v2/#list-snapshots
+ https://developer.openstack.org/api-ref/block-storage/v2/#list-snapshots-with-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/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:
+ https://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 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/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."""
+ 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 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/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 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/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:
+ https://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 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)
diff --git a/tempest/lib/services/volume/v1/types_client.py b/tempest/lib/services/volume/v1/types_client.py
new file mode 100644
index 0000000..58a80b7
--- /dev/null
+++ b/tempest/lib/services/volume/v1/types_client.py
@@ -0,0 +1,165 @@
+# 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 Types 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/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:
+ https://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:
+ https://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:
+ https://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)
diff --git a/tempest/lib/services/volume/v1/volumes_client.py b/tempest/lib/services/volume/v1/volumes_client.py
new file mode 100644
index 0000000..0e6ea9f
--- /dev/null
+++ b/tempest/lib/services/volume/v1/volumes_client.py
@@ -0,0 +1,306 @@
+# 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
+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
+
+
+class VolumesClient(rest_client.RestClient):
+ """Client class to send CRUD Volume V1 API requests"""
+
+ 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/v2/#list-volumes
+ https://developer.openstack.org/api-ref/block-storage/v2/#list-volumes-with-details
+ """
+ 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/v2/#create-volume
+ """
+ post_body = json.dumps({'volume': kwargs})
+ resp, body = self.post('volumes', post_body)
+ body = json.loads(body)
+ self.expected_success(200, 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/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):
+ """Deletes the Specified Volume."""
+ resp, body = self.delete("volumes/%s" % volume_id)
+ 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:
+ https://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."""
+ 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):
+ try:
+ self.show_volume(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'
+
+ 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/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:
+ https://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 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, **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
+ """
+ url = 'os-volume-transfer'
+ 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)
+
+ 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."""
+ 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."""
+ 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 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."""
+ post_body = json.dumps({'os-retype': kwargs})
+ resp, body = self.post('volumes/%s/action' % volume_id, post_body)
+ self.expected_success(202, resp.status)
diff --git a/tempest/lib/services/volume/v2/__init__.py b/tempest/lib/services/volume/v2/__init__.py
new file mode 100644
index 0000000..68982d9
--- /dev/null
+++ b/tempest/lib/services/volume/v2/__init__.py
@@ -0,0 +1,46 @@
+# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P.
+#
+# 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.volume.v2.availability_zone_client \
+ import AvailabilityZoneClient
+from tempest.lib.services.volume.v2.backups_client import BackupsClient
+from tempest.lib.services.volume.v2.capabilities_client import \
+ CapabilitiesClient
+from tempest.lib.services.volume.v2.encryption_types_client import \
+ EncryptionTypesClient
+from tempest.lib.services.volume.v2.extensions_client import ExtensionsClient
+from tempest.lib.services.volume.v2.hosts_client import HostsClient
+from tempest.lib.services.volume.v2.limits_client import LimitsClient
+from tempest.lib.services.volume.v2.qos_client import QosSpecsClient
+from tempest.lib.services.volume.v2.quota_classes_client import \
+ QuotaClassesClient
+from tempest.lib.services.volume.v2.quotas_client import QuotasClient
+from tempest.lib.services.volume.v2.scheduler_stats_client import \
+ SchedulerStatsClient
+from tempest.lib.services.volume.v2.services_client import ServicesClient
+from tempest.lib.services.volume.v2.snapshot_manage_client import \
+ SnapshotManageClient
+from tempest.lib.services.volume.v2.snapshots_client import SnapshotsClient
+from tempest.lib.services.volume.v2.transfers_client import TransfersClient
+from tempest.lib.services.volume.v2.types_client import TypesClient
+from tempest.lib.services.volume.v2.volume_manage_client import \
+ VolumeManageClient
+from tempest.lib.services.volume.v2.volumes_client import VolumesClient
+
+__all__ = ['AvailabilityZoneClient', 'BackupsClient', 'EncryptionTypesClient',
+ 'ExtensionsClient', 'HostsClient', 'QosSpecsClient', 'QuotasClient',
+ 'ServicesClient', 'SnapshotsClient', 'TypesClient', 'VolumesClient',
+ 'LimitsClient', 'CapabilitiesClient', 'SchedulerStatsClient',
+ 'SnapshotManageClient', 'VolumeManageClient', 'TransfersClient',
+ 'QuotaClassesClient']
diff --git a/tempest/lib/services/volume/v2/availability_zone_client.py b/tempest/lib/services/volume/v2/availability_zone_client.py
new file mode 100644
index 0000000..bdb2304
--- /dev/null
+++ b/tempest/lib/services/volume/v2/availability_zone_client.py
@@ -0,0 +1,23 @@
+# 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 debtcollector import moves
+
+from tempest.lib.services.volume.v3 import availability_zone_client
+
+
+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
new file mode 100644
index 0000000..80b3631
--- /dev/null
+++ b/tempest/lib/services/volume/v2/backups_client.py
@@ -0,0 +1,22 @@
+# 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 debtcollector import moves
+
+from tempest.lib.services.volume.v3 import backups_client
+
+
+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
new file mode 100644
index 0000000..d8cf806
--- /dev/null
+++ b/tempest/lib/services/volume/v2/capabilities_client.py
@@ -0,0 +1,23 @@
+# 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 debtcollector import moves
+
+from tempest.lib.services.volume.v3 import capabilities_client
+
+
+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
new file mode 100644
index 0000000..875e59e
--- /dev/null
+++ b/tempest/lib/services/volume/v2/encryption_types_client.py
@@ -0,0 +1,23 @@
+# 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 debtcollector import moves
+
+from tempest.lib.services.volume.v3 import encryption_types_client
+
+
+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
new file mode 100644
index 0000000..6316ef5
--- /dev/null
+++ b/tempest/lib/services/volume/v2/extensions_client.py
@@ -0,0 +1,22 @@
+# 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 debtcollector import moves
+
+from tempest.lib.services.volume.v3 import extensions_client
+
+
+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
new file mode 100644
index 0000000..38f1b38
--- /dev/null
+++ b/tempest/lib/services/volume/v2/hosts_client.py
@@ -0,0 +1,22 @@
+# 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 debtcollector import moves
+
+from tempest.lib.services.volume.v3 import hosts_client
+
+
+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
new file mode 100644
index 0000000..a6b8c5a
--- /dev/null
+++ b/tempest/lib/services/volume/v2/limits_client.py
@@ -0,0 +1,22 @@
+# 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 debtcollector import moves
+
+from tempest.lib.services.volume.v3 import limits_client
+
+
+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
new file mode 100644
index 0000000..b81384e
--- /dev/null
+++ b/tempest/lib/services/volume/v2/qos_client.py
@@ -0,0 +1,21 @@
+# 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 debtcollector import moves
+
+from tempest.lib.services.volume.v3 import qos_client
+
+
+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
new file mode 100644
index 0000000..24aab89
--- /dev/null
+++ b/tempest/lib/services/volume/v2/quota_classes_client.py
@@ -0,0 +1,22 @@
+# 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 debtcollector import moves
+
+from tempest.lib.services.volume.v3 import quota_classes_client
+
+
+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
new file mode 100644
index 0000000..6f9f61c
--- /dev/null
+++ b/tempest/lib/services/volume/v2/quotas_client.py
@@ -0,0 +1,22 @@
+# 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 debtcollector import moves
+
+from tempest.lib.services.volume.v3 import quotas_client
+
+
+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
new file mode 100644
index 0000000..a5adb34
--- /dev/null
+++ b/tempest/lib/services/volume/v2/scheduler_stats_client.py
@@ -0,0 +1,22 @@
+# 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 debtcollector import moves
+
+from tempest.lib.services.volume.v3 import scheduler_stats_client
+
+
+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
new file mode 100644
index 0000000..a4491d3
--- /dev/null
+++ b/tempest/lib/services/volume/v2/services_client.py
@@ -0,0 +1,22 @@
+# 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 debtcollector import moves
+
+from tempest.lib.services.volume.v3 import services_client
+
+
+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
new file mode 100644
index 0000000..132209f
--- /dev/null
+++ b/tempest/lib/services/volume/v2/snapshot_manage_client.py
@@ -0,0 +1,22 @@
+# 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 debtcollector import moves
+
+from tempest.lib.services.volume.v3 import snapshot_manage_client
+
+
+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
new file mode 100644
index 0000000..3a72cc1
--- /dev/null
+++ b/tempest/lib/services/volume/v2/snapshots_client.py
@@ -0,0 +1,19 @@
+# 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 debtcollector import moves
+
+from tempest.lib.services.volume.v3 import snapshots_client
+
+
+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
new file mode 100644
index 0000000..701d0ae
--- /dev/null
+++ b/tempest/lib/services/volume/v2/transfers_client.py
@@ -0,0 +1,22 @@
+# 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 debtcollector import moves
+
+from tempest.lib.services.volume.v3 import transfers_client
+
+
+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
new file mode 100644
index 0000000..8457f91
--- /dev/null
+++ b/tempest/lib/services/volume/v2/types_client.py
@@ -0,0 +1,22 @@
+# 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 debtcollector import moves
+
+from tempest.lib.services.volume.v3 import types_client
+
+
+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
new file mode 100644
index 0000000..0669326
--- /dev/null
+++ b/tempest/lib/services/volume/v2/volume_manage_client.py
@@ -0,0 +1,22 @@
+# 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 debtcollector import moves
+
+from tempest.lib.services.volume.v3 import volume_manage_client
+
+
+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
new file mode 100644
index 0000000..f5f9e6e
--- /dev/null
+++ b/tempest/lib/services/volume/v2/volumes_client.py
@@ -0,0 +1,22 @@
+# 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 debtcollector import moves
+
+from tempest.lib.services.volume.v3 import volumes_client
+
+
+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
new file mode 100644
index 0000000..a1b7de3
--- /dev/null
+++ b/tempest/lib/services/volume/v3/__init__.py
@@ -0,0 +1,54 @@
+# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P.
+#
+# 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.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__ = ['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
new file mode 100644
index 0000000..10538b0
--- /dev/null
+++ b/tempest/lib/services/volume/v3/backups_client.py
@@ -0,0 +1,132 @@
+# 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 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
+
+
+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.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/block-storage/v3/#update-a-backup
+ """
+ put_body = json.dumps({'backup': kwargs})
+ resp, body = self.put('backups/%s' % backup_id, put_body)
+ 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/base_client.py b/tempest/lib/services/volume/v3/base_client.py
new file mode 100644
index 0000000..e78380b
--- /dev/null
+++ b/tempest/lib/services/volume/v3/base_client.py
@@ -0,0 +1,23 @@
+# Copyright 2016 Andrew Kerr
+# 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 debtcollector import moves
+
+from tempest.lib.services.volume import base_client
+
+
+BaseClient = moves.moved_class(base_client.BaseClient, 'BaseClient', __name__,
+ version="Pike", removal_version='?')
+BaseClient.api_version = 'v3'
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..03de187
--- /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.
+
+ :param 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/group_snapshots_client.py b/tempest/lib/services/volume/v3/group_snapshots_client.py
new file mode 100644
index 0000000..6e53e3e
--- /dev/null
+++ b/tempest/lib/services/volume/v3/group_snapshots_client.py
@@ -0,0 +1,102 @@
+# Copyright (C) 2017 Dell Inc. or its subsidiaries.
+# 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
+from tempest.lib.services.volume import base_client
+
+
+class GroupSnapshotsClient(base_client.BaseClient):
+ """Client class to send CRUD Volume Group Snapshot API requests"""
+ api_version = 'v3'
+
+ def create_group_snapshot(self, **kwargs):
+ """Creates a group snapshot.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/block-storage/v3/#create-group-snapshot
+ """
+ post_body = json.dumps({'group_snapshot': kwargs})
+ resp, body = self.post('group_snapshots', post_body)
+ body = json.loads(body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_group_snapshot(self, group_snapshot_id):
+ """Deletes a group snapshot.
+
+ For more information, please refer to the official API reference:
+ https://developer.openstack.org/api-ref/block-storage/v3/#delete-group-snapshot
+ """
+ resp, body = self.delete('group_snapshots/%s' % group_snapshot_id)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_group_snapshot(self, group_snapshot_id):
+ """Returns the details of a single group snapshot.
+
+ For more information, please refer to the official API reference:
+ https://developer.openstack.org/api-ref/block-storage/v3/#show-group-snapshot-details
+ """
+ url = "group_snapshots/%s" % str(group_snapshot_id)
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_group_snapshots(self, detail=False, **params):
+ """Information for all the tenant's group snapshots.
+
+ For more information, please refer to the official API reference:
+ https://developer.openstack.org/api-ref/block-storage/v3/#list-group-snapshots
+ https://developer.openstack.org/api-ref/block-storage/v3/#list-group-snapshots-with-details
+ """
+ url = "group_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 reset_group_snapshot_status(self, group_snapshot_id, status_to_set):
+ """Resets group snapshot status.
+
+ For more information, please refer to the official API reference:
+ https://developer.openstack.org/api-ref/block-storage/v3/#reset-group-snapshot-status
+ """
+ post_body = json.dumps({'reset_status': {'status': status_to_set}})
+ resp, body = self.post('group_snapshots/%s/action' % group_snapshot_id,
+ post_body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def is_resource_deleted(self, id):
+ try:
+ self.show_group_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 'group-snapshot'
diff --git a/tempest/lib/services/volume/v3/group_types_client.py b/tempest/lib/services/volume/v3/group_types_client.py
new file mode 100644
index 0000000..ecbcba1
--- /dev/null
+++ b/tempest/lib/services/volume/v3/group_types_client.py
@@ -0,0 +1,141 @@
+# Copyright (C) 2017 Dell Inc. or its subsidiaries.
+# 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.services.volume import base_client
+
+
+class GroupTypesClient(base_client.BaseClient):
+ """Client class to send CRUD Volume V3 Group Types API requests"""
+ api_version = 'v3'
+
+ @property
+ def resource_type(self):
+ """Returns the primary type of resource this client works with."""
+ return 'group-type'
+
+ def create_group_type(self, **kwargs):
+ """Create group_type.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/block-storage/v3/#create-group-type
+ """
+ post_body = json.dumps({'group_type': kwargs})
+ resp, body = self.post('group_types', post_body)
+ body = json.loads(body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_group_type(self, group_type_id):
+ """Deletes the specified group_type."""
+ resp, body = self.delete("group_types/%s" % group_type_id)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_group_types(self, **params):
+ """List all the group_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/#list-group-types
+ """
+ url = 'group_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_group_type(self, group_type_id):
+ """Returns the details of a single group_type.
+
+ For more information, please refer to the official API reference:
+ https://developer.openstack.org/api-ref/block-storage/v3/#show-group-type-details
+ """
+ url = "group_types/%s" % group_type_id
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_group_type(self, group_type_id, **kwargs):
+ """Updates a group type.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/block-storage/v3/#update-group-type
+ """
+ post_body = json.dumps({'group_type': kwargs})
+ resp, body = self.put('group_types/%s' % group_type_id, post_body)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def create_or_update_group_type_specs(self, group_type_id, group_specs):
+ """Creates new group specs or updates existing group specs.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/block-storage/v3/#create-or-update-group-specs-for-a-group-type
+ """
+ url = "group_types/%s/group_specs" % group_type_id
+ post_body = json.dumps({'group_specs': group_specs})
+ resp, body = self.post(url, post_body)
+ body = json.loads(body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_group_type_specs(self, group_type_id):
+ """Lists all group specs for a given group type."""
+ url = 'group_types/%s/group_specs' % group_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_group_type_specs_item(self, group_type_id, spec_id):
+ """Shows specified item of group specs for a given group type."""
+ url = "group_types/%s/group_specs/%s" % (group_type_id, spec_id)
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_group_type_specs_item(self, group_type_id, spec_id, spec):
+ """Updates specified item of group specs for a given group type.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/block-storage/v3/#update-one-specific-group-spec-for-a-group-type
+ """
+ url = "group_types/%s/group_specs/%s" % (group_type_id, spec_id)
+ put_body = json.dumps(spec)
+ 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_group_type_specs_item(self, group_type_id, spec_id):
+ """Deletes specified item of group specs for a given group type."""
+ resp, body = self.delete("group_types/%s/group_specs/%s" % (
+ group_type_id, spec_id))
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/volume/v3/groups_client.py b/tempest/lib/services/volume/v3/groups_client.py
new file mode 100644
index 0000000..e2e477d
--- /dev/null
+++ b/tempest/lib/services/volume/v3/groups_client.py
@@ -0,0 +1,133 @@
+# Copyright (C) 2017 Dell Inc. or its subsidiaries.
+# 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
+from tempest.lib.services.volume import base_client
+
+
+class GroupsClient(base_client.BaseClient):
+ """Client class to send CRUD Volume Group API requests"""
+ api_version = 'v3'
+
+ def create_group(self, **kwargs):
+ """Creates a group.
+
+ group_type and volume_types are required parameters in kwargs.
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/block-storage/v3/#create-group
+ """
+ post_body = json.dumps({'group': kwargs})
+ resp, body = self.post('groups', post_body)
+ body = json.loads(body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_group(self, group_id, delete_volumes=True):
+ """Deletes a group.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/block-storage/v3/#delete-group
+ """
+ post_body = {'delete-volumes': delete_volumes}
+ post_body = json.dumps({'delete': post_body})
+ resp, body = self.post('groups/%s/action' % group_id,
+ post_body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_group(self, group_id):
+ """Returns the details of a single group.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/block-storage/v3/#show-group-details
+ """
+ url = "groups/%s" % str(group_id)
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_groups(self, detail=False, **params):
+ """Lists information for all the tenant's groups.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/block-storage/v3/#list-groups
+ https://developer.openstack.org/api-ref/block-storage/v3/#list-groups-with-details
+ """
+ url = "groups"
+ 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 create_group_from_source(self, **kwargs):
+ """Creates a group from source.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/block-storage/v3/#create-group-from-source
+ """
+ post_body = json.dumps({'create-from-src': kwargs})
+ resp, body = self.post('groups/action', post_body)
+ body = json.loads(body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_group(self, group_id, **kwargs):
+ """Updates the specified group.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/block-storage/v3/#update-group
+ """
+ put_body = json.dumps({'group': kwargs})
+ resp, body = self.put('groups/%s' % group_id, put_body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def reset_group_status(self, group_id, status_to_set):
+ """Resets group status.
+
+ For more information, please refer to the official API reference:
+ https://developer.openstack.org/api-ref/block-storage/v3/#reset-group-status
+ """
+ post_body = json.dumps({'reset_status': {'status': status_to_set}})
+ resp, body = self.post('groups/%s/action' % group_id, post_body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def is_resource_deleted(self, id):
+ try:
+ self.show_group(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 'group'
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/messages_client.py b/tempest/lib/services/volume/v3/messages_client.py
new file mode 100644
index 0000000..0127271
--- /dev/null
+++ b/tempest/lib/services/volume/v3/messages_client.py
@@ -0,0 +1,60 @@
+# Copyright 2016 Andrew Kerr
+# 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
+from tempest.lib.services.volume import base_client
+
+
+class MessagesClient(base_client.BaseClient):
+ """Client class to send user messages API requests."""
+ api_version = 'v3'
+
+ def show_message(self, message_id):
+ """Show details for a single message."""
+ url = 'messages/%s' % str(message_id)
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_messages(self):
+ """List all messages."""
+ url = 'messages'
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_message(self, message_id):
+ """Delete a single message."""
+ url = 'messages/%s' % str(message_id)
+ resp, body = self.delete(url)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def is_resource_deleted(self, id):
+ try:
+ self.show_message(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 'message'
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..22155a9
--- /dev/null
+++ b/tempest/lib/services/volume/v3/services_client.py
@@ -0,0 +1,102 @@
+# 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 Services API requests"""
+
+ def list_services(self, **params):
+ """List all Cinder services.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/block-storage/v3/#list-all-cinder-services
+ """
+ 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)
+
+ def enable_service(self, **kwargs):
+ """Enable service on a host.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/block-storage/v3/#enable-a-cinder-service
+ """
+ put_body = json.dumps(kwargs)
+ resp, body = self.put('os-services/enable', put_body)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def disable_service(self, **kwargs):
+ """Disable service on a host.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/block-storage/v3/#disable-a-cinder-service
+ """
+ put_body = json.dumps(kwargs)
+ resp, body = self.put('os-services/disable', put_body)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def disable_log_reason(self, **kwargs):
+ """Disable scheduling for a volume service and log disabled reason.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/block-storage/v3/#log-disabled-cinder-service-information
+ """
+ put_body = json.dumps(kwargs)
+ resp, body = self.put('os-services/disable-log-reason', put_body)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def freeze_host(self, **kwargs):
+ """Freeze a Cinder backend host.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/block-storage/v3/#freeze-a-cinder-backend-host
+ """
+ put_body = json.dumps(kwargs)
+ resp, _ = self.put('os-services/freeze', put_body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp)
+
+ def thaw_host(self, **kwargs):
+ """Thaw a Cinder backend host.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/block-storage/v3/#thaw-a-cinder-backend-host
+ """
+ put_body = json.dumps(kwargs)
+ resp, _ = self.put('os-services/thaw', put_body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp)
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
new file mode 100644
index 0000000..f79bcd8
--- /dev/null
+++ b/tempest/lib/services/volume/v3/snapshots_client.py
@@ -0,0 +1,212 @@
+# Copyright (C) 2017 Dell Inc. or its subsidiaries.
+# 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 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 for the snapshot for a specific key.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/block-storage/v3/#update-a-snapshot-s-metadata-for-a-specific-key
+ """
+ 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..13ecd15
--- /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 Types 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 type extra specs created.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/block-storage/v3/#show-all-extra-specifications-for-volume-type
+ """
+ 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 new volume type extra specs.
+
+ :param volume_type_id: Id of volume type.
+ :param 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.
+
+ :param volume_type_id: Id of volume type.
+ :param extra_spec_name: Name of the extra spec to be updated.
+ :param extra_specs: 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/versions_client.py b/tempest/lib/services/volume/v3/versions_client.py
new file mode 100644
index 0000000..5702f95
--- /dev/null
+++ b/tempest/lib/services/volume/v3/versions_client.py
@@ -0,0 +1,48 @@
+# Copyright 2017 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.
+
+import time
+
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.api_schema.response.volume import versions as schema
+from tempest.lib.common import rest_client
+from tempest.lib.services.volume import base_client
+
+
+class VersionsClient(base_client.BaseClient):
+ api_version = 'v3'
+
+ def list_versions(self):
+ """List API versions
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/block-storage/v3/#list-all-api-versions
+ """
+ version_url = self._get_base_version_url()
+
+ start = time.time()
+ resp, body = self.raw_request(version_url, 'GET')
+ end = time.time()
+ # NOTE: We need a raw_request() here instead of request() call because
+ # "list API versions" API doesn't require an authentication and we can
+ # skip it with raw_request() call.
+ self._log_request('GET', version_url, resp, secs=(end - start),
+ resp_body=body)
+ self._error_checker(resp, body)
+
+ body = json.loads(body)
+ self.validate_response(schema.list_versions, resp, body)
+ 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
new file mode 100644
index 0000000..a1185c4
--- /dev/null
+++ b/tempest/lib/services/volume/v3/volumes_client.py
@@ -0,0 +1,367 @@
+# 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
+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
+
+
+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.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/block-storage/v3/#get-volumes-summary
+ """
+ url = 'volumes/summary'
+ 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 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/manager.py b/tempest/manager.py
index 9904aa6..e3174d4 100644
--- a/tempest/manager.py
+++ b/tempest/manager.py
@@ -13,68 +13,50 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest_lib import auth
+from oslo_log import log as logging
-from tempest.common import cred_provider
+from tempest import clients as tempest_clients
from tempest import config
-from tempest import exceptions
+from tempest.lib.services import clients
CONF = config.CONF
+LOG = logging.getLogger(__name__)
-class Manager(object):
- """Base manager class
+class Manager(clients.ServiceClients):
+ """Service client manager class for backward compatibility
- Manager objects are responsible for providing a configuration object
- and a client object for a test case to use in performing actions.
+ The former manager.Manager is not a stable interface in Tempest,
+ nonetheless it is consumed by a number of plugins already. This class
+ exists to provide some grace time for the move to tempest.lib.
"""
- def __init__(self, credentials):
- """Initialization of base manager class
-
- Credentials to be used within the various client classes managed by the
- Manager object must be defined.
-
- :param credentials: type Credentials or TestResources
- """
- self.credentials = credentials
- # Check if passed or default credentials are valid
- if not self.credentials.is_valid():
- raise exceptions.InvalidCredentials()
- self.auth_version = CONF.identity.auth_version
- # Tenant isolation creates TestResources, but
- # PreProvisionedCredentialProvider and some tests create Credentials
- if isinstance(credentials, cred_provider.TestResources):
- creds = self.credentials.credentials
- else:
- creds = self.credentials
- # Creates an auth provider for the credentials
- self.auth_provider = get_auth_provider(creds, pre_auth=True)
- # FIXME(andreaf) unused
- self.client_attr_names = []
+ def __init__(self, credentials, scope='project'):
+ msg = ("tempest.manager.Manager is not a stable interface and as such "
+ "it should not imported directly. It will be removed as "
+ "soon as the client manager becomes available in tempest.lib.")
+ LOG.warning(msg)
+ dscv = CONF.identity.disable_ssl_certificate_validation
+ _, uri = tempest_clients.get_auth_provider_class(credentials)
+ super(Manager, self).__init__(
+ credentials=credentials, scope=scope,
+ identity_uri=uri,
+ disable_ssl_certificate_validation=dscv,
+ ca_certs=CONF.identity.ca_certificates_file,
+ trace_requests=CONF.debug.trace_requests)
-def get_auth_provider_class(credentials):
- if isinstance(credentials, auth.KeystoneV3Credentials):
- return auth.KeystoneV3AuthProvider, CONF.identity.uri_v3
- else:
- return auth.KeystoneV2AuthProvider, CONF.identity.uri
+def get_auth_provider(credentials, pre_auth=False, scope='project'):
+ """Shim to get_auth_provider in clients.py
-
-def get_auth_provider(credentials, pre_auth=False):
- default_params = {
- 'disable_ssl_certificate_validation':
- CONF.identity.disable_ssl_certificate_validation,
- 'ca_certs': CONF.identity.ca_certificates_file,
- 'trace_requests': CONF.debug.trace_requests
- }
- if credentials is None:
- raise exceptions.InvalidCredentials(
- 'Credentials must be specified')
- auth_provider_class, auth_url = get_auth_provider_class(
- credentials)
- _auth_provider = auth_provider_class(credentials, auth_url,
- **default_params)
- if pre_auth:
- _auth_provider.set_auth()
- return _auth_provider
+ get_auth_provider used to be hosted in this module, but it has been
+ moved to clients.py now as a more permanent location.
+ This module will be removed eventually, and this shim is only
+ maintained for the benefit of plugins already consuming this interface.
+ """
+ msg = ("tempest.manager.get_auth_provider is not a stable interface and "
+ "as such it should not imported directly. It will be removed as "
+ "the client manager becomes available in tempest.lib.")
+ LOG.warning(msg)
+ return tempest_clients.get_auth_provider(credentials=credentials,
+ pre_auth=pre_auth, scope=scope)
diff --git a/tempest/scenario/README.rst b/tempest/scenario/README.rst
index 38e0de9..efcd139 100644
--- a/tempest/scenario/README.rst
+++ b/tempest/scenario/README.rst
@@ -14,16 +14,17 @@
Any scenario test should have a real-life use case. An example would be:
- - "As operator I want to start with a blank environment":
- 1. upload a glance image
- 2. deploy a vm from it
- 3. ssh to the guest
- 4. create a snapshot of the vm
+- "As operator I want to start with a blank environment":
+
+ 1. upload a glance image
+ 2. deploy a vm from it
+ 3. ssh to the guest
+ 4. create a snapshot of the vm
-Why are these tests in tempest?
+Why are these tests in Tempest?
-------------------------------
-This is one of tempests core purposes, testing the integration between
+This is one of Tempest's core purposes, testing the integration between
projects.
@@ -43,7 +44,7 @@
specific in your interactions. A giant "this is my data center" smoke
test is hard to debug when it goes wrong.
-A flow of interactions between glance and nova, like in the
+A flow of interactions between Glance and Nova, like in the
introduction, is a good example. Especially if it involves a repeated
interaction when a resource is setup, modified, detached, and then
reused later again.
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index 3ec6800..145dcf1 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -19,17 +19,18 @@
import netaddr
from oslo_log import log
from oslo_serialization import jsonutils as json
-import six
-from tempest_lib.common.utils import misc as misc_utils
-from tempest_lib import exceptions as lib_exc
+from oslo_utils import netutils
from tempest.common import compute
-from tempest.common.utils import data_utils
+from tempest.common import image as common_image
from tempest.common.utils.linux import remote_client
+from tempest.common.utils import net_utils
from tempest.common import waiters
from tempest import config
from tempest import exceptions
-from tempest.services.network import resources as net_resources
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import exceptions as lib_exc
import tempest.test
CONF = config.CONF
@@ -46,110 +47,62 @@
def setup_clients(cls):
super(ScenarioTest, cls).setup_clients()
# Clients (in alphabetical order)
- cls.flavors_client = cls.manager.flavors_client
+ cls.flavors_client = cls.os_primary.flavors_client
cls.compute_floating_ips_client = (
- cls.manager.compute_floating_ips_client)
- # Glance image client v1
- cls.image_client = cls.manager.image_client
+ cls.os_primary.compute_floating_ips_client)
+ if CONF.service_available.glance:
+ # Check if glance v1 is available to determine which client to use.
+ if CONF.image_feature_enabled.api_v1:
+ cls.image_client = cls.os_primary.image_client
+ elif CONF.image_feature_enabled.api_v2:
+ cls.image_client = cls.os_primary.image_client_v2
+ else:
+ raise lib_exc.InvalidConfiguration(
+ 'Either api_v1 or api_v2 must be True in '
+ '[image-feature-enabled].')
# Compute image client
- cls.images_client = cls.manager.images_client
- cls.keypairs_client = cls.manager.keypairs_client
+ cls.compute_images_client = cls.os_primary.compute_images_client
+ cls.keypairs_client = cls.os_primary.keypairs_client
# Nova security groups client
cls.compute_security_groups_client = (
- cls.manager.compute_security_groups_client)
- cls.security_group_rules_client = (
- cls.manager.security_group_rules_client)
- cls.servers_client = cls.manager.servers_client
- cls.interface_client = cls.manager.interfaces_client
+ cls.os_primary.compute_security_groups_client)
+ cls.compute_security_group_rules_client = (
+ cls.os_primary.compute_security_group_rules_client)
+ cls.servers_client = cls.os_primary.servers_client
+ cls.interface_client = cls.os_primary.interfaces_client
# Neutron network client
- cls.network_client = cls.manager.network_client
- cls.networks_client = cls.manager.networks_client
- cls.ports_client = cls.manager.ports_client
- cls.subnets_client = cls.manager.subnets_client
- cls.floating_ips_client = cls.manager.floating_ips_client
- cls.security_groups_client = cls.manager.security_groups_client
- # Heat client
- cls.orchestration_client = cls.manager.orchestration_client
-
- if CONF.volume_feature_enabled.api_v1:
- cls.volumes_client = cls.manager.volumes_client
- cls.snapshots_client = cls.manager.snapshots_client
- else:
- cls.volumes_client = cls.manager.volumes_v2_client
- cls.snapshots_client = cls.manager.snapshots_v2_client
-
- # ## Methods to handle sync and async deletes
-
- def setUp(self):
- super(ScenarioTest, self).setUp()
- self.cleanup_waits = []
- # NOTE(mtreinish) This is safe to do in setUp instead of setUp class
- # because scenario tests in the same test class should not share
- # resources. If resources were shared between test cases then it
- # should be a single scenario test instead of multiples.
-
- # NOTE(yfried): this list is cleaned at the end of test_methods and
- # not at the end of the class
- self.addCleanup(self._wait_for_cleanups)
-
- def delete_wrapper(self, delete_thing, *args, **kwargs):
- """Ignores NotFound exceptions for delete operations.
-
- @param delete_thing: delete method of a resource. method will be
- executed as delete_thing(*args, **kwargs)
-
- """
- try:
- # Tempest clients return dicts, so there is no common delete
- # method available. Using a callable instead
- delete_thing(*args, **kwargs)
- except lib_exc.NotFound:
- # If the resource is already missing, mission accomplished.
- pass
-
- def addCleanup_with_wait(self, waiter_callable, thing_id, thing_id_param,
- cleanup_callable, cleanup_args=None,
- cleanup_kwargs=None, waiter_client=None):
- """Adds wait for async resource deletion at the end of cleanups
-
- @param waiter_callable: callable to wait for the resource to delete
- with the following waiter_client if specified.
- @param thing_id: the id of the resource to be cleaned-up
- @param thing_id_param: the name of the id param in the waiter
- @param cleanup_callable: method to load pass to self.addCleanup with
- the following *cleanup_args, **cleanup_kwargs.
- usually a delete method.
- """
- if cleanup_args is None:
- cleanup_args = []
- if cleanup_kwargs is None:
- cleanup_kwargs = {}
- self.addCleanup(cleanup_callable, *cleanup_args, **cleanup_kwargs)
- wait_dict = {
- 'waiter_callable': waiter_callable,
- thing_id_param: thing_id
- }
- if waiter_client:
- wait_dict['client'] = waiter_client
- self.cleanup_waits.append(wait_dict)
-
- def _wait_for_cleanups(self):
- # To handle async delete actions, a list of waits is added
- # which will be iterated over as the last step of clearing the
- # cleanup queue. That way all the delete calls are made up front
- # and the tests won't succeed unless the deletes are eventually
- # successful. This is the same basic approach used in the api tests to
- # limit cleanup execution time except here it is multi-resource,
- # because of the nature of the scenario tests.
- for wait in self.cleanup_waits:
- waiter_callable = wait.pop('waiter_callable')
- waiter_callable(**wait)
+ cls.networks_client = cls.os_primary.networks_client
+ cls.ports_client = cls.os_primary.ports_client
+ cls.routers_client = cls.os_primary.routers_client
+ cls.subnets_client = cls.os_primary.subnets_client
+ cls.floating_ips_client = cls.os_primary.floating_ips_client
+ cls.security_groups_client = cls.os_primary.security_groups_client
+ cls.security_group_rules_client = (
+ cls.os_primary.security_group_rules_client)
+ # Use the latest available volume clients
+ 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
#
# The create_[resource] functions only return body and discard the
# resp part which is not used in scenario tests
+ def create_port(self, network_id, client=None, **kwargs):
+ if not client:
+ client = self.ports_client
+ name = data_utils.rand_name(self.__class__.__name__)
+ result = client.create_port(
+ name=name,
+ network_id=network_id,
+ **kwargs)
+ port = result['port']
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ client.delete_port, port['id'])
+ return port
+
def create_keypair(self, client=None):
if not client:
client = self.keypairs_client
@@ -159,9 +112,9 @@
self.addCleanup(client.delete_keypair, name)
return body['keypair']
- def create_server(self, name=None, image=None, flavor=None,
- validatable=False, wait_until=None,
- wait_on_delete=True, clients=None, **kwargs):
+ def create_server(self, name=None, image_id=None, flavor=None,
+ validatable=False, wait_until='ACTIVE',
+ clients=None, **kwargs):
"""Wrapper utility that returns a test server.
This wrapper utility calls the common create test server and
@@ -181,22 +134,31 @@
# Needed for the cross_tenant_traffic test:
if clients is None:
- clients = self.manager
+ clients = self.os_primary
+
+ if name is None:
+ name = data_utils.rand_name(self.__class__.__name__ + "-server")
vnic_type = CONF.network.port_vnic_type
+ profile = CONF.network.port_profile
- # If vnic_type is configured create port for
+ # If vnic_type or profile are configured create port for
# every network
- if vnic_type:
+ if vnic_type or profile:
ports = []
- networks = []
- create_port_body = {'binding:vnic_type': vnic_type,
- 'namestart': 'port-smoke'}
+ create_port_body = {}
+
+ if vnic_type:
+ create_port_body['binding:vnic_type'] = vnic_type
+
+ if profile:
+ create_port_body['binding:profile'] = profile
+
if kwargs:
# Convert security group names to security group ids
# to pass to create_port
if 'security_groups' in kwargs:
- security_groups =\
+ security_groups = \
clients.security_groups_client.list_security_groups(
).get('security_groups')
sec_dict = dict([(s['name'], s['id'])
@@ -210,86 +172,149 @@
if security_groups_ids:
create_port_body[
'security_groups'] = security_groups_ids
- networks = kwargs.pop('networks')
+ networks = kwargs.pop('networks', [])
+ else:
+ networks = []
# If there are no networks passed to us we look up
- # for the tenant's private networks and create a port
- # if there is only one private network. The same behaviour
- # as we would expect when passing the call to the clients
- # with no networks
+ # for the project's private networks and create a port.
+ # The same behaviour as we would expect when passing
+ # the call to the clients with no networks
if not networks:
networks = clients.networks_client.list_networks(
- filters={'router:external': False})
- self.assertEqual(1, len(networks),
- "There is more than one"
- " network for the tenant")
+ **{'router:external': False, 'fields': 'id'})['networks']
+
+ # It's net['uuid'] if networks come from kwargs
+ # and net['id'] if they come from
+ # clients.networks_client.list_networks
for net in networks:
- net_id = net['uuid']
- port = self._create_port(network_id=net_id,
- client=clients.ports_client,
- **create_port_body)
- ports.append({'port': port.id})
+ net_id = net.get('uuid', net.get('id'))
+ if 'port' not in net:
+ port = self.create_port(network_id=net_id,
+ client=clients.ports_client,
+ **create_port_body)
+ ports.append({'port': port['id']})
+ else:
+ ports.append({'port': net['port']})
if ports:
kwargs['networks'] = ports
self.ports = ports
tenant_network = self.get_tenant_network()
- body, servers = compute.create_test_server(
+ body, _ = compute.create_test_server(
clients,
tenant_network=tenant_network,
wait_until=wait_until,
- **kwargs)
+ name=name, flavor=flavor,
+ image_id=image_id, **kwargs)
- # TODO(jlanoux) Move wait_on_delete in compute.py
- if wait_on_delete:
- self.addCleanup(waiters.wait_for_server_termination,
- clients.servers_client,
- body['id'])
-
- self.addCleanup_with_wait(
- waiter_callable=waiters.wait_for_server_termination,
- thing_id=body['id'], thing_id_param='server_id',
- cleanup_callable=self.delete_wrapper,
- cleanup_args=[clients.servers_client.delete_server, body['id']],
- waiter_client=clients.servers_client)
+ self.addCleanup(waiters.wait_for_server_termination,
+ clients.servers_client, body['id'])
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ clients.servers_client.delete_server, body['id'])
server = clients.servers_client.show_server(body['id'])['server']
return server
def create_volume(self, size=None, name=None, snapshot_id=None,
- imageRef=None, volume_type=None, wait_on_delete=True):
+ imageRef=None, volume_type=None):
+ if size is None:
+ size = CONF.volume.volume_size
+ if imageRef:
+ image = self.compute_images_client.show_image(imageRef)['image']
+ min_disk = image.get('minDisk')
+ size = max(size, min_disk)
if name is None:
- name = data_utils.rand_name(self.__class__.__name__)
- volume = self.volumes_client.create_volume(
- size=size, display_name=name, snapshot_id=snapshot_id,
- imageRef=imageRef, volume_type=volume_type)['volume']
+ name = data_utils.rand_name(self.__class__.__name__ + "-volume")
+ kwargs = {'display_name': name,
+ 'snapshot_id': snapshot_id,
+ 'imageRef': imageRef,
+ 'volume_type': volume_type,
+ 'size': size}
+ volume = self.volumes_client.create_volume(**kwargs)['volume']
- if wait_on_delete:
- self.addCleanup(self.volumes_client.wait_for_resource_deletion,
- volume['id'])
- self.addCleanup(self.delete_wrapper,
- self.volumes_client.delete_volume, volume['id'])
- else:
- self.addCleanup_with_wait(
- waiter_callable=self.volumes_client.wait_for_resource_deletion,
- thing_id=volume['id'], thing_id_param='id',
- cleanup_callable=self.delete_wrapper,
- cleanup_args=[self.volumes_client.delete_volume, volume['id']])
-
- # NOTE(e0ne): Cinder API v2 uses name instead of display_name
- if 'display_name' in volume:
- self.assertEqual(name, volume['display_name'])
- else:
- self.assertEqual(name, volume['name'])
- self.volumes_client.wait_for_volume_status(volume['id'], 'available')
+ self.addCleanup(self.volumes_client.wait_for_resource_deletion,
+ volume['id'])
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.volumes_client.delete_volume, volume['id'])
+ self.assertEqual(name, volume['name'])
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume['id'], 'available')
# The volume retrieved on creation has a non-up-to-date status.
# Retrieval after it becomes active ensures correct details.
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(
+ self.__class__.__name__ + '-snapshot')
+ snapshot = self.snapshots_client.create_snapshot(
+ volume_id=volume_id,
+ force=force,
+ display_name=name,
+ description=description,
+ metadata=metadata)['snapshot']
+ self.addCleanup(self.snapshots_client.wait_for_resource_deletion,
+ snapshot['id'])
+ self.addCleanup(self.snapshots_client.delete_snapshot, snapshot['id'])
+ waiters.wait_for_volume_resource_status(self.snapshots_client,
+ snapshot['id'], 'available')
+ snapshot = self.snapshots_client.show_snapshot(
+ snapshot['id'])['snapshot']
+ return snapshot
+
+ def create_volume_type(self, client=None, name=None, backend_name=None):
+ if not client:
+ client = self.os_admin.volume_types_v2_client
+ 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)
+ extra_specs = {}
+ if backend_name:
+ extra_specs = {"volume_backend_name": backend_name}
+
+ volume_type = client.create_volume_type(
+ name=randomized_name, extra_specs=extra_specs)['volume_type']
+ self.addCleanup(client.delete_volume_type, volume_type['id'])
+ return volume_type
+
def _create_loginable_secgroup_rule(self, secgroup_id=None):
_client = self.compute_security_groups_client
- _client_rules = self.security_group_rules_client
+ _client_rules = self.compute_security_group_rules_client
if secgroup_id is None:
sgs = _client.list_security_groups()['security_groups']
for sg in sgs:
@@ -320,9 +345,6 @@
for ruleset in rulesets:
sg_rule = _client_rules.create_security_group_rule(
parent_group_id=secgroup_id, **ruleset)['security_group_rule']
- self.addCleanup(self.delete_wrapper,
- _client_rules.delete_security_group_rule,
- sg_rule['id'])
rules.append(sg_rule)
return rules
@@ -335,7 +357,7 @@
self.assertEqual(secgroup['name'], sg_name)
self.assertEqual(secgroup['description'], sg_desc)
self.addCleanup(
- self.delete_wrapper,
+ test_utils.call_and_ignore_notfound_exc,
self.compute_security_groups_client.delete_security_group,
secgroup['id'])
@@ -344,29 +366,17 @@
return secgroup
- def get_remote_client(self, server_or_ip, username=None, private_key=None,
- log_console_of_servers=None):
+ def get_remote_client(self, ip_address, username=None, private_key=None,
+ server=None):
"""Get a SSH client to a remote server
- @param server_or_ip a server object as returned by Tempest compute
- client or an IP address to connect to
+ @param ip_address the server floating or fixed IP address to use
+ for ssh validation
@param username name of the Linux account on the remote server
@param private_key the SSH private key to use
- @param log_console_of_servers a list of server objects. Each server
- in the list will have its console printed in the logs in case the
- SSH connection failed to be established
+ @param server: server dict, used for debugging purposes
@return a RemoteClient object
"""
- if isinstance(server_or_ip, six.string_types):
- ip = server_or_ip
- else:
- addrs = server_or_ip['addresses'][CONF.validation.network_for_ssh]
- try:
- ip = (addr['addr'] for addr in addrs if
- netaddr.valid_ipv4(addr['addr'])).next()
- except StopIteration:
- raise lib_exc.NotFound("No IPv4 addresses to use for SSH to "
- "remote server.")
if username is None:
username = CONF.validation.image_ssh_user
@@ -379,24 +389,10 @@
else:
password = CONF.validation.image_ssh_password
private_key = None
- linux_client = remote_client.RemoteClient(ip, username,
- pkey=private_key,
- password=password)
- try:
- linux_client.validate_authentication()
- except Exception as e:
- message = ('Initializing SSH connection to %(ip)s failed. '
- 'Error: %(error)s' % {'ip': ip, 'error': e})
- caller = misc_utils.find_test_caller()
- if caller:
- message = '(%s) %s' % (caller, message)
- LOG.exception(message)
- # If we don't explicitly set for which servers we want to
- # log the console output then all the servers will be logged.
- # See the definition of _log_console_output()
- self._log_console_output(log_console_of_servers)
- raise
-
+ linux_client = remote_client.RemoteClient(
+ ip_address, username, pkey=private_key, password=password,
+ server=server, servers_client=self.servers_client)
+ linux_client.validate_authentication()
return linux_client
def _image_create(self, name, fmt, path,
@@ -404,19 +400,28 @@
if properties is None:
properties = {}
name = data_utils.rand_name('%s-' % name)
- image_file = open(path, 'rb')
- self.addCleanup(image_file.close)
params = {
'name': name,
'container_format': fmt,
'disk_format': disk_format or fmt,
- 'is_public': 'False',
}
- params['properties'] = properties
- image = self.image_client.create_image(**params)['image']
+ if CONF.image_feature_enabled.api_v1:
+ params['is_public'] = 'False'
+ params['properties'] = properties
+ params = {'headers': common_image.image_meta_to_headers(**params)}
+ else:
+ params['visibility'] = 'private'
+ # Additional properties are flattened out in the v2 API.
+ params.update(properties)
+ body = self.image_client.create_image(**params)
+ image = body['image'] if 'image' in body else body
self.addCleanup(self.image_client.delete_image, image['id'])
self.assertEqual("queued", image['status'])
- self.image_client.update_image(image['id'], data=image_file)
+ with open(path, 'rb') as image_file:
+ if CONF.image_feature_enabled.api_v1:
+ self.image_client.update_image(image['id'], data=image_file)
+ else:
+ self.image_client.store_image_file(image['id'], image_file)
return image['id']
def glance_image_create(self):
@@ -427,10 +432,10 @@
img_container_format = CONF.scenario.img_container_format
img_disk_format = CONF.scenario.img_disk_format
img_properties = CONF.scenario.img_properties
- LOG.debug("paths: img: %s, container_fomat: %s, disk_format: %s, "
- "properties: %s, ami: %s, ari: %s, aki: %s" %
- (img_path, img_container_format, img_disk_format,
- img_properties, ami_img_path, ari_img_path, aki_img_path))
+ LOG.debug("paths: img: %s, container_format: %s, disk_format: %s, "
+ "properties: %s, ami: %s, ari: %s, aki: %s",
+ img_path, img_container_format, img_disk_format,
+ img_properties, ami_img_path, ari_img_path, aki_img_path)
try:
image = self._image_create('scenario-img',
img_container_format,
@@ -447,22 +452,27 @@
image = self._image_create('scenario-ami', 'ami',
path=ami_img_path,
properties=properties)
- LOG.debug("image:%s" % image)
+ LOG.debug("image:%s", image)
return image
- def _log_console_output(self, servers=None):
+ def _log_console_output(self, servers=None, client=None):
if not CONF.compute_feature_enabled.console_output:
LOG.debug('Console output not supported, cannot log')
return
+ client = client or self.servers_client
if not servers:
- servers = self.servers_client.list_servers()
+ servers = client.list_servers()
servers = servers['servers']
for server in servers:
- console_output = self.servers_client.get_console_output(
- server['id'])['output']
- LOG.debug('Console output for %s\nbody=\n%s',
- server['id'], console_output)
+ try:
+ console_output = client.get_console_output(
+ server['id'])['output']
+ LOG.debug('Console output for %s\nbody=\n%s',
+ server['id'], console_output)
+ except lib_exc.NotFound:
+ LOG.debug("Server %s disappeared(deleted) while looking "
+ "for the console log", server['id'])
def _log_net_info(self, exc):
# network debug is called as part of ssh init
@@ -473,21 +483,30 @@
# Glance client
_image_client = self.image_client
# Compute client
- _images_client = self.images_client
+ _images_client = self.compute_images_client
if name is None:
- name = data_utils.rand_name('scenario-snapshot')
+ name = data_utils.rand_name(self.__class__.__name__ + 'snapshot')
LOG.debug("Creating a snapshot image for server: %s", server['name'])
image = _images_client.create_image(server['id'], name=name)
image_id = image.response['location'].split('images/')[1]
- _image_client.wait_for_image_status(image_id, 'active')
- self.addCleanup_with_wait(
- waiter_callable=_image_client.wait_for_resource_deletion,
- thing_id=image_id, thing_id_param='id',
- cleanup_callable=self.delete_wrapper,
- cleanup_args=[_image_client.delete_image, image_id])
- snapshot_image = _image_client.get_image_meta(image_id)
+ waiters.wait_for_image_status(_image_client, image_id, 'active')
- bdm = snapshot_image.get('properties', {}).get('block_device_mapping')
+ self.addCleanup(_image_client.wait_for_resource_deletion,
+ image_id)
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ _image_client.delete_image, image_id)
+
+ if CONF.image_feature_enabled.api_v1:
+ # In glance v1 the additional properties are stored in the headers.
+ resp = _image_client.check_image(image_id)
+ snapshot_image = common_image.get_image_meta_from_headers(resp)
+ image_props = snapshot_image.get('properties', {})
+ else:
+ # In glance v2 the additional properties are flattened.
+ snapshot_image = _image_client.show_image(image_id)
+ image_props = snapshot_image
+
+ bdm = image_props.get('block_device_mapping')
if bdm:
bdm = json.loads(bdm)
if bdm and 'snapshot_id' in bdm[0]:
@@ -495,12 +514,12 @@
self.addCleanup(
self.snapshots_client.wait_for_resource_deletion,
snapshot_id)
- self.addCleanup(
- self.delete_wrapper, self.snapshots_client.delete_snapshot,
- snapshot_id)
- self.snapshots_client.wait_for_snapshot_status(snapshot_id,
- 'available')
-
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.snapshots_client.delete_snapshot,
+ snapshot_id)
+ waiters.wait_for_volume_resource_status(self.snapshots_client,
+ snapshot_id,
+ 'available')
image_name = snapshot_image['name']
self.assertEqual(name, image_name)
LOG.debug("Created snapshot image %s for server %s",
@@ -512,40 +531,30 @@
server['id'], volumeId=volume_to_attach['id'], device='/dev/%s'
% CONF.compute.volume_device_name)['volumeAttachment']
self.assertEqual(volume_to_attach['id'], volume['id'])
- self.volumes_client.wait_for_volume_status(volume['id'], 'in-use')
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume['id'], 'in-use')
# Return the updated volume after the attachment
return self.volumes_client.show_volume(volume['id'])['volume']
def nova_volume_detach(self, server, volume):
self.servers_client.detach_volume(server['id'], volume['id'])
- self.volumes_client.wait_for_volume_status(volume['id'], 'available')
-
- volume = self.volumes_client.show_volume(volume['id'])['volume']
- self.assertEqual('available', volume['status'])
-
- def rebuild_server(self, server_id, image=None,
- preserve_ephemeral=False, wait=True,
- rebuild_kwargs=None):
- if image is None:
- image = CONF.compute.image_ref
-
- rebuild_kwargs = rebuild_kwargs or {}
-
- LOG.debug("Rebuilding server (id: %s, image: %s, preserve eph: %s)",
- server_id, image, preserve_ephemeral)
- self.servers_client.rebuild_server(
- server_id=server_id, image_ref=image,
- preserve_ephemeral=preserve_ephemeral,
- **rebuild_kwargs)
- if wait:
- waiters.wait_for_server_status(self.servers_client,
- server_id, 'ACTIVE')
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume['id'], 'available')
def ping_ip_address(self, ip_address, should_succeed=True,
- ping_timeout=None):
+ ping_timeout=None, mtu=None):
timeout = ping_timeout or CONF.validation.ping_timeout
- cmd = ['ping', '-c1', '-w1', ip_address]
+ cmd = ['ping', '-c1', '-w1']
+
+ if mtu:
+ cmd += [
+ # don't fragment
+ '-M', 'do',
+ # ping receives just the size of ICMP payload
+ '-s', str(net_utils.get_ping_payload_size(mtu, 4))
+ ]
+ cmd.append(ip_address)
def ping():
proc = subprocess.Popen(cmd,
@@ -555,16 +564,16 @@
return (proc.returncode == 0) == should_succeed
- caller = misc_utils.find_test_caller()
+ caller = test_utils.find_test_caller()
LOG.debug('%(caller)s begins to ping %(ip)s in %(timeout)s sec and the'
- ' expected result is %(should_succeed)s' % {
+ ' expected result is %(should_succeed)s', {
'caller': caller, 'ip': ip_address, 'timeout': timeout,
'should_succeed':
'reachable' if should_succeed else 'unreachable'
})
- result = tempest.test.call_until_true(ping, timeout, 1)
+ result = test_utils.call_until_true(ping, timeout, 1)
LOG.debug('%(caller)s finishes ping %(ip)s in %(timeout)s sec and the '
- 'ping result is %(result)s' % {
+ 'ping result is %(result)s', {
'caller': caller, 'ip': ip_address, 'timeout': timeout,
'result': 'expected' if result else 'unexpected'
})
@@ -573,7 +582,8 @@
def check_vm_connectivity(self, ip_address,
username=None,
private_key=None,
- should_connect=True):
+ should_connect=True,
+ mtu=None):
"""Check server connectivity
:param ip_address: server to test against
@@ -582,6 +592,7 @@
:param should_connect: True/False indicates positive/negative test
positive - attempt ping and ssh
negative - attempt ping and fail if succeed
+ :param mtu: network MTU to use for connectivity validation
:raises: AssertError if the result of the connectivity check does
not match the value of the should_connect param
@@ -591,7 +602,8 @@
else:
msg = "ip address %s is reachable" % ip_address
self.assertTrue(self.ping_ip_address(ip_address,
- should_succeed=should_connect),
+ should_succeed=should_connect,
+ mtu=mtu),
msg=msg)
if should_connect:
# no need to check ssh for negative connectivity
@@ -599,16 +611,17 @@
def check_public_network_connectivity(self, ip_address, username,
private_key, should_connect=True,
- msg=None, servers=None):
+ msg=None, servers=None, mtu=None):
# The target login is assumed to have been configured for
# key-based authentication by cloud-init.
- LOG.debug('checking network connections to IP %s with user: %s' %
- (ip_address, username))
+ LOG.debug('checking network connections to IP %s with user: %s',
+ ip_address, username)
try:
self.check_vm_connectivity(ip_address,
username,
private_key,
- should_connect=should_connect)
+ should_connect=should_connect,
+ mtu=mtu)
except Exception:
ex_msg = 'Public network connectivity check failed'
if msg:
@@ -620,48 +633,73 @@
def create_floating_ip(self, thing, pool_name=None):
"""Create a floating IP and associates to a server on Nova"""
+ if not pool_name:
+ pool_name = CONF.network.floating_network_name
floating_ip = (self.compute_floating_ips_client.
create_floating_ip(pool=pool_name)['floating_ip'])
- self.addCleanup(self.delete_wrapper,
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
self.compute_floating_ips_client.delete_floating_ip,
floating_ip['id'])
self.compute_floating_ips_client.associate_floating_ip_to_server(
floating_ip['ip'], thing['id'])
return floating_ip
- def create_timestamp(self, server_or_ip, dev_name=None, mount_path='/mnt',
+ def create_timestamp(self, ip_address, dev_name=None, mount_path='/mnt',
private_key=None):
- ssh_client = self.get_remote_client(server_or_ip,
+ ssh_client = self.get_remote_client(ip_address,
private_key=private_key)
if dev_name is not None:
ssh_client.make_fs(dev_name)
- ssh_client.mount(dev_name, mount_path)
+ ssh_client.exec_command('sudo mount /dev/%s %s' % (dev_name,
+ mount_path))
cmd_timestamp = 'sudo sh -c "date > %s/timestamp; sync"' % mount_path
ssh_client.exec_command(cmd_timestamp)
timestamp = ssh_client.exec_command('sudo cat %s/timestamp'
% mount_path)
if dev_name is not None:
- ssh_client.umount(mount_path)
+ ssh_client.exec_command('sudo umount %s' % mount_path)
return timestamp
- def get_timestamp(self, server_or_ip, dev_name=None, mount_path='/mnt',
+ def get_timestamp(self, ip_address, dev_name=None, mount_path='/mnt',
private_key=None):
- ssh_client = self.get_remote_client(server_or_ip,
+ ssh_client = self.get_remote_client(ip_address,
private_key=private_key)
if dev_name is not None:
ssh_client.mount(dev_name, mount_path)
timestamp = ssh_client.exec_command('sudo cat %s/timestamp'
% mount_path)
if dev_name is not None:
- ssh_client.umount(mount_path)
+ ssh_client.exec_command('sudo umount %s' % mount_path)
return timestamp
- def get_server_or_ip(self, server):
+ def get_server_ip(self, server):
+ """Get the server fixed or floating IP.
+
+ Based on the configuration we're in, return a correct ip
+ address for validating that a guest is up.
+ """
if CONF.validation.connect_method == 'floating':
- ip = self.create_floating_ip(server)['ip']
+ # The tests calling this method don't have a floating IP
+ # and can't make use of the validation resources. So the
+ # method is creating the floating IP there.
+ return self.create_floating_ip(server)['ip']
+ elif CONF.validation.connect_method == 'fixed':
+ # Determine the network name to look for based on config or creds
+ # provider network resources.
+ if CONF.validation.network_for_ssh:
+ addresses = server['addresses'][
+ CONF.validation.network_for_ssh]
+ else:
+ network = self.get_tenant_network()
+ addresses = (server['addresses'][network['name']]
+ if network else [])
+ for address in addresses:
+ 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:
- ip = server
- return ip
+ raise lib_exc.InvalidConfiguration()
class NetworkScenarioTest(ScenarioTest):
@@ -684,65 +722,35 @@
if not CONF.service_available.neutron:
raise cls.skipException('Neutron not available')
- @classmethod
- def resource_setup(cls):
- super(NetworkScenarioTest, cls).resource_setup()
- cls.tenant_id = cls.manager.identity_client.tenant_id
-
- def _create_network(self, client=None, networks_client=None,
- tenant_id=None, namestart='network-smoke-'):
- if not client:
- client = self.network_client
+ def _create_network(self, networks_client=None,
+ tenant_id=None,
+ namestart='network-smoke-',
+ port_security_enabled=True):
if not networks_client:
networks_client = self.networks_client
if not tenant_id:
- tenant_id = client.tenant_id
+ tenant_id = networks_client.tenant_id
name = data_utils.rand_name(namestart)
- result = networks_client.create_network(name=name, tenant_id=tenant_id)
- network = net_resources.DeletableNetwork(
- networks_client=networks_client, **result['network'])
- self.assertEqual(network.name, name)
- self.addCleanup(self.delete_wrapper, network.delete)
+ network_kwargs = dict(name=name, tenant_id=tenant_id)
+ # Neutron disables port security by default so we have to check the
+ # config before trying to create the network with port_security_enabled
+ if CONF.network_feature_enabled.port_security:
+ network_kwargs['port_security_enabled'] = port_security_enabled
+ result = networks_client.create_network(**network_kwargs)
+ network = result['network']
+
+ self.assertEqual(network['name'], name)
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ networks_client.delete_network,
+ network['id'])
return network
- def _list_networks(self, *args, **kwargs):
- """List networks using admin creds """
- networks_list = self.admin_manager.networks_client.list_networks(
- *args, **kwargs)
- return networks_list['networks']
-
- def _list_subnets(self, *args, **kwargs):
- """List subnets using admin creds """
- subnets_list = self.admin_manager.subnets_client.list_subnets(
- *args, **kwargs)
- return subnets_list['subnets']
-
- def _list_routers(self, *args, **kwargs):
- """List routers using admin creds """
- routers_list = self.admin_manager.network_client.list_routers(
- *args, **kwargs)
- return routers_list['routers']
-
- def _list_ports(self, *args, **kwargs):
- """List ports using admin creds """
- ports_list = self.admin_manager.ports_client.list_ports(
- *args, **kwargs)
- return ports_list['ports']
-
- def _list_agents(self, *args, **kwargs):
- """List agents using admin creds """
- agents_list = self.admin_manager.network_agents_client.list_agents(
- *args, **kwargs)
- return agents_list['agents']
-
- def _create_subnet(self, network, client=None, subnets_client=None,
- namestart='subnet-smoke', **kwargs):
+ def create_subnet(self, network, subnets_client=None,
+ namestart='subnet-smoke', **kwargs):
"""Create a subnet for the given network
within the cidr block configured for tenant networks.
"""
- if not client:
- client = self.network_client
if not subnets_client:
subnets_client = self.subnets_client
@@ -752,18 +760,19 @@
:returns: True if subnet with cidr already exist in tenant
False else
"""
- cidr_in_use = self._list_subnets(tenant_id=tenant_id, cidr=cidr)
+ cidr_in_use = self.os_admin.subnets_client.list_subnets(
+ tenant_id=tenant_id, cidr=cidr)['subnets']
return len(cidr_in_use) != 0
ip_version = kwargs.pop('ip_version', 4)
if ip_version == 6:
tenant_cidr = netaddr.IPNetwork(
- CONF.network.tenant_network_v6_cidr)
- num_bits = CONF.network.tenant_network_v6_mask_bits
+ CONF.network.project_network_v6_cidr)
+ num_bits = CONF.network.project_network_v6_mask_bits
else:
- tenant_cidr = netaddr.IPNetwork(CONF.network.tenant_network_cidr)
- num_bits = CONF.network.tenant_network_mask_bits
+ tenant_cidr = netaddr.IPNetwork(CONF.network.project_network_cidr)
+ num_bits = CONF.network.project_network_mask_bits
result = None
str_cidr = None
@@ -771,13 +780,13 @@
# blocks until an unallocated block is found.
for subnet_cidr in tenant_cidr.subnet(num_bits):
str_cidr = str(subnet_cidr)
- if cidr_in_use(str_cidr, tenant_id=network.tenant_id):
+ if cidr_in_use(str_cidr, tenant_id=network['tenant_id']):
continue
subnet = dict(
name=data_utils.rand_name(namestart),
- network_id=network.id,
- tenant_id=network.tenant_id,
+ network_id=network['id'],
+ tenant_id=network['tenant_id'],
cidr=str_cidr,
ip_version=ip_version,
**kwargs
@@ -790,40 +799,38 @@
if not is_overlapping_cidr:
raise
self.assertIsNotNone(result, 'Unable to allocate tenant network')
- subnet = net_resources.DeletableSubnet(
- network_client=client, subnets_client=subnets_client,
- **result['subnet'])
- self.assertEqual(subnet.cidr, str_cidr)
- self.addCleanup(self.delete_wrapper, subnet.delete)
+
+ subnet = result['subnet']
+ self.assertEqual(subnet['cidr'], str_cidr)
+
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ subnets_client.delete_subnet, subnet['id'])
+
return subnet
- def _create_port(self, network_id, client=None, namestart='port-quotatest',
- **kwargs):
- if not client:
- client = self.ports_client
- name = data_utils.rand_name(namestart)
- result = client.create_port(
- name=name,
- network_id=network_id,
- **kwargs)
- self.assertIsNotNone(result, 'Unable to allocate port')
- port = net_resources.DeletablePort(ports_client=client,
- **result['port'])
- self.addCleanup(self.delete_wrapper, port.delete)
- return port
-
def _get_server_port_id_and_ip4(self, server, ip_addr=None):
- ports = self._list_ports(device_id=server['id'], status='ACTIVE',
- fixed_ip=ip_addr)
- # A port can have more then one IP address in some cases.
+ ports = self.os_admin.ports_client.list_ports(
+ device_id=server['id'], fixed_ip=ip_addr)['ports']
+ # A port can have more than one IP address in some cases.
# If the network is dual-stack (IPv4 + IPv6), this port is associated
# with 2 subnets
+ p_status = ['ACTIVE']
+ # NOTE(vsaienko) With Ironic, instances live on separate hardware
+ # servers. Neutron does not bind ports for Ironic instances, as a
+ # result the port remains in the DOWN state.
+ # TODO(vsaienko) remove once bug: #1599836 is resolved.
+ if getattr(CONF.service_available, 'ironic', False):
+ p_status.append('DOWN')
port_map = [(p["id"], fxip["ip_address"])
for p in ports
for fxip in p["fixed_ips"]
- if netaddr.valid_ipv4(fxip["ip_address"])]
+ 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)
- self.assertNotEqual(0, len(port_map),
+ self.assertNotEmpty(port_map,
"No IPv4 addresses found in: %s" % ports)
self.assertEqual(len(port_map), 1,
"Found multiple IPv4 addresses: %s. "
@@ -832,10 +839,11 @@
return port_map[0]
def _get_network_by_name(self, network_name):
- net = self._list_networks(name=network_name)
- self.assertNotEqual(len(net), 0,
+ net = self.os_admin.networks_client.list_networks(
+ name=network_name)['networks']
+ self.assertNotEmpty(net,
"Unable to get network by name: %s" % network_name)
- return net_resources.AttributeDict(net[0])
+ return net[0]
def create_floating_ip(self, thing, external_network_id=None,
port_id=None, client=None):
@@ -854,60 +862,52 @@
tenant_id=thing['tenant_id'],
fixed_ip_address=ip4
)
- floating_ip = net_resources.DeletableFloatingIp(
- client=client,
- **result['floatingip'])
- self.addCleanup(self.delete_wrapper, floating_ip.delete)
- return floating_ip
-
- def _associate_floating_ip(self, floating_ip, server):
- port_id, _ = self._get_server_port_id_and_ip4(server)
- floating_ip.update(port_id=port_id)
- self.assertEqual(port_id, floating_ip.port_id)
- return floating_ip
-
- def _disassociate_floating_ip(self, floating_ip):
- """:param floating_ip: type DeletableFloatingIp"""
- floating_ip.update(port_id=None)
- self.assertIsNone(floating_ip.port_id)
+ floating_ip = result['floatingip']
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ client.delete_floatingip,
+ floating_ip['id'])
return floating_ip
def check_floating_ip_status(self, floating_ip, status):
"""Verifies floatingip reaches the given status
- :param floating_ip: net_resources.DeletableFloatingIp floating IP to
- to check status
+ :param dict floating_ip: floating IP dict to check status
:param status: target status
:raises: AssertionError if status doesn't match
"""
- def refresh():
- floating_ip.refresh()
- return status == floating_ip.status
+ floatingip_id = floating_ip['id']
- tempest.test.call_until_true(refresh,
- CONF.network.build_timeout,
- CONF.network.build_interval)
- self.assertEqual(status, floating_ip.status,
- message="FloatingIP: {fp} is at status: {cst}. "
- "failed to reach status: {st}"
- .format(fp=floating_ip, cst=floating_ip.status,
- st=status))
+ def refresh():
+ result = (self.floating_ips_client.
+ show_floatingip(floatingip_id)['floatingip'])
+ return status == result['status']
+
+ if not test_utils.call_until_true(refresh,
+ CONF.network.build_timeout,
+ CONF.network.build_interval):
+ floating_ip = self.floating_ips_client.show_floatingip(
+ floatingip_id)['floatingip']
+ self.assertEqual(status, floating_ip['status'],
+ message="FloatingIP: {fp} is at status: {cst}. "
+ "failed to reach status: {st}"
+ .format(fp=floating_ip, cst=floating_ip['status'],
+ st=status))
LOG.info("FloatingIP: {fp} is at status: {st}"
.format(fp=floating_ip, st=status))
- def _check_tenant_network_connectivity(self, server,
- username,
- private_key,
- should_connect=True,
- servers_for_debug=None):
- if not CONF.network.tenant_networks_reachable:
+ def check_tenant_network_connectivity(self, server,
+ username,
+ private_key,
+ should_connect=True,
+ servers_for_debug=None):
+ if not CONF.network.project_networks_reachable:
msg = 'Tenant networks not configured to be reachable.'
LOG.info(msg)
return
# The target login is assumed to have been configured for
# key-based authentication by cloud-init.
try:
- for net_name, ip_addresses in six.iteritems(server['addresses']):
+ for ip_addresses in server['addresses'].values():
for ip_address in ip_addresses:
self.check_vm_connectivity(ip_address['addr'],
username,
@@ -919,35 +919,44 @@
self._log_net_info(e)
raise
- def _check_remote_connectivity(self, source, dest, should_succeed=True,
- nic=None):
- """check ping server via source ssh connection
+ def check_remote_connectivity(self, source, dest, should_succeed=True,
+ nic=None):
+ """assert ping server via source ssh connection
:param source: RemoteClient: an ssh connection from which to ping
- :param dest: and IP to ping against
- :param should_succeed: boolean should ping succeed or not
+ :param dest: an IP to ping against
+ :param should_succeed: boolean: should ping succeed or not
:param nic: specific network interface to ping from
- :returns: boolean -- should_succeed == ping
- :returns: ping is false if ping failed
"""
def ping_remote():
try:
source.ping_host(dest, nic=nic)
except lib_exc.SSHExecCommandFailed:
LOG.warning('Failed to ping IP: %s via a ssh connection '
- 'from: %s.' % (dest, source.ssh_client.host))
+ 'from: %s.', dest, source.ssh_client.host)
return not should_succeed
return should_succeed
- return tempest.test.call_until_true(ping_remote,
- CONF.validation.ping_timeout,
- 1)
+ result = test_utils.call_until_true(ping_remote,
+ CONF.validation.ping_timeout, 1)
+ if result:
+ return
- def _create_security_group(self, client=None, tenant_id=None,
+ source_host = source.ssh_client.host
+ if should_succeed:
+ msg = "Timed out waiting for %s to become reachable from %s" \
+ % (dest, source_host)
+ else:
+ msg = "%s is reachable from %s" % (dest, source_host)
+ self._log_console_output()
+ self.fail(msg)
+
+ def _create_security_group(self, security_group_rules_client=None,
+ tenant_id=None,
namestart='secgroup-smoke',
security_groups_client=None):
- if client is None:
- client = self.network_client
+ if security_group_rules_client is None:
+ security_group_rules_client = self.security_group_rules_client
if security_groups_client is None:
security_groups_client = self.security_groups_client
if tenant_id is None:
@@ -958,11 +967,12 @@
# Add rules to the security group
rules = self._create_loginable_secgroup_rule(
- client=client, secgroup=secgroup,
+ security_group_rules_client=security_group_rules_client,
+ secgroup=secgroup,
security_groups_client=security_groups_client)
for rule in rules:
- self.assertEqual(tenant_id, rule.tenant_id)
- self.assertEqual(secgroup.id, rule.security_group_id)
+ self.assertEqual(tenant_id, rule['tenant_id'])
+ self.assertEqual(secgroup['id'], rule['security_group_id'])
return secgroup
def _create_empty_security_group(self, client=None, tenant_id=None,
@@ -974,7 +984,7 @@
- IPv6 egress to any
:param tenant_id: secgroup will be created in this tenant
- :returns: DeletableSecurityGroup -- containing the secgroup created
+ :returns: the created security group
"""
if client is None:
client = self.security_groups_client
@@ -986,35 +996,18 @@
description=sg_desc)
sg_dict['tenant_id'] = tenant_id
result = client.create_security_group(**sg_dict)
- secgroup = net_resources.DeletableSecurityGroup(
- client=client,
- **result['security_group']
- )
- self.assertEqual(secgroup.name, sg_name)
- self.assertEqual(tenant_id, secgroup.tenant_id)
- self.assertEqual(secgroup.description, sg_desc)
- self.addCleanup(self.delete_wrapper, secgroup.delete)
+
+ secgroup = result['security_group']
+ self.assertEqual(secgroup['name'], sg_name)
+ self.assertEqual(tenant_id, secgroup['tenant_id'])
+ self.assertEqual(secgroup['description'], sg_desc)
+
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ client.delete_security_group, secgroup['id'])
return secgroup
- def _default_security_group(self, client=None, tenant_id=None):
- """Get default secgroup for given tenant_id.
-
- :returns: DeletableSecurityGroup -- default secgroup for given tenant
- """
- if client is None:
- client = self.security_groups_client
- if not tenant_id:
- tenant_id = client.tenant_id
- sgs = [
- sg for sg in client.list_security_groups().values()[0]
- if sg['tenant_id'] == tenant_id and sg['name'] == 'default'
- ]
- msg = "No default security group for tenant %s." % (tenant_id)
- self.assertTrue(len(sgs) > 0, msg)
- return net_resources.DeletableSecurityGroup(client=client,
- **sgs[0])
-
- def _create_security_group_rule(self, secgroup=None, client=None,
+ def _create_security_group_rule(self, secgroup=None,
+ sec_group_rules_client=None,
tenant_id=None,
security_groups_client=None, **kwargs):
"""Create a rule from a dictionary of rule parameters.
@@ -1022,7 +1015,7 @@
Create a rule in a secgroup. if secgroup not defined will search for
default secgroup in tenant_id.
- :param secgroup: type DeletableSecurityGroup.
+ :param secgroup: the security group.
:param tenant_id: if secgroup not passed -- the tenant in which to
search for default secgroup
:param kwargs: a dictionary containing rule parameters:
@@ -1034,43 +1027,46 @@
port_range_max: 22
}
"""
- if client is None:
- client = self.network_client
+ if sec_group_rules_client is None:
+ sec_group_rules_client = self.security_group_rules_client
if security_groups_client is None:
security_groups_client = self.security_groups_client
if not tenant_id:
tenant_id = security_groups_client.tenant_id
if secgroup is None:
- secgroup = self._default_security_group(
- client=security_groups_client, tenant_id=tenant_id)
+ # Get default secgroup for tenant_id
+ default_secgroups = security_groups_client.list_security_groups(
+ name='default', tenant_id=tenant_id)['security_groups']
+ msg = "No default security group for tenant %s." % (tenant_id)
+ self.assertNotEmpty(default_secgroups, msg)
+ secgroup = default_secgroups[0]
- ruleset = dict(security_group_id=secgroup.id,
- tenant_id=secgroup.tenant_id)
+ ruleset = dict(security_group_id=secgroup['id'],
+ tenant_id=secgroup['tenant_id'])
ruleset.update(kwargs)
- sg_rule = client.create_security_group_rule(**ruleset)
- sg_rule = net_resources.DeletableSecurityGroupRule(
- client=client,
- **sg_rule['security_group_rule']
- )
- self.addCleanup(self.delete_wrapper, sg_rule.delete)
- self.assertEqual(secgroup.tenant_id, sg_rule.tenant_id)
- self.assertEqual(secgroup.id, sg_rule.security_group_id)
+ sg_rule = sec_group_rules_client.create_security_group_rule(**ruleset)
+ sg_rule = sg_rule['security_group_rule']
+
+ self.assertEqual(secgroup['tenant_id'], sg_rule['tenant_id'])
+ self.assertEqual(secgroup['id'], sg_rule['security_group_id'])
return sg_rule
- def _create_loginable_secgroup_rule(self, client=None, secgroup=None,
+ def _create_loginable_secgroup_rule(self, security_group_rules_client=None,
+ secgroup=None,
security_groups_client=None):
"""Create loginable security group rule
- These rules are intended to permit inbound ssh and icmp
- traffic from all sources, so no group_id is provided.
- Setting a group_id would only permit traffic from ports
- belonging to the same security group.
+ This function will create:
+ 1. egress and ingress tcp port 22 allow rule in order to allow ssh
+ access for ipv4.
+ 2. egress and ingress ipv6 icmp allow rule, in order to allow icmpv6.
+ 3. egress and ingress ipv4 icmp allow rule, in order to allow icmpv4.
"""
- if client is None:
- client = self.network_client
+ if security_group_rules_client is None:
+ security_group_rules_client = self.security_group_rules_client
if security_groups_client is None:
security_groups_client = self.security_groups_client
rules = []
@@ -1091,12 +1087,14 @@
ethertype='IPv6',
)
]
+ sec_group_rules_client = security_group_rules_client
for ruleset in rulesets:
for r_direction in ['ingress', 'egress']:
ruleset['direction'] = r_direction
try:
sg_rule = self._create_security_group_rule(
- client=client, secgroup=secgroup,
+ sec_group_rules_client=sec_group_rules_client,
+ secgroup=secgroup,
security_groups_client=security_groups_client,
**ruleset)
except lib_exc.Conflict as ex:
@@ -1105,17 +1103,11 @@
if msg not in ex._error_string:
raise ex
else:
- self.assertEqual(r_direction, sg_rule.direction)
+ self.assertEqual(r_direction, sg_rule['direction'])
rules.append(sg_rule)
return rules
- def _ssh_to_server(self, server, private_key):
- ssh_login = CONF.validation.image_ssh_user
- return self.get_remote_client(server,
- username=ssh_login,
- private_key=private_key)
-
def _get_router(self, client=None, tenant_id=None):
"""Retrieve a router for the given tenant id.
@@ -1126,56 +1118,41 @@
routes traffic to the public network.
"""
if not client:
- client = self.network_client
+ client = self.routers_client
if not tenant_id:
tenant_id = client.tenant_id
router_id = CONF.network.public_router_id
network_id = CONF.network.public_network_id
if router_id:
body = client.show_router(router_id)
- return net_resources.AttributeDict(**body['router'])
+ return body['router']
elif network_id:
- router = self._create_router(client, tenant_id)
- router.set_gateway(network_id)
+ router = client.create_router(
+ name=data_utils.rand_name(self.__class__.__name__ + '-router'),
+ admin_state_up=True,
+ tenant_id=tenant_id,
+ external_gateway_info=dict(network_id=network_id))['router']
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ client.delete_router, router['id'])
return router
else:
raise Exception("Neither of 'public_router_id' or "
"'public_network_id' has been defined.")
- def _create_router(self, client=None, tenant_id=None,
- namestart='router-smoke'):
- if not client:
- client = self.network_client
- if not tenant_id:
- tenant_id = client.tenant_id
- name = data_utils.rand_name(namestart)
- result = client.create_router(name=name,
- admin_state_up=True,
- tenant_id=tenant_id)
- router = net_resources.DeletableRouter(client=client,
- **result['router'])
- self.assertEqual(router.name, name)
- self.addCleanup(self.delete_wrapper, router.delete)
- return router
-
- def _update_router_admin_state(self, router, admin_state_up):
- router.update(admin_state_up=admin_state_up)
- self.assertEqual(admin_state_up, router.admin_state_up)
-
- def create_networks(self, client=None, networks_client=None,
- subnets_client=None, tenant_id=None,
- dns_nameservers=None):
+ def create_networks(self, networks_client=None,
+ routers_client=None, subnets_client=None,
+ tenant_id=None, dns_nameservers=None,
+ port_security_enabled=True):
"""Create a network with a subnet connected to a router.
The baremetal driver is a special case since all nodes are
on the same shared network.
- :param client: network client to create resources with.
:param tenant_id: id of tenant to create resources in.
:param dns_nameservers: list of dns servers to send to subnet.
:returns: network, subnet, router
"""
- if CONF.baremetal.driver_enabled:
+ if CONF.network.shared_physical_network:
# NOTE(Shrews): This exception is for environments where tenant
# credential isolation is available, but network separation is
# not (the current baremetal case). Likely can be removed when
@@ -1183,182 +1160,38 @@
# https://blueprints.launchpad.net/tempest/+spec/test-accounts
if not CONF.compute.fixed_network_name:
m = 'fixed_network_name must be specified in config'
- raise exceptions.InvalidConfiguration(m)
+ raise lib_exc.InvalidConfiguration(m)
network = self._get_network_by_name(
CONF.compute.fixed_network_name)
router = None
subnet = None
else:
network = self._create_network(
- client=client, networks_client=networks_client,
- tenant_id=tenant_id)
- router = self._get_router(client=client, tenant_id=tenant_id)
-
- subnet_kwargs = dict(network=network, client=client,
+ networks_client=networks_client,
+ tenant_id=tenant_id,
+ port_security_enabled=port_security_enabled)
+ router = self._get_router(client=routers_client,
+ tenant_id=tenant_id)
+ subnet_kwargs = dict(network=network,
subnets_client=subnets_client)
# use explicit check because empty list is a valid option
if dns_nameservers is not None:
subnet_kwargs['dns_nameservers'] = dns_nameservers
- subnet = self._create_subnet(**subnet_kwargs)
- subnet.add_to_router(router.id)
+ subnet = self.create_subnet(**subnet_kwargs)
+ if not routers_client:
+ routers_client = self.routers_client
+ router_id = router['id']
+ routers_client.add_router_interface(router_id,
+ subnet_id=subnet['id'])
+
+ # save a cleanup job to remove this association between
+ # router and subnet
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ routers_client.remove_router_interface, router_id,
+ subnet_id=subnet['id'])
return network, subnet, router
-# power/provision states as of icehouse
-class BaremetalPowerStates(object):
- """Possible power states of an Ironic node."""
- POWER_ON = 'power on'
- POWER_OFF = 'power off'
- REBOOT = 'rebooting'
- SUSPEND = 'suspended'
-
-
-class BaremetalProvisionStates(object):
- """Possible provision states of an Ironic node."""
- NOSTATE = None
- INIT = 'initializing'
- ACTIVE = 'active'
- BUILDING = 'building'
- DEPLOYWAIT = 'wait call-back'
- DEPLOYING = 'deploying'
- DEPLOYFAIL = 'deploy failed'
- DEPLOYDONE = 'deploy complete'
- DELETING = 'deleting'
- DELETED = 'deleted'
- ERROR = 'error'
-
-
-class BaremetalScenarioTest(ScenarioTest):
-
- credentials = ['primary', 'admin']
-
- @classmethod
- def skip_checks(cls):
- super(BaremetalScenarioTest, cls).skip_checks()
- if (not CONF.service_available.ironic or
- not CONF.baremetal.driver_enabled):
- msg = 'Ironic not available or Ironic compute driver not enabled'
- raise cls.skipException(msg)
-
- @classmethod
- def setup_clients(cls):
- super(BaremetalScenarioTest, cls).setup_clients()
-
- cls.baremetal_client = cls.admin_manager.baremetal_client
-
- @classmethod
- def resource_setup(cls):
- super(BaremetalScenarioTest, cls).resource_setup()
- # allow any issues obtaining the node list to raise early
- cls.baremetal_client.list_nodes()
-
- def _node_state_timeout(self, node_id, state_attr,
- target_states, timeout=10, interval=1):
- if not isinstance(target_states, list):
- target_states = [target_states]
-
- def check_state():
- node = self.get_node(node_id=node_id)
- if node.get(state_attr) in target_states:
- return True
- return False
-
- if not tempest.test.call_until_true(
- check_state, timeout, interval):
- msg = ("Timed out waiting for node %s to reach %s state(s) %s" %
- (node_id, state_attr, target_states))
- raise exceptions.TimeoutException(msg)
-
- def wait_provisioning_state(self, node_id, state, timeout):
- self._node_state_timeout(
- node_id=node_id, state_attr='provision_state',
- target_states=state, timeout=timeout)
-
- def wait_power_state(self, node_id, state):
- self._node_state_timeout(
- node_id=node_id, state_attr='power_state',
- target_states=state, timeout=CONF.baremetal.power_timeout)
-
- def wait_node(self, instance_id):
- """Waits for a node to be associated with instance_id."""
-
- def _get_node():
- node = None
- try:
- node = self.get_node(instance_id=instance_id)
- except lib_exc.NotFound:
- pass
- return node is not None
-
- if not tempest.test.call_until_true(
- _get_node, CONF.baremetal.association_timeout, 1):
- msg = ('Timed out waiting to get Ironic node by instance id %s'
- % instance_id)
- raise exceptions.TimeoutException(msg)
-
- def get_node(self, node_id=None, instance_id=None):
- if node_id:
- _, body = self.baremetal_client.show_node(node_id)
- return body
- elif instance_id:
- _, body = self.baremetal_client.show_node_by_instance_uuid(
- instance_id)
- if body['nodes']:
- return body['nodes'][0]
-
- def get_ports(self, node_uuid):
- ports = []
- _, body = self.baremetal_client.list_node_ports(node_uuid)
- for port in body['ports']:
- _, p = self.baremetal_client.show_port(port['uuid'])
- ports.append(p)
- return ports
-
- def add_keypair(self):
- self.keypair = self.create_keypair()
-
- def verify_connectivity(self, ip=None):
- if ip:
- dest = self.get_remote_client(ip)
- else:
- dest = self.get_remote_client(self.instance)
- dest.validate_authentication()
-
- def boot_instance(self):
- self.instance = self.create_server(
- key_name=self.keypair['name'])
-
- self.wait_node(self.instance['id'])
- self.node = self.get_node(instance_id=self.instance['id'])
-
- self.wait_power_state(self.node['uuid'], BaremetalPowerStates.POWER_ON)
-
- self.wait_provisioning_state(
- self.node['uuid'],
- [BaremetalProvisionStates.DEPLOYWAIT,
- BaremetalProvisionStates.ACTIVE],
- timeout=15)
-
- self.wait_provisioning_state(self.node['uuid'],
- BaremetalProvisionStates.ACTIVE,
- timeout=CONF.baremetal.active_timeout)
-
- waiters.wait_for_server_status(self.servers_client,
- self.instance['id'], 'ACTIVE')
- self.node = self.get_node(instance_id=self.instance['id'])
- self.instance = (self.servers_client.show_server(self.instance['id'])
- ['server'])
-
- def terminate_instance(self):
- self.servers_client.delete_server(self.instance['id'])
- self.wait_power_state(self.node['uuid'],
- BaremetalPowerStates.POWER_OFF)
- self.wait_provisioning_state(
- self.node['uuid'],
- BaremetalProvisionStates.NOSTATE,
- timeout=CONF.baremetal.unprovision_timeout)
-
-
class EncryptionScenarioTest(ScenarioTest):
"""Base class for encryption scenario tests"""
@@ -1367,36 +1200,33 @@
@classmethod
def setup_clients(cls):
super(EncryptionScenarioTest, cls).setup_clients()
- if CONF.volume_feature_enabled.api_v1:
- cls.admin_volume_types_client = cls.os_adm.volume_types_client
- else:
- cls.admin_volume_types_client = cls.os_adm.volume_types_v2_client
-
- def create_volume_type(self, client=None, name=None):
- if not client:
- client = self.admin_volume_types_client
- if not name:
- name = 'generic'
- randomized_name = data_utils.rand_name('scenario-type-' + name)
- LOG.debug("Creating a volume type: %s", randomized_name)
- body = client.create_volume_type(
- name=randomized_name)['volume_type']
- self.assertIn('id', body)
- self.addCleanup(client.delete_volume_type, body['id'])
- return body
+ cls.admin_volume_types_client = cls.os_admin.volume_types_v2_client
+ cls.admin_encryption_types_client =\
+ cls.os_admin.encryption_types_v2_client
def create_encryption_type(self, client=None, type_id=None, provider=None,
key_size=None, cipher=None,
control_location=None):
if not client:
- client = self.admin_volume_types_client
+ client = self.admin_encryption_types_client
if not type_id:
volume_type = self.create_volume_type()
type_id = volume_type['id']
LOG.debug("Creating an encryption type for volume type: %s", type_id)
client.create_encryption_type(
type_id, provider=provider, key_size=key_size, cipher=cipher,
- control_location=control_location)['encryption']
+ control_location=control_location)
+
+ def create_encrypted_volume(self, encryption_provider, volume_type,
+ key_size=256, cipher='aes-xts-plain64',
+ control_location='front-end'):
+ volume_type = self.create_volume_type(name=volume_type)
+ self.create_encryption_type(type_id=volume_type['id'],
+ provider=encryption_provider,
+ key_size=key_size,
+ cipher=cipher,
+ control_location=control_location)
+ return self.create_volume(volume_type=volume_type['name'])
class ObjectStorageScenarioTest(ScenarioTest):
@@ -1437,24 +1267,24 @@
def create_container(self, container_name=None):
name = container_name or data_utils.rand_name(
'swift-scenario-container')
- self.container_client.create_container(name)
+ self.container_client.update_container(name)
# look for the container to assure it is created
self.list_and_check_container_objects(name)
- LOG.debug('Container %s created' % (name))
- self.addCleanup(self.delete_wrapper,
+ LOG.debug('Container %s created', name)
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
self.container_client.delete_container,
name)
return name
def delete_container(self, container_name):
self.container_client.delete_container(container_name)
- LOG.debug('Container %s deleted' % (container_name))
+ LOG.debug('Container %s deleted', container_name)
def upload_object_to_container(self, container_name, obj_name=None):
obj_name = obj_name or data_utils.rand_name('swift-scenario-object')
- obj_data = data_utils.arbitrary_string()
+ obj_data = data_utils.random_bytes()
self.object_client.create_object(container_name, obj_name, obj_data)
- self.addCleanup(self.delete_wrapper,
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
self.object_client.delete_object,
container_name,
obj_name)
@@ -1474,7 +1304,7 @@
present_obj = []
if not_present_obj is None:
not_present_obj = []
- _, object_list = self.container_client.list_container_contents(
+ _, object_list = self.container_client.list_container_objects(
container_name)
if present_obj:
for obj in present_obj:
@@ -1483,14 +1313,6 @@
for obj in not_present_obj:
self.assertNotIn(obj, object_list)
- def change_container_acl(self, container_name, acl):
- metadata_param = {'metadata_prefix': 'x-container-',
- 'metadata': {'read': acl}}
- self.container_client.update_container_metadata(container_name,
- **metadata_param)
- resp, _ = self.container_client.list_container_metadata(container_name)
- self.assertEqual(resp['x-container-read'], acl)
-
def download_and_verify(self, container_name, obj_name, expected_data):
_, obj = self.object_client.get_object(container_name, obj_name)
self.assertEqual(obj, expected_data)
diff --git a/tempest/scenario/test_aggregates_basic_ops.py b/tempest/scenario/test_aggregates_basic_ops.py
index 62c0262..b515639 100644
--- a/tempest/scenario/test_aggregates_basic_ops.py
+++ b/tempest/scenario/test_aggregates_basic_ops.py
@@ -13,15 +13,11 @@
# License for the specific language governing permissions and limitations
# under the License.
-from oslo_log import log as logging
-
from tempest.common import tempest_fixtures as fixtures
-from tempest.common.utils import data_utils
+from tempest.common import utils
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
from tempest.scenario import manager
-from tempest import test
-
-
-LOG = logging.getLogger(__name__)
class TestAggregatesBasicOps(manager.ScenarioTest):
@@ -40,29 +36,25 @@
def setup_clients(cls):
super(TestAggregatesBasicOps, cls).setup_clients()
# Use admin client by default
- cls.manager = cls.admin_manager
- super(TestAggregatesBasicOps, cls).resource_setup()
- cls.aggregates_client = cls.manager.aggregates_client
- cls.hosts_client = cls.manager.hosts_client
+ cls.aggregates_client = cls.os_admin.aggregates_client
+ cls.services_client = cls.os_admin.services_client
def _create_aggregate(self, **kwargs):
aggregate = (self.aggregates_client.create_aggregate(**kwargs)
['aggregate'])
- self.addCleanup(self._delete_aggregate, aggregate)
+ self.addCleanup(self.aggregates_client.delete_aggregate,
+ aggregate['id'])
aggregate_name = kwargs['name']
availability_zone = kwargs['availability_zone']
self.assertEqual(aggregate['name'], aggregate_name)
self.assertEqual(aggregate['availability_zone'], availability_zone)
return aggregate
- def _delete_aggregate(self, aggregate):
- self.aggregates_client.delete_aggregate(aggregate['id'])
-
def _get_host_name(self):
- hosts = self.hosts_client.list_hosts()['hosts']
- self.assertTrue(len(hosts) >= 1)
- computes = [x for x in hosts if x['service'] == 'compute']
- return computes[0]['host_name']
+ 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)
@@ -81,7 +73,7 @@
self.assertEqual(aggregate_name, aggregate['name'])
self.assertEqual(azone, aggregate['availability_zone'])
self.assertEqual(hosts, aggregate['hosts'])
- for meta_key in metadata.keys():
+ for meta_key in metadata:
self.assertIn(meta_key, aggregate['metadata'])
self.assertEqual(metadata[meta_key],
aggregate['metadata'][meta_key])
@@ -90,7 +82,7 @@
aggregate = self.aggregates_client.set_metadata(aggregate['id'],
metadata=meta)
- for key, value in meta.items():
+ for key in meta.keys():
self.assertEqual(meta[key],
aggregate['aggregate']['metadata'][key])
@@ -103,8 +95,9 @@
self.assertEqual(aggregate['availability_zone'], availability_zone)
return aggregate
- @test.idempotent_id('cb2b4c4f-0c7c-4164-bdde-6285b302a081')
- @test.services('compute')
+ @decorators.idempotent_id('cb2b4c4f-0c7c-4164-bdde-6285b302a081')
+ @decorators.attr(type='slow')
+ @utils.services('compute')
def test_aggregate_basic_ops(self):
self.useFixture(fixtures.LockFixture('availability_zone'))
az = 'foo_zone'
diff --git a/tempest/scenario/test_baremetal_basic_ops.py b/tempest/scenario/test_baremetal_basic_ops.py
deleted file mode 100644
index 93b32f7..0000000
--- a/tempest/scenario/test_baremetal_basic_ops.py
+++ /dev/null
@@ -1,131 +0,0 @@
-#
-# Copyright 2014 Hewlett-Packard Development Company, L.P.
-#
-# 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_log import log as logging
-
-from tempest.common import waiters
-from tempest import config
-from tempest.scenario import manager
-from tempest import test
-
-CONF = config.CONF
-
-LOG = logging.getLogger(__name__)
-
-
-class BaremetalBasicOps(manager.BaremetalScenarioTest):
- """This smoke test tests the pxe_ssh Ironic driver.
-
- It follows this basic set of operations:
- * Creates a keypair
- * Boots an instance using the keypair
- * Monitors the associated Ironic node for power and
- expected state transitions
- * Validates Ironic node's port data has been properly updated
- * Verifies SSH connectivity using created keypair via fixed IP
- * Associates a floating ip
- * Verifies SSH connectivity using created keypair via floating IP
- * Verifies instance rebuild with ephemeral partition preservation
- * Deletes instance
- * Monitors the associated Ironic node for power and
- expected state transitions
- """
- def rebuild_instance(self, preserve_ephemeral=False):
- self.rebuild_server(server_id=self.instance['id'],
- preserve_ephemeral=preserve_ephemeral,
- wait=False)
-
- node = self.get_node(instance_id=self.instance['id'])
-
- # We should remain on the same node
- self.assertEqual(self.node['uuid'], node['uuid'])
- self.node = node
-
- waiters.wait_for_server_status(
- self.servers_client,
- server_id=self.instance['id'],
- status='REBUILD',
- ready_wait=False)
- waiters.wait_for_server_status(
- self.servers_client,
- server_id=self.instance['id'],
- status='ACTIVE')
-
- def verify_partition(self, client, label, mount, gib_size):
- """Verify a labeled partition's mount point and size."""
- LOG.info("Looking for partition %s mounted on %s" % (label, mount))
-
- # Validate we have a device with the given partition label
- cmd = "/sbin/blkid | grep '%s' | cut -d':' -f1" % label
- device = client.exec_command(cmd).rstrip('\n')
- LOG.debug("Partition device is %s" % device)
- self.assertNotEqual('', device)
-
- # Validate the mount point for the device
- cmd = "mount | grep '%s' | cut -d' ' -f3" % device
- actual_mount = client.exec_command(cmd).rstrip('\n')
- LOG.debug("Partition mount point is %s" % actual_mount)
- self.assertEqual(actual_mount, mount)
-
- # Validate the partition size matches what we expect
- numbers = '0123456789'
- devnum = device.replace('/dev/', '')
- cmd = "cat /sys/block/%s/%s/size" % (devnum.rstrip(numbers), devnum)
- num_bytes = client.exec_command(cmd).rstrip('\n')
- num_bytes = int(num_bytes) * 512
- actual_gib_size = num_bytes / (1024 * 1024 * 1024)
- LOG.debug("Partition size is %d GiB" % actual_gib_size)
- self.assertEqual(actual_gib_size, gib_size)
-
- def get_flavor_ephemeral_size(self):
- """Returns size of the ephemeral partition in GiB."""
- f_id = self.instance['flavor']['id']
- flavor = self.flavors_client.show_flavor(f_id)['flavor']
- ephemeral = flavor.get('OS-FLV-EXT-DATA:ephemeral')
- if not ephemeral or ephemeral == 'N/A':
- return None
- return int(ephemeral)
-
- def validate_ports(self):
- for port in self.get_ports(self.node['uuid']):
- n_port_id = port['extra']['vif_port_id']
- body = self.ports_client.show_port(n_port_id)
- n_port = body['port']
- self.assertEqual(n_port['device_id'], self.instance['id'])
- self.assertEqual(n_port['mac_address'], port['address'])
-
- @test.idempotent_id('549173a5-38ec-42bb-b0e2-c8b9f4a08943')
- @test.services('baremetal', 'compute', 'image', 'network')
- def test_baremetal_server_ops(self):
- self.add_keypair()
- self.boot_instance()
- self.validate_ports()
- self.verify_connectivity()
- if CONF.validation.connect_method == 'floating':
- floating_ip = self.create_floating_ip(self.instance)['ip']
- self.verify_connectivity(ip=floating_ip)
-
- vm_client = self.get_remote_client(self.instance)
-
- # We expect the ephemeral partition to be mounted on /mnt and to have
- # the same size as our flavor definition.
- eph_size = self.get_flavor_ephemeral_size()
- if eph_size:
- self.verify_partition(vm_client, 'ephemeral0', '/mnt', eph_size)
- # Create the test file
- self.create_timestamp(
- floating_ip, private_key=self.keypair['private_key'])
-
- self.terminate_instance()
diff --git a/tempest/scenario/test_dashboard_basic_ops.py b/tempest/scenario/test_dashboard_basic_ops.py
deleted file mode 100644
index cb6b968..0000000
--- a/tempest/scenario/test_dashboard_basic_ops.py
+++ /dev/null
@@ -1,116 +0,0 @@
-# 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 six.moves import html_parser as HTMLParser
-from six.moves.urllib import parse
-from six.moves.urllib import request
-
-from tempest import config
-from tempest.scenario import manager
-from tempest import test
-
-CONF = config.CONF
-
-
-class HorizonHTMLParser(HTMLParser.HTMLParser):
- csrf_token = None
- region = None
- login = None
-
- def _find_name(self, attrs, name):
- for attrpair in attrs:
- if attrpair[0] == 'name' and attrpair[1] == name:
- return True
- return False
-
- def _find_value(self, attrs):
- for attrpair in attrs:
- if attrpair[0] == 'value':
- return attrpair[1]
- return None
-
- def _find_attr_value(self, attrs, attr_name):
- for attrpair in attrs:
- if attrpair[0] == attr_name:
- return attrpair[1]
- return None
-
- def handle_starttag(self, tag, attrs):
- if tag == 'input':
- if self._find_name(attrs, 'csrfmiddlewaretoken'):
- self.csrf_token = self._find_value(attrs)
- if self._find_name(attrs, 'region'):
- self.region = self._find_value(attrs)
- if tag == 'form':
- self.login = self._find_attr_value(attrs, 'action')
-
-
-class TestDashboardBasicOps(manager.ScenarioTest):
-
- """The test suite for dashboard basic operations
-
- This is a basic scenario test:
- * checks that the login page is available
- * logs in as a regular user
- * checks that the user home page loads without error
- """
-
- @classmethod
- def skip_checks(cls):
- super(TestDashboardBasicOps, cls).skip_checks()
- if not CONF.service_available.horizon:
- raise cls.skipException("Horizon support is required")
-
- @classmethod
- def setup_credentials(cls):
- cls.set_network_resources()
- super(TestDashboardBasicOps, cls).setup_credentials()
-
- def check_login_page(self):
- response = request.urlopen(CONF.dashboard.dashboard_url)
- self.assertIn("id_username", response.read())
-
- def user_login(self, username, password):
- self.opener = request.build_opener(request.HTTPCookieProcessor())
- response = self.opener.open(CONF.dashboard.dashboard_url).read()
-
- # Grab the CSRF token and default region
- parser = HorizonHTMLParser()
- parser.feed(response)
-
- # construct login url for dashboard, discovery accommodates non-/ web
- # root for dashboard
- login_url = parse.urljoin(CONF.dashboard.dashboard_url, parser.login)
-
- # Prepare login form request
- req = request.Request(login_url)
- req.add_header('Content-type', 'application/x-www-form-urlencoded')
- req.add_header('Referer', CONF.dashboard.dashboard_url)
- params = {'username': username,
- 'password': password,
- 'region': parser.region,
- 'csrfmiddlewaretoken': parser.csrf_token}
- self.opener.open(req, parse.urlencode(params))
-
- def check_home_page(self):
- response = self.opener.open(CONF.dashboard.dashboard_url)
- self.assertIn('Overview', response.read())
-
- @test.idempotent_id('4f8851b1-0e69-482b-b63b-84c6e76f6c80')
- @test.services('dashboard')
- def test_basic_scenario(self):
- creds = self.os.credentials
- self.check_login_page()
- self.user_login(creds.username, creds.password)
- self.check_home_page()
diff --git a/tempest/scenario/test_encrypted_cinder_volumes.py b/tempest/scenario/test_encrypted_cinder_volumes.py
index dcd77ad..8c210d5 100644
--- a/tempest/scenario/test_encrypted_cinder_volumes.py
+++ b/tempest/scenario/test_encrypted_cinder_volumes.py
@@ -13,9 +13,10 @@
# 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
-from tempest import test
CONF = config.CONF
@@ -45,37 +46,26 @@
image = self.glance_image_create()
keypair = self.create_keypair()
- return self.create_server(image_id=image,
- key_name=keypair['name'],
- wait_until='ACTIVE')
-
- def create_encrypted_volume(self, encryption_provider, volume_type):
- volume_type = self.create_volume_type(name=volume_type)
- self.create_encryption_type(type_id=volume_type['id'],
- provider=encryption_provider,
- key_size=512,
- cipher='aes-xts-plain64',
- control_location='front-end')
- return self.create_volume(volume_type=volume_type['name'])
+ return self.create_server(image_id=image, key_name=keypair['name'])
def attach_detach_volume(self, server, volume):
attached_volume = self.nova_volume_attach(server, volume)
self.nova_volume_detach(server, attached_volume)
- @test.idempotent_id('79165fb4-5534-4b9d-8429-97ccffb8f86e')
- @test.services('compute', 'volume', 'image')
+ @decorators.idempotent_id('79165fb4-5534-4b9d-8429-97ccffb8f86e')
+ @decorators.attr(type='slow')
+ @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)
- @test.idempotent_id('cbc752ed-b716-4717-910f-956cce965722')
- @test.services('compute', 'volume', 'image')
+ @decorators.idempotent_id('cbc752ed-b716-4717-910f-956cce965722')
+ @decorators.attr(type='slow')
+ @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_large_ops.py b/tempest/scenario/test_large_ops.py
deleted file mode 100644
index b549ecb..0000000
--- a/tempest/scenario/test_large_ops.py
+++ /dev/null
@@ -1,138 +0,0 @@
-# Copyright 2013 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 oslo_log import log as logging
-from tempest_lib import exceptions as lib_exc
-
-from tempest.common import fixed_network
-from tempest.common.utils import data_utils
-from tempest.common import waiters
-from tempest import config
-from tempest.scenario import manager
-from tempest import test
-
-CONF = config.CONF
-
-
-LOG = logging.getLogger(__name__)
-
-
-class TestLargeOpsScenario(manager.ScenarioTest):
-
- """Test large operations.
-
- This test below:
- * Spin up multiple instances in one nova call, and repeat three times
- * as a regular user
- * TODO: same thing for cinder
-
- """
-
- @classmethod
- def skip_checks(cls):
- super(TestLargeOpsScenario, cls).skip_checks()
- if CONF.scenario.large_ops_number < 1:
- raise cls.skipException("large_ops_number not set to multiple "
- "instances")
-
- @classmethod
- def setup_credentials(cls):
- cls.set_network_resources()
- super(TestLargeOpsScenario, cls).setup_credentials()
-
- @classmethod
- def resource_setup(cls):
- super(TestLargeOpsScenario, cls).resource_setup()
- # list of cleanup calls to be executed in reverse order
- cls._cleanup_resources = []
-
- @classmethod
- def resource_cleanup(cls):
- while cls._cleanup_resources:
- function, args, kwargs = cls._cleanup_resources.pop(-1)
- try:
- function(*args, **kwargs)
- except lib_exc.NotFound:
- pass
- super(TestLargeOpsScenario, cls).resource_cleanup()
-
- @classmethod
- def addCleanupClass(cls, function, *arguments, **keywordArguments):
- cls._cleanup_resources.append((function, arguments, keywordArguments))
-
- def _wait_for_server_status(self, status):
- for server in self.servers:
- # Make sure nova list keeps working throughout the build process
- self.servers_client.list_servers()
- waiters.wait_for_server_status(self.servers_client,
- server['id'], status)
-
- def nova_boot(self, image):
- name = data_utils.rand_name('scenario-server')
- flavor_id = CONF.compute.flavor_ref
- # Explicitly create secgroup to avoid cleanup at the end of testcases.
- # Since no traffic is tested, we don't need to actually add rules to
- # secgroup
- secgroup = self.compute_security_groups_client.create_security_group(
- name='secgroup-%s' % name,
- description='secgroup-desc-%s' % name)['security_group']
- self.addCleanupClass(
- self.compute_security_groups_client.delete_security_group,
- secgroup['id'])
- create_kwargs = {
- 'min_count': CONF.scenario.large_ops_number,
- 'security_groups': [{'name': secgroup['name']}]
- }
- network = self.get_tenant_network()
- create_kwargs = fixed_network.set_networks_kwarg(network,
- create_kwargs)
- self.servers_client.create_server(
- name=name,
- imageRef=image,
- flavorRef=flavor_id,
- **create_kwargs)
- # needed because of bug 1199788
- params = {'name': name}
- server_list = self.servers_client.list_servers(**params)
- self.servers = server_list['servers']
- for server in self.servers:
- # after deleting all servers - wait for all servers to clear
- # before cleanup continues
- self.addCleanupClass(waiters.wait_for_server_termination,
- self.servers_client,
- server['id'])
- for server in self.servers:
- self.addCleanupClass(self.servers_client.delete_server,
- server['id'])
- self._wait_for_server_status('ACTIVE')
-
- def _large_ops_scenario(self):
- image = self.glance_image_create()
- self.nova_boot(image)
-
- @test.idempotent_id('14ba0e78-2ed9-4d17-9659-a48f4756ecb3')
- @test.services('compute', 'image')
- def test_large_ops_scenario_1(self):
- self._large_ops_scenario()
-
- @test.idempotent_id('b9b79b88-32aa-42db-8f8f-dcc8f4b4ccfe')
- @test.services('compute', 'image')
- def test_large_ops_scenario_2(self):
- self._large_ops_scenario()
-
- @test.idempotent_id('3aab7e82-2de3-419a-9da1-9f3a070668fb')
- @test.services('compute', 'image')
- def test_large_ops_scenario_3(self):
- self._large_ops_scenario()
diff --git a/tempest/scenario/test_minimum_basic.py b/tempest/scenario/test_minimum_basic.py
index 2ef3cee..2b35e45 100644
--- a/tempest/scenario/test_minimum_basic.py
+++ b/tempest/scenario/test_minimum_basic.py
@@ -13,19 +13,17 @@
# License for the specific language governing permissions and limitations
# under the License.
-from oslo_log import log as logging
-
from tempest.common import custom_matchers
+from tempest.common import utils
from tempest.common import waiters
from tempest import config
-from tempest import exceptions
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions
from tempest.scenario import manager
-from tempest import test
CONF = config.CONF
-LOG = logging.getLogger(__name__)
-
class TestMinimumBasicScenario(manager.ScenarioTest):
@@ -50,19 +48,6 @@
10. Check SSH connection to instance after reboot
"""
-
- def _wait_for_server_status(self, server, status):
- server_id = server['id']
- # Raise on error defaults to True, which is consistent with the
- # original function from scenario tests here
- waiters.wait_for_server_status(self.servers_client,
- server_id, status)
-
- def nova_list(self):
- servers = self.servers_client.list_servers()
- # The list servers in the compute client is inconsistent...
- return servers['servers']
-
def nova_show(self, server):
got_server = (self.servers_client.show_server(server['id'])
['server'])
@@ -73,24 +58,19 @@
server, custom_matchers.MatchesDictExceptForKeys(
got_server, excluded_keys=excluded_keys))
- def cinder_create(self):
- return self.create_volume()
-
- def cinder_list(self):
- return self.volumes_client.list_volumes()['volumes']
-
def cinder_show(self, volume):
got_volume = self.volumes_client.show_volume(volume['id'])['volume']
self.assertEqual(volume, got_volume)
def nova_reboot(self, server):
self.servers_client.reboot_server(server['id'], type='SOFT')
- self._wait_for_server_status(server, 'ACTIVE')
+ waiters.wait_for_server_status(self.servers_client,
+ server['id'], 'ACTIVE')
- def check_partitions(self):
+ def check_disks(self):
# NOTE(andreaf) The device name may be different on different guest OS
- partitions = self.linux_client.get_partitions()
- self.assertEqual(1, partitions.count(CONF.compute.volume_device_name))
+ disks = self.linux_client.get_disks()
+ self.assertEqual(1, disks.count(CONF.compute.volume_device_name))
def create_and_add_security_group_to_server(self, server):
secgroup = self._create_security_group()
@@ -104,29 +84,34 @@
['server'])
return {'name': secgroup['name']} in body['security_groups']
- if not test.call_until_true(wait_for_secgroup_add,
- CONF.compute.build_timeout,
- CONF.compute.build_interval):
+ if not test_utils.call_until_true(wait_for_secgroup_add,
+ CONF.compute.build_timeout,
+ CONF.compute.build_interval):
msg = ('Timed out waiting for adding security group %s to server '
'%s' % (secgroup['id'], server['id']))
raise exceptions.TimeoutException(msg)
- @test.idempotent_id('bdbb5441-9204-419d-a225-b4fdbfb1a1a8')
- @test.services('compute', 'volume', 'image', 'network')
+ def _get_floating_ip_in_server_addresses(self, floating_ip, server):
+ for addresses in server['addresses'].values():
+ for address in addresses:
+ if (address['OS-EXT-IPS:type'] == 'floating' and
+ address['addr'] == floating_ip['ip']):
+ return address
+
+ @decorators.idempotent_id('bdbb5441-9204-419d-a225-b4fdbfb1a1a8')
+ @utils.services('compute', 'volume', 'image', 'network')
def test_minimum_basic_scenario(self):
image = self.glance_image_create()
keypair = self.create_keypair()
- server = self.create_server(image_id=image,
- key_name=keypair['name'],
- wait_until='ACTIVE')
- servers = self.nova_list()
+ server = self.create_server(image_id=image, key_name=keypair['name'])
+ servers = self.servers_client.list_servers()['servers']
self.assertIn(server['id'], [x['id'] for x in servers])
self.nova_show(server)
- volume = self.cinder_create()
- volumes = self.cinder_list()
+ volume = self.create_volume()
+ volumes = self.volumes_client.list_volumes()['volumes']
self.assertIn(volume['id'], [x['id'] for x in volumes])
self.cinder_show(volume)
@@ -135,18 +120,57 @@
self.addCleanup(self.nova_volume_detach, server, volume)
self.cinder_show(volume)
- floating_ip = self.create_floating_ip(server)
+ floating_ip = None
+ server = self.servers_client.show_server(server['id'])['server']
+ if (CONF.network_feature_enabled.floating_ips and
+ CONF.network.floating_network_name):
+ floating_ip = self.create_floating_ip(server)
+ # fetch the server again to make sure the addresses were refreshed
+ # after associating the floating IP
+ server = self.servers_client.show_server(server['id'])['server']
+ address = self._get_floating_ip_in_server_addresses(
+ floating_ip, server)
+ self.assertIsNotNone(
+ address,
+ "Failed to find floating IP '%s' in server addresses: %s" %
+ (floating_ip['ip'], server['addresses']))
+ ssh_ip = floating_ip['ip']
+ else:
+ ssh_ip = self.get_server_ip(server)
+
self.create_and_add_security_group_to_server(server)
# check that we can SSH to the server before reboot
self.linux_client = self.get_remote_client(
- floating_ip['ip'], private_key=keypair['private_key'])
+ ssh_ip, private_key=keypair['private_key'],
+ server=server)
self.nova_reboot(server)
# check that we can SSH to the server after reboot
# (both connections are part of the scenario)
self.linux_client = self.get_remote_client(
- floating_ip['ip'], private_key=keypair['private_key'])
+ ssh_ip, private_key=keypair['private_key'],
+ server=server)
- self.check_partitions()
+ self.check_disks()
+
+ if floating_ip:
+ # delete the floating IP, this should refresh the server addresses
+ self.compute_floating_ips_client.delete_floating_ip(
+ floating_ip['id'])
+
+ def is_floating_ip_detached_from_server():
+ server_info = self.servers_client.show_server(
+ server['id'])['server']
+ address = self._get_floating_ip_in_server_addresses(
+ floating_ip, server_info)
+ return (not address)
+
+ if not test_utils.call_until_true(
+ is_floating_ip_detached_from_server,
+ CONF.compute.build_timeout,
+ CONF.compute.build_interval):
+ msg = ("Floating IP '%s' should not be in server addresses: %s"
+ % (floating_ip['ip'], server['addresses']))
+ raise exceptions.TimeoutException(msg)
diff --git a/tempest/scenario/test_network_advanced_server_ops.py b/tempest/scenario/test_network_advanced_server_ops.py
index d6dd6f3..87ce951 100644
--- a/tempest/scenario/test_network_advanced_server_ops.py
+++ b/tempest/scenario/test_network_advanced_server_ops.py
@@ -13,17 +13,15 @@
# License for the specific language governing permissions and limitations
# under the License.
-from oslo_log import log as logging
import testtools
-from tempest.common.utils import data_utils
+from tempest.common import utils
from tempest.common import waiters
from tempest import config
+from tempest.lib import decorators
from tempest.scenario import manager
-from tempest import test
CONF = config.CONF
-LOG = logging.getLogger(__name__)
class TestNetworkAdvancedServerOps(manager.NetworkScenarioTest):
@@ -38,13 +36,20 @@
"""
@classmethod
+ def setup_clients(cls):
+ super(TestNetworkAdvancedServerOps, cls).setup_clients()
+ cls.admin_servers_client = cls.os_admin.servers_client
+
+ @classmethod
def skip_checks(cls):
super(TestNetworkAdvancedServerOps, cls).skip_checks()
- if not (CONF.network.tenant_networks_reachable
- or CONF.network.public_network_id):
- msg = ('Either tenant_networks_reachable must be "true", or '
+ 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)
+ if not CONF.network_feature_enabled.floating_ips:
+ raise cls.skipException("Floating ips are not available")
@classmethod
def setup_credentials(cls):
@@ -52,35 +57,37 @@
cls.set_network_resources()
super(TestNetworkAdvancedServerOps, cls).setup_credentials()
- def _setup_network_and_servers(self):
- keypair = self.create_keypair()
- security_group = self._create_security_group()
- network, subnet, router = self.create_networks()
- public_network_id = CONF.network.public_network_id
- server_name = data_utils.rand_name('server-smoke')
+ def _setup_server(self, keypair):
+ security_groups = []
+ if utils.is_extension_enabled('security-group', 'network'):
+ security_group = self._create_security_group()
+ security_groups = [{'name': security_group['name']}]
+ network, _, _ = self.create_networks()
server = self.create_server(
- name=server_name,
- networks=[{'uuid': network.id}],
+ networks=[{'uuid': network['id']}],
key_name=keypair['name'],
- security_groups=[{'name': security_group['name']}],
- wait_until='ACTIVE')
+ security_groups=security_groups)
+ return server
+
+ def _setup_network(self, server, keypair):
+ public_network_id = CONF.network.public_network_id
floating_ip = self.create_floating_ip(server, public_network_id)
# Verify that we can indeed connect to the server before we mess with
# it's state
self._wait_server_status_and_check_network_connectivity(
server, keypair, floating_ip)
- return server, keypair, floating_ip
+ return floating_ip
def _check_network_connectivity(self, server, keypair, floating_ip,
should_connect=True):
username = CONF.validation.image_ssh_user
private_key = keypair['private_key']
- self._check_tenant_network_connectivity(
+ self.check_tenant_network_connectivity(
server, username, private_key,
should_connect=should_connect,
servers_for_debug=[server])
- floating_ip_addr = floating_ip.floating_ip_address
+ floating_ip_addr = floating_ip['floating_ip_address']
# Check FloatingIP status before checking the connectivity
self.check_floating_ip_status(floating_ip, 'ACTIVE')
self.check_public_network_connectivity(floating_ip_addr, username,
@@ -94,11 +101,17 @@
'ACTIVE')
self._check_network_connectivity(server, keypair, floating_ip)
- @test.idempotent_id('61f1aa9a-1573-410e-9054-afa557cab021')
- @test.stresstest(class_setup_per='process')
- @test.services('compute', 'network')
+ def _get_host_for_server(self, server_id):
+ body = self.admin_servers_client.show_server(server_id)['server']
+ return body['OS-EXT-SRV-ATTR:host']
+
+ @decorators.idempotent_id('61f1aa9a-1573-410e-9054-afa557cab021')
+ @decorators.attr(type='slow')
+ @utils.services('compute', 'network')
def test_server_connectivity_stop_start(self):
- server, keypair, floating_ip = self._setup_network_and_servers()
+ keypair = self.create_keypair()
+ server = self._setup_server(keypair)
+ floating_ip = self._setup_network(server, keypair)
self.servers_client.stop_server(server['id'])
waiters.wait_for_server_status(self.servers_client, server['id'],
'SHUTOFF')
@@ -108,30 +121,38 @@
self._wait_server_status_and_check_network_connectivity(
server, keypair, floating_ip)
- @test.idempotent_id('7b6860c2-afa3-4846-9522-adeb38dfbe08')
- @test.services('compute', 'network')
+ @decorators.idempotent_id('7b6860c2-afa3-4846-9522-adeb38dfbe08')
+ @utils.services('compute', 'network')
def test_server_connectivity_reboot(self):
- server, keypair, floating_ip = self._setup_network_and_servers()
+ keypair = self.create_keypair()
+ server = self._setup_server(keypair)
+ floating_ip = self._setup_network(server, keypair)
self.servers_client.reboot_server(server['id'], type='SOFT')
self._wait_server_status_and_check_network_connectivity(
server, keypair, floating_ip)
- @test.idempotent_id('88a529c2-1daa-4c85-9aec-d541ba3eb699')
- @test.services('compute', 'network')
+ @decorators.idempotent_id('88a529c2-1daa-4c85-9aec-d541ba3eb699')
+ @decorators.attr(type='slow')
+ @utils.services('compute', 'network')
def test_server_connectivity_rebuild(self):
- server, keypair, floating_ip = self._setup_network_and_servers()
+ keypair = self.create_keypair()
+ server = self._setup_server(keypair)
+ floating_ip = self._setup_network(server, keypair)
image_ref_alt = CONF.compute.image_ref_alt
self.servers_client.rebuild_server(server['id'],
image_ref=image_ref_alt)
self._wait_server_status_and_check_network_connectivity(
server, keypair, floating_ip)
- @test.idempotent_id('2b2642db-6568-4b35-b812-eceed3fa20ce')
+ @decorators.idempotent_id('2b2642db-6568-4b35-b812-eceed3fa20ce')
@testtools.skipUnless(CONF.compute_feature_enabled.pause,
'Pause is not available.')
- @test.services('compute', 'network')
+ @decorators.attr(type='slow')
+ @utils.services('compute', 'network')
def test_server_connectivity_pause_unpause(self):
- server, keypair, floating_ip = self._setup_network_and_servers()
+ keypair = self.create_keypair()
+ server = self._setup_server(keypair)
+ floating_ip = self._setup_network(server, keypair)
self.servers_client.pause_server(server['id'])
waiters.wait_for_server_status(self.servers_client, server['id'],
'PAUSED')
@@ -141,12 +162,15 @@
self._wait_server_status_and_check_network_connectivity(
server, keypair, floating_ip)
- @test.idempotent_id('5cdf9499-541d-4923-804e-b9a60620a7f0')
+ @decorators.idempotent_id('5cdf9499-541d-4923-804e-b9a60620a7f0')
@testtools.skipUnless(CONF.compute_feature_enabled.suspend,
'Suspend is not available.')
- @test.services('compute', 'network')
+ @decorators.attr(type='slow')
+ @utils.services('compute', 'network')
def test_server_connectivity_suspend_resume(self):
- server, keypair, floating_ip = self._setup_network_and_servers()
+ keypair = self.create_keypair()
+ server = self._setup_server(keypair)
+ floating_ip = self._setup_network(server, keypair)
self.servers_client.suspend_server(server['id'])
waiters.wait_for_server_status(self.servers_client, server['id'],
'SUSPENDED')
@@ -156,20 +180,74 @@
self._wait_server_status_and_check_network_connectivity(
server, keypair, floating_ip)
- @test.idempotent_id('719eb59d-2f42-4b66-b8b1-bb1254473967')
+ @decorators.idempotent_id('719eb59d-2f42-4b66-b8b1-bb1254473967')
@testtools.skipUnless(CONF.compute_feature_enabled.resize,
'Resize is not available.')
- @test.services('compute', 'network')
+ @decorators.attr(type='slow')
+ @utils.services('compute', 'network')
def test_server_connectivity_resize(self):
resize_flavor = CONF.compute.flavor_ref_alt
- if resize_flavor == CONF.compute.flavor_ref:
- msg = "Skipping test - flavor_ref and flavor_ref_alt are identical"
- raise self.skipException(msg)
- server, keypair, floating_ip = self._setup_network_and_servers()
+ keypair = self.create_keypair()
+ server = self._setup_server(keypair)
+ floating_ip = self._setup_network(server, keypair)
self.servers_client.resize_server(server['id'],
flavor_ref=resize_flavor)
waiters.wait_for_server_status(self.servers_client, server['id'],
'VERIFY_RESIZE')
self.servers_client.confirm_resize_server(server['id'])
+ server = self.servers_client.show_server(server['id'])['server']
+ self.assertEqual(resize_flavor, server['flavor']['id'])
self._wait_server_status_and_check_network_connectivity(
server, keypair, floating_ip)
+
+ @decorators.idempotent_id('a4858f6c-401e-4155-9a49-d5cd053d1a2f')
+ @testtools.skipUnless(CONF.compute_feature_enabled.cold_migration,
+ 'Cold migration is not available.')
+ @testtools.skipUnless(CONF.compute.min_compute_nodes > 1,
+ 'Less than 2 compute nodes, skipping multinode '
+ 'tests.')
+ @decorators.attr(type='slow')
+ @utils.services('compute', 'network')
+ def test_server_connectivity_cold_migration(self):
+ keypair = self.create_keypair()
+ server = self._setup_server(keypair)
+ floating_ip = self._setup_network(server, keypair)
+ src_host = self._get_host_for_server(server['id'])
+ self._wait_server_status_and_check_network_connectivity(
+ server, keypair, floating_ip)
+
+ self.admin_servers_client.migrate_server(server['id'])
+ waiters.wait_for_server_status(self.servers_client, server['id'],
+ 'VERIFY_RESIZE')
+ self.servers_client.confirm_resize_server(server['id'])
+ self._wait_server_status_and_check_network_connectivity(
+ server, keypair, floating_ip)
+ dst_host = self._get_host_for_server(server['id'])
+
+ self.assertNotEqual(src_host, dst_host)
+
+ @decorators.idempotent_id('25b188d7-0183-4b1e-a11d-15840c8e2fd6')
+ @testtools.skipUnless(CONF.compute_feature_enabled.cold_migration,
+ 'Cold migration is not available.')
+ @testtools.skipUnless(CONF.compute.min_compute_nodes > 1,
+ 'Less than 2 compute nodes, skipping multinode '
+ 'tests.')
+ @decorators.attr(type='slow')
+ @utils.services('compute', 'network')
+ def test_server_connectivity_cold_migration_revert(self):
+ keypair = self.create_keypair()
+ server = self._setup_server(keypair)
+ floating_ip = self._setup_network(server, keypair)
+ src_host = self._get_host_for_server(server['id'])
+ self._wait_server_status_and_check_network_connectivity(
+ server, keypair, floating_ip)
+
+ self.admin_servers_client.migrate_server(server['id'])
+ waiters.wait_for_server_status(self.servers_client, server['id'],
+ 'VERIFY_RESIZE')
+ self.servers_client.revert_resize_server(server['id'])
+ self._wait_server_status_and_check_network_connectivity(
+ server, keypair, floating_ip)
+ dst_host = self._get_host_for_server(server['id'])
+
+ self.assertEqual(src_host, dst_host)
diff --git a/tempest/scenario/test_network_basic_ops.py b/tempest/scenario/test_network_basic_ops.py
index b8bec16..bcd4ddb 100644
--- a/tempest/scenario/test_network_basic_ops.py
+++ b/tempest/scenario/test_network_basic_ops.py
@@ -19,13 +19,13 @@
from oslo_log import log as logging
import testtools
-from tempest.common.utils import data_utils
+from tempest.common import utils
from tempest.common import waiters
from tempest import config
-from tempest import exceptions
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions
from tempest.scenario import manager
-from tempest.services.network import resources as net_resources
-from tempest import test
CONF = config.CONF
LOG = logging.getLogger(__name__)
@@ -59,7 +59,7 @@
Determine which types of networks to test as follows:
* Configure tenant network checks (via the
- 'tenant_networks_reachable' key) if the Tempest host should
+ 'project_networks_reachable' key) if the Tempest host should
have direct connectivity to tenant networks. This is likely to
be the case if Tempest is running on the same host as a
single-node devstack installation with IP namespaces disabled.
@@ -81,15 +81,17 @@
@classmethod
def skip_checks(cls):
super(TestNetworkBasicOps, cls).skip_checks()
- if not (CONF.network.tenant_networks_reachable
- or CONF.network.public_network_id):
- msg = ('Either tenant_networks_reachable must be "true", or '
+ 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)
for ext in ['router', 'security-group']:
- if not test.is_extension_enabled(ext, 'network'):
+ if not utils.is_extension_enabled(ext, 'network'):
msg = "%s extension not enabled." % ext
raise cls.skipException(msg)
+ if not CONF.network_feature_enabled.floating_ips:
+ raise cls.skipException("Floating ips are not available")
@classmethod
def setup_credentials(cls):
@@ -104,21 +106,23 @@
def _setup_network_and_servers(self, **kwargs):
boot_with_port = kwargs.pop('boot_with_port', False)
- self.security_group = \
- self._create_security_group(tenant_id=self.tenant_id)
self.network, self.subnet, self.router = self.create_networks(**kwargs)
self.check_networks()
self.ports = []
- self.port_id = None
+ port_id = None
if boot_with_port:
# create a port on the network and boot with that
- self.port_id = self._create_port(self.network['id']).id
- self.ports.append({'port': self.port_id})
+ port_id = self.create_port(self.network['id'])['id']
+ self.ports.append({'port': port_id})
- name = data_utils.rand_name('server-smoke')
- server = self._create_server(name, self.network, self.port_id)
- self._check_tenant_network_connectivity()
+ server = self._create_server(self.network, port_id)
+ ssh_login = CONF.validation.image_ssh_user
+ for server in self.servers:
+ # call the common method in the parent class
+ self.check_tenant_network_connectivity(
+ server, ssh_login, self._get_server_key(server),
+ servers_for_debug=self.servers)
floating_ip = self.create_floating_ip(server)
self.floating_ip_tuple = Floating_IP_tuple(floating_ip, server)
@@ -129,60 +133,51 @@
via checking the result of list_[networks,routers,subnets]
"""
- seen_nets = self._list_networks()
- seen_names = [n['name'] for n in seen_nets]
- seen_ids = [n['id'] for n in seen_nets]
- self.assertIn(self.network.name, seen_names)
- self.assertIn(self.network.id, seen_ids)
+ seen_nets = self.os_admin.networks_client.list_networks()
+ seen_names = [n['name'] for n in seen_nets['networks']]
+ seen_ids = [n['id'] for n in seen_nets['networks']]
+ self.assertIn(self.network['name'], seen_names)
+ self.assertIn(self.network['id'], seen_ids)
if self.subnet:
- seen_subnets = self._list_subnets()
- seen_net_ids = [n['network_id'] for n in seen_subnets]
- seen_subnet_ids = [n['id'] for n in seen_subnets]
- self.assertIn(self.network.id, seen_net_ids)
- self.assertIn(self.subnet.id, seen_subnet_ids)
+ seen_subnets = self.os_admin.subnets_client.list_subnets()
+ seen_net_ids = [n['network_id'] for n in seen_subnets['subnets']]
+ seen_subnet_ids = [n['id'] for n in seen_subnets['subnets']]
+ self.assertIn(self.network['id'], seen_net_ids)
+ self.assertIn(self.subnet['id'], seen_subnet_ids)
if self.router:
- seen_routers = self._list_routers()
- seen_router_ids = [n['id'] for n in seen_routers]
- seen_router_names = [n['name'] for n in seen_routers]
- self.assertIn(self.router.name,
+ seen_routers = self.os_admin.routers_client.list_routers()
+ seen_router_ids = [n['id'] for n in seen_routers['routers']]
+ seen_router_names = [n['name'] for n in seen_routers['routers']]
+ self.assertIn(self.router['name'],
seen_router_names)
- self.assertIn(self.router.id,
+ self.assertIn(self.router['id'],
seen_router_ids)
- def _create_server(self, name, network, port_id=None):
+ def _create_server(self, network, port_id=None):
keypair = self.create_keypair()
self.keypairs[keypair['name']] = keypair
- security_groups = [{'name': self.security_group['name']}]
- network = {'uuid': network.id}
+ security_groups = [
+ {'name': self._create_security_group()['name']}
+ ]
+ network = {'uuid': network['id']}
if port_id is not None:
network['port'] = port_id
server = self.create_server(
- name=name,
networks=[network],
key_name=keypair['name'],
- security_groups=security_groups,
- wait_until='ACTIVE')
+ security_groups=security_groups)
self.servers.append(server)
return server
def _get_server_key(self, server):
return self.keypairs[server['key_name']]['private_key']
- def _check_tenant_network_connectivity(self):
- ssh_login = CONF.validation.image_ssh_user
- for server in self.servers:
- # call the common method in the parent class
- super(TestNetworkBasicOps, self).\
- _check_tenant_network_connectivity(
- server, ssh_login, self._get_server_key(server),
- servers_for_debug=self.servers)
-
def check_public_network_connectivity(
self, should_connect=True, msg=None,
- should_check_floating_ip_status=True):
+ should_check_floating_ip_status=True, mtu=None):
"""Verifies connectivty to a VM via public network and floating IP
and verifies floating IP has resource status is correct.
@@ -194,10 +189,11 @@
to indicate the context of the failure
:param should_check_floating_ip_status: bool. should status of
floating_ip be checked or not
+ :param mtu: int. MTU network to use for connectivity validation
"""
ssh_login = CONF.validation.image_ssh_user
floating_ip, server = self.floating_ip_tuple
- ip_address = floating_ip.floating_ip_address
+ ip_address = floating_ip['floating_ip_address']
private_key = None
floatingip_status = 'DOWN'
if should_connect:
@@ -209,89 +205,97 @@
# call the common method in the parent class
super(TestNetworkBasicOps, self).check_public_network_connectivity(
ip_address, ssh_login, private_key, should_connect, msg,
- self.servers)
+ self.servers, mtu=mtu)
def _disassociate_floating_ips(self):
- floating_ip, server = self.floating_ip_tuple
- self._disassociate_floating_ip(floating_ip)
- self.floating_ip_tuple = Floating_IP_tuple(
- floating_ip, None)
+ floating_ip, _ = self.floating_ip_tuple
+ floating_ip = self.floating_ips_client.update_floatingip(
+ floating_ip['id'], port_id=None)['floatingip']
+ self.assertIsNone(floating_ip['port_id'])
+ self.floating_ip_tuple = Floating_IP_tuple(floating_ip, None)
def _reassociate_floating_ips(self):
floating_ip, server = self.floating_ip_tuple
- name = data_utils.rand_name('new_server-smoke')
# create a new server for the floating ip
- server = self._create_server(name, self.network)
- self._associate_floating_ip(floating_ip, server)
- self.floating_ip_tuple = Floating_IP_tuple(
- floating_ip, server)
+ server = self._create_server(self.network)
+ port_id, _ = self._get_server_port_id_and_ip4(server)
+ floating_ip = self.floating_ips_client.update_floatingip(
+ floating_ip['id'], port_id=port_id)['floatingip']
+ self.assertEqual(port_id, floating_ip['port_id'])
+ self.floating_ip_tuple = Floating_IP_tuple(floating_ip, server)
def _create_new_network(self, create_gateway=False):
- self.new_net = self._create_network(tenant_id=self.tenant_id)
+ self.new_net = self._create_network()
if create_gateway:
- self.new_subnet = self._create_subnet(
+ self.new_subnet = self.create_subnet(
network=self.new_net)
else:
- self.new_subnet = self._create_subnet(
+ self.new_subnet = self.create_subnet(
network=self.new_net,
gateway_ip=None)
def _hotplug_server(self):
old_floating_ip, server = self.floating_ip_tuple
- ip_address = old_floating_ip.floating_ip_address
+ ip_address = old_floating_ip['floating_ip_address']
private_key = self._get_server_key(server)
- ssh_client = self.get_remote_client(ip_address,
- private_key=private_key)
+ ssh_client = self.get_remote_client(
+ ip_address, private_key=private_key, server=server)
old_nic_list = self._get_server_nics(ssh_client)
# get a port from a list of one item
- port_list = self._list_ports(device_id=server['id'])
+ port_list = self.os_admin.ports_client.list_ports(
+ device_id=server['id'])['ports']
self.assertEqual(1, len(port_list))
old_port = port_list[0]
interface = self.interface_client.create_interface(
server_id=server['id'],
- net_id=self.new_net.id)['interfaceAttachment']
- self.addCleanup(self.network_client.wait_for_resource_deletion,
- 'port',
- interface['port_id'], client=self.ports_client)
- self.addCleanup(self.delete_wrapper,
+ net_id=self.new_net['id'])['interfaceAttachment']
+ self.addCleanup(self.ports_client.wait_for_resource_deletion,
+ interface['port_id'])
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
self.interface_client.delete_interface,
server['id'], interface['port_id'])
def check_ports():
- self.new_port_list = [port for port in
- self._list_ports(device_id=server['id'])
- if port['id'] != old_port['id']]
+ self.new_port_list = [
+ port for port in
+ self.os_admin.ports_client.list_ports(
+ device_id=server['id'])['ports']
+ if port['id'] != old_port['id']
+ ]
return len(self.new_port_list) == 1
- if not test.call_until_true(check_ports, CONF.network.build_timeout,
- CONF.network.build_interval):
+ if not test_utils.call_until_true(
+ check_ports, CONF.network.build_timeout,
+ CONF.network.build_interval):
raise exceptions.TimeoutException(
"No new port attached to the server in time (%s sec)! "
"Old port: %s. Number of new ports: %d" % (
CONF.network.build_timeout, old_port,
len(self.new_port_list)))
- new_port = net_resources.DeletablePort(ports_client=self.ports_client,
- **self.new_port_list[0])
+ new_port = self.new_port_list[0]
def check_new_nic():
new_nic_list = self._get_server_nics(ssh_client)
self.diff_list = [n for n in new_nic_list if n not in old_nic_list]
return len(self.diff_list) == 1
- if not test.call_until_true(check_new_nic, CONF.network.build_timeout,
- CONF.network.build_interval):
+ if not test_utils.call_until_true(
+ check_new_nic, CONF.network.build_timeout,
+ CONF.network.build_interval):
raise exceptions.TimeoutException("Interface not visible on the "
"guest after %s sec"
% CONF.network.build_timeout)
- num, new_nic = self.diff_list[0]
- ssh_client.assign_static_ip(nic=new_nic,
- addr=new_port.fixed_ips[0]['ip_address'])
- ssh_client.set_nic_state(nic=new_nic)
+ _, new_nic = self.diff_list[0]
+ ssh_client.exec_command("sudo ip addr add %s/%s dev %s" % (
+ new_port['fixed_ips'][0]['ip_address'],
+ CONF.network.project_network_mask_bits,
+ new_nic))
+ ssh_client.exec_command("sudo ip link set %s up" % new_nic)
def _get_server_nics(self, ssh_client):
- reg = re.compile(r'(?P<num>\d+): (?P<nic_name>\w+):')
- ipatxt = ssh_client.get_ip_list()
+ reg = re.compile(r'(?P<num>\d+): (?P<nic_name>\w+)[@]?.*:')
+ ipatxt = ssh_client.exec_command("ip address")
return reg.findall(ipatxt)
def _check_network_internal_connectivity(self, network,
@@ -300,14 +304,20 @@
- ping internal gateway and DHCP port, implying in-tenant connectivity
pinging both, because L3 and DHCP agents might be on different nodes
+ - ping internal compute port, implying connectivity to other VMs on
+ this network
"""
floating_ip, server = self.floating_ip_tuple
# get internal ports' ips:
- # get all network ports in the new network
- internal_ips = (p['fixed_ips'][0]['ip_address'] for p in
- self._list_ports(tenant_id=server['tenant_id'],
- network_id=network.id)
- if p['device_owner'].startswith('network'))
+ # get all network and compute ports in the new network
+ internal_ips = (
+ p['fixed_ips'][0]['ip_address'] for p in
+ self.os_admin.ports_client.list_ports(
+ tenant_id=server['tenant_id'],
+ network_id=network['id'])['ports']
+ if p['device_owner'].startswith('network') or
+ p['device_owner'].startswith('compute')
+ )
self._check_server_connectivity(floating_ip,
internal_ips,
@@ -323,8 +333,11 @@
# We ping the external IP from the instance using its floating IP
# which is always IPv4, so we must only test connectivity to
# external IPv4 IPs if the external network is dualstack.
- v4_subnets = [s for s in self._list_subnets(
- network_id=CONF.network.public_network_id) if s['ip_version'] == 4]
+ v4_subnets = [
+ s for s in self.os_admin.subnets_client.list_subnets(
+ network_id=CONF.network.public_network_id)['subnets']
+ if s['ip_version'] == 4
+ ]
self.assertEqual(1, len(v4_subnets),
"Found %d IPv4 subnets" % len(v4_subnets))
@@ -334,29 +347,25 @@
def _check_server_connectivity(self, floating_ip, address_list,
should_connect=True):
- ip_address = floating_ip.floating_ip_address
+ ip_address = floating_ip['floating_ip_address']
private_key = self._get_server_key(self.floating_ip_tuple.server)
- ssh_source = self._ssh_to_server(ip_address, private_key)
+ ssh_source = self.get_remote_client(
+ ip_address, private_key=private_key,
+ server=self.floating_ip_tuple.server)
for remote_ip in address_list:
- if should_connect:
- msg = ("Timed out waiting for %s to become "
- "reachable") % remote_ip
- else:
- msg = "ip address %s is reachable" % remote_ip
- try:
- self.assertTrue(self._check_remote_connectivity
- (ssh_source, remote_ip, should_connect),
- msg)
- except Exception:
- LOG.exception("Unable to access {dest} via ssh to "
- "floating-ip {src}".format(dest=remote_ip,
- src=floating_ip))
- raise
+ self.check_remote_connectivity(ssh_source, remote_ip,
+ should_connect)
- @test.attr(type='smoke')
- @test.idempotent_id('f323b3ba-82f8-4db7-8ea6-6a895869ec49')
- @test.services('compute', 'network')
+ def _update_router_admin_state(self, router, admin_state_up):
+ kwargs = dict(admin_state_up=admin_state_up)
+ router = self.routers_client.update_router(
+ router['id'], **kwargs)['router']
+ self.assertEqual(admin_state_up, router['admin_state_up'])
+
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('f323b3ba-82f8-4db7-8ea6-6a895869ec49')
+ @utils.services('compute', 'network')
def test_network_basic_ops(self):
"""Basic network operation test
@@ -407,10 +416,28 @@
msg="after re-associate "
"floating ip")
- @test.idempotent_id('1546850e-fbaa-42f5-8b5f-03d8a6a95f15')
- @testtools.skipIf(CONF.baremetal.driver_enabled,
- 'Baremetal relies on a shared physical network.')
- @test.services('compute', 'network')
+ @decorators.idempotent_id('b158ea55-472e-4086-8fa9-c64ac0c6c1d0')
+ @testtools.skipUnless(utils.is_extension_enabled('net-mtu', 'network'),
+ 'No way to calculate MTU for networks')
+ @decorators.attr(type='slow')
+ @utils.services('compute', 'network')
+ def test_mtu_sized_frames(self):
+ """Validate that network MTU sized frames fit through."""
+ self._setup_network_and_servers()
+ # first check that connectivity works in general for the instance
+ self.check_public_network_connectivity(should_connect=True)
+ # now that we checked general connectivity, test that full size frames
+ # can also pass between nodes
+ self.check_public_network_connectivity(
+ should_connect=True, mtu=self.network['mtu'])
+
+ @decorators.idempotent_id('1546850e-fbaa-42f5-8b5f-03d8a6a95f15')
+ @testtools.skipIf(CONF.network.shared_physical_network,
+ 'Connectivity can only be tested when in a '
+ 'multitenant network environment')
+ @decorators.skip_because(bug="1610994")
+ @decorators.attr(type='slow')
+ @utils.services('compute', 'network')
def test_connectivity_between_vms_on_different_networks(self):
"""Test connectivity between VMs on different networks
@@ -445,29 +472,34 @@
self._check_network_internal_connectivity(network=self.network)
self._check_network_external_connectivity()
self._create_new_network(create_gateway=True)
- name = data_utils.rand_name('server-smoke')
- self._create_server(name, self.new_net)
+ self._create_server(self.new_net)
self._check_network_internal_connectivity(network=self.new_net,
should_connect=False)
- self.new_subnet.add_to_router(self.router.id)
+ router_id = self.router['id']
+ self.routers_client.add_router_interface(
+ router_id, subnet_id=self.new_subnet['id'])
+
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.routers_client.remove_router_interface,
+ router_id, subnet_id=self.new_subnet['id'])
self._check_network_internal_connectivity(network=self.new_net,
should_connect=True)
- @test.idempotent_id('c5adff73-e961-41f1-b4a9-343614f18cfa')
+ @decorators.idempotent_id('c5adff73-e961-41f1-b4a9-343614f18cfa')
@testtools.skipUnless(CONF.compute_feature_enabled.interface_attach,
'NIC hotplug not available')
@testtools.skipIf(CONF.network.port_vnic_type in ['direct', 'macvtap'],
'NIC hotplug not supported for '
'vnic_type direct or macvtap')
- @test.services('compute', 'network')
+ @utils.services('compute', 'network')
def test_hotplug_nic(self):
"""Test hotplug network interface
- 1. create a new network, with no gateway (to prevent overwriting VM's
- gateway)
- 2. connect VM to new network
- 3. set static ip and bring new nic up
- 4. check VM can ping new network dhcp port
+ 1. Create a network and a VM.
+ 2. Check connectivity to the VM via a public network.
+ 3. Create a new network, with no gateway.
+ 4. Bring up a new interface
+ 5. check the VM reach the new network
"""
self._setup_network_and_servers()
@@ -476,11 +508,12 @@
self._hotplug_server()
self._check_network_internal_connectivity(network=self.new_net)
- @test.idempotent_id('04b9fe4e-85e8-4aea-b937-ea93885ac59f')
- @testtools.skipIf(CONF.baremetal.driver_enabled,
- 'Router state cannot be altered on a shared baremetal '
- 'network')
- @test.services('compute', 'network')
+ @decorators.idempotent_id('04b9fe4e-85e8-4aea-b937-ea93885ac59f')
+ @testtools.skipIf(CONF.network.shared_physical_network,
+ 'Router state can be altered only with multitenant '
+ 'networks capabilities')
+ @decorators.attr(type='slow')
+ @utils.services('compute', 'network')
def test_update_router_admin_state(self):
"""Test to update admin state up of router
@@ -508,12 +541,13 @@
should_connect=True, msg="after updating "
"admin_state_up of router to True")
- @test.idempotent_id('d8bb918e-e2df-48b2-97cd-b73c95450980')
- @testtools.skipIf(CONF.baremetal.driver_enabled,
- 'network isolation not available for baremetal nodes')
+ @decorators.idempotent_id('d8bb918e-e2df-48b2-97cd-b73c95450980')
+ @testtools.skipIf(CONF.network.shared_physical_network,
+ 'network isolation not available')
@testtools.skipUnless(CONF.scenario.dhcp_client,
"DHCP client is not available.")
- @test.services('compute', 'network')
+ @decorators.attr(type='slow')
+ @utils.services('compute', 'network')
def test_subnet_details(self):
"""Tests that subnet's extra configuration details are affecting VMs.
@@ -551,9 +585,10 @@
self.check_public_network_connectivity(should_connect=True)
floating_ip, server = self.floating_ip_tuple
- ip_address = floating_ip.floating_ip_address
+ ip_address = floating_ip['floating_ip_address']
private_key = self._get_server_key(server)
- ssh_client = self._ssh_to_server(ip_address, private_key)
+ ssh_client = self.get_remote_client(
+ ip_address, private_key=private_key, server=server)
dns_servers = [initial_dns_server]
servers = ssh_client.get_dns_servers()
@@ -565,9 +600,11 @@
act_serv=servers,
trgt_serv=dns_servers))
- self.subnet.update(dns_nameservers=[alt_dns_server])
+ self.subnet = self.subnets_client.update_subnet(
+ self.subnet['id'], dns_nameservers=[alt_dns_server])['subnet']
+
# asserts that Neutron DB has updated the nameservers
- self.assertEqual([alt_dns_server], self.subnet.dns_nameservers,
+ self.assertEqual([alt_dns_server], self.subnet['dns_nameservers'],
"Failed to update subnet's nameservers")
def check_new_dns_server():
@@ -576,58 +613,72 @@
# NOTE(amuller): we are renewing the lease as part of the retry
# because Neutron updates dnsmasq asynchronously after the
# subnet-update API call returns.
- ssh_client.renew_lease(fixed_ip=floating_ip['fixed_ip_address'])
+ ssh_client.renew_lease(fixed_ip=floating_ip['fixed_ip_address'],
+ dhcp_client=CONF.scenario.dhcp_client)
if ssh_client.get_dns_servers() != [alt_dns_server]:
LOG.debug("Failed to update DNS nameservers")
return False
return True
- self.assertTrue(test.call_until_true(check_new_dns_server,
- renew_timeout,
- renew_delay),
+ self.assertTrue(test_utils.call_until_true(check_new_dns_server,
+ renew_timeout,
+ renew_delay),
msg="DHCP renewal failed to fetch "
"new DNS nameservers")
- @test.idempotent_id('f5dfcc22-45fd-409f-954c-5bd500d7890b')
- @testtools.skipIf(CONF.baremetal.driver_enabled,
- 'admin_state of instance ports cannot be altered '
- 'for baremetal nodes')
+ @decorators.idempotent_id('f5dfcc22-45fd-409f-954c-5bd500d7890b')
@testtools.skipUnless(CONF.network_feature_enabled.port_admin_state_change,
"Changing a port's admin state is not supported "
"by the test environment")
- @test.services('compute', 'network')
+ @decorators.attr(type='slow')
+ @utils.services('compute', 'network')
def test_update_instance_port_admin_state(self):
"""Test to update admin_state_up attribute of instance port
- 1. Check public connectivity before updating
+ 1. Check public and project connectivity before updating
admin_state_up attribute of instance port to False
- 2. Check public connectivity after updating
+ 2. Check public and project connectivity after updating
admin_state_up attribute of instance port to False
- 3. Check public connectivity after updating
+ 3. Check public and project connectivity after updating
admin_state_up attribute of instance port to True
"""
self._setup_network_and_servers()
- floating_ip, server = self.floating_ip_tuple
+ _, server = self.floating_ip_tuple
server_id = server['id']
- port_id = self._list_ports(device_id=server_id)[0]['id']
+ port_id = self.os_admin.ports_client.list_ports(
+ device_id=server_id)['ports'][0]['id']
+ server_pip = server['addresses'][self.network['name']][0]['addr']
+
+ server2 = self._create_server(self.network)
+ server2_fip = self.create_floating_ip(server2)
+
+ private_key = self._get_server_key(server2)
+ ssh_client = self.get_remote_client(server2_fip['floating_ip_address'],
+ private_key=private_key,
+ server=server2)
+
self.check_public_network_connectivity(
should_connect=True, msg="before updating "
"admin_state_up of instance port to False")
+ self.check_remote_connectivity(ssh_client, dest=server_pip,
+ should_succeed=True)
self.ports_client.update_port(port_id, admin_state_up=False)
self.check_public_network_connectivity(
should_connect=False, msg="after updating "
"admin_state_up of instance port to False",
should_check_floating_ip_status=False)
+ self.check_remote_connectivity(ssh_client, dest=server_pip,
+ should_succeed=False)
self.ports_client.update_port(port_id, admin_state_up=True)
self.check_public_network_connectivity(
should_connect=True, msg="after updating "
"admin_state_up of instance port to True")
+ self.check_remote_connectivity(ssh_client, dest=server_pip,
+ should_succeed=True)
- @test.idempotent_id('759462e1-8535-46b0-ab3a-33aa45c55aaa')
- @testtools.skipUnless(CONF.compute_feature_enabled.preserve_ports,
- 'Preserving ports on instance delete may not be '
- 'supported in the version of Nova being tested.')
- @test.services('compute', 'network')
+ @decorators.idempotent_id('759462e1-8535-46b0-ab3a-33aa45c55aaa')
+ @decorators.attr(type='slow')
+ @utils.services('compute', 'network')
def test_preserve_preexisting_port(self):
"""Test preserve pre-existing port
@@ -636,6 +687,8 @@
Nova should unbind the port from the instance on delete if the port was
not created by Nova as part of the boot request.
+
+ We should also be able to boot another server with the same port.
"""
# Setup the network, create a port and boot the server from that port.
self._setup_network_and_servers(boot_with_port=True)
@@ -648,8 +701,8 @@
'Server should have been created from a '
'pre-existing port.')
# Assert the port is bound to the server.
- port_list = self._list_ports(device_id=server['id'],
- network_id=self.network['id'])
+ port_list = self.os_admin.ports_client.list_ports(
+ device_id=server['id'], network_id=self.network['id'])['ports']
self.assertEqual(1, len(port_list),
'There should only be one port created for '
'server %s.' % server['id'])
@@ -664,9 +717,20 @@
self.assertEqual('', port['device_id'])
self.assertEqual('', port['device_owner'])
- @test.requires_ext(service='network', extension='l3_agent_scheduler')
- @test.idempotent_id('2e788c46-fb3f-4ac9-8f82-0561555bea73')
- @test.services('compute', 'network')
+ # Boot another server with the same port to make sure nothing was
+ # left around that could cause issues.
+ server = self._create_server(self.network, port['id'])
+ port_list = self.os_admin.ports_client.list_ports(
+ device_id=server['id'], network_id=self.network['id'])['ports']
+ self.assertEqual(1, len(port_list),
+ 'There should only be one port created for '
+ 'server %s.' % server['id'])
+ self.assertEqual(port['id'], port_list[0]['id'])
+
+ @utils.requires_ext(service='network', extension='l3_agent_scheduler')
+ @decorators.idempotent_id('2e788c46-fb3f-4ac9-8f82-0561555bea73')
+ @decorators.attr(type='slow')
+ @utils.services('compute', 'network')
def test_router_rescheduling(self):
"""Tests that router can be removed from agent and add to a new agent.
@@ -681,20 +745,24 @@
# TODO(yfried): refactor this test to be used for other agents (dhcp)
# as well
- list_hosts = (self.admin_manager.network_client.
+ list_hosts = (self.os_admin.routers_client.
list_l3_agents_hosting_router)
- schedule_router = (self.admin_manager.network_agents_client.
- add_router_to_l3_agent)
- unschedule_router = (self.admin_manager.network_agents_client.
- remove_router_from_l3_agent)
+ schedule_router = (self.os_admin.network_agents_client.
+ create_router_on_l3_agent)
+ unschedule_router = (self.os_admin.network_agents_client.
+ delete_router_from_l3_agent)
- agent_list = set(a["id"] for a in
- self._list_agents(agent_type="L3 agent"))
+ agent_list_alive = set(
+ a["id"] for a in
+ self.os_admin.network_agents_client.list_agents(
+ agent_type="L3 agent")['agents'] if a["alive"] is True
+ )
self._setup_network_and_servers()
# NOTE(kevinbenton): we have to use the admin credentials to check
- # for the distributed flag because self.router only has a tenant view.
- admin = self.admin_manager.network_client.show_router(self.router.id)
+ # for the distributed flag because self.router only has a project view.
+ admin = self.os_admin.routers_client.show_router(
+ self.router['id'])
if admin['router'].get('distributed', False):
msg = "Rescheduling test does not apply to distributed routers."
raise self.skipException(msg)
@@ -703,33 +771,32 @@
# remove resource from agents
hosting_agents = set(a["id"] for a in
- list_hosts(self.router.id)['agents'])
- no_migration = agent_list == hosting_agents
+ list_hosts(self.router['id'])['agents'])
+ no_migration = agent_list_alive == hosting_agents
LOG.info("Router will be assigned to {mig} hosting agent".
format(mig="the same" if no_migration else "a new"))
for hosting_agent in hosting_agents:
- unschedule_router(hosting_agent, self.router.id)
+ unschedule_router(hosting_agent, self.router['id'])
self.assertNotIn(hosting_agent,
[a["id"] for a in
- list_hosts(self.router.id)['agents']],
+ list_hosts(self.router['id'])['agents']],
'unscheduling router failed')
# verify resource is un-functional
self.check_public_network_connectivity(
should_connect=False,
msg='after router unscheduling',
- should_check_floating_ip_status=False
)
# schedule resource to new agent
target_agent = list(hosting_agents if no_migration else
- agent_list - hosting_agents)[0]
+ agent_list_alive - hosting_agents)[0]
schedule_router(target_agent,
router_id=self.router['id'])
self.assertEqual(
target_agent,
- list_hosts(self.router.id)['agents'][0]['id'],
+ list_hosts(self.router['id'])['agents'][0]['id'],
"Router failed to reschedule. Hosting agent doesn't match "
"target agent")
@@ -738,9 +805,12 @@
should_connect=True,
msg='After router rescheduling')
- @test.requires_ext(service='network', extension='port-security')
- @test.idempotent_id('7c0bb1a2-d053-49a4-98f9-ca1a1d849f63')
- @test.services('compute', 'network')
+ @utils.requires_ext(service='network', extension='port-security')
+ @testtools.skipUnless(CONF.compute_feature_enabled.interface_attach,
+ 'NIC hotplug not available')
+ @decorators.idempotent_id('7c0bb1a2-d053-49a4-98f9-ca1a1d849f63')
+ @decorators.attr(type='slow')
+ @utils.services('compute', 'network')
def test_port_security_macspoofing_port(self):
"""Tests port_security extension enforces mac spoofing
@@ -752,10 +822,10 @@
The test steps are :
1. Create a new network.
2. Connect (hotplug) the VM to a new network.
- 3. Check the VM can ping the DHCP interface of this network.
+ 3. Check the VM can ping a server on the new network ("peer")
4. Spoof the mac address of the new VM interface.
5. Check the Security Group enforces mac spoofing and blocks pings via
- spoofed interface (VM cannot ping the DHCP interface).
+ spoofed interface (VM cannot ping the peer).
6. Disable port-security of the spoofed port- set the flag to false.
7. Retest 3rd step and check that the Security Group allows pings via
the spoofed interface.
@@ -769,25 +839,31 @@
self._create_new_network()
self._hotplug_server()
fip, server = self.floating_ip_tuple
- new_ports = self._list_ports(device_id=server["id"],
- network_id=self.new_net["id"])
+ new_ports = self.os_admin.ports_client.list_ports(
+ device_id=server["id"], network_id=self.new_net["id"])['ports']
spoof_port = new_ports[0]
private_key = self._get_server_key(server)
- ssh_client = self.get_remote_client(fip.floating_ip_address,
- private_key=private_key)
- spoof_nic = ssh_client.get_nic_name(spoof_port["mac_address"])
- dhcp_ports = self._list_ports(device_owner="network:dhcp",
- network_id=self.new_net["id"])
- new_net_dhcp = dhcp_ports[0]["fixed_ips"][0]["ip_address"]
- self._check_remote_connectivity(ssh_client, dest=new_net_dhcp,
- nic=spoof_nic, should_succeed=True)
- ssh_client.set_mac_address(spoof_nic, spoof_mac)
+ ssh_client = self.get_remote_client(fip['floating_ip_address'],
+ private_key=private_key,
+ server=server)
+ spoof_nic = ssh_client.get_nic_name_by_mac(spoof_port["mac_address"])
+ peer = self._create_server(self.new_net)
+ peer_address = peer['addresses'][self.new_net['name']][0]['addr']
+ self.check_remote_connectivity(ssh_client, dest=peer_address,
+ nic=spoof_nic, should_succeed=True)
+ # Set a mac address by making nic down temporary
+ cmd = ("sudo ip link set {nic} down;"
+ "sudo ip link set dev {nic} address {mac};"
+ "sudo ip link set {nic} up").format(nic=spoof_nic,
+ mac=spoof_mac)
+ ssh_client.exec_command(cmd)
+
new_mac = ssh_client.get_mac_address(nic=spoof_nic)
self.assertEqual(spoof_mac, new_mac)
- self._check_remote_connectivity(ssh_client, dest=new_net_dhcp,
- nic=spoof_nic, should_succeed=False)
+ self.check_remote_connectivity(ssh_client, dest=peer_address,
+ nic=spoof_nic, should_succeed=False)
self.ports_client.update_port(spoof_port["id"],
port_security_enabled=False,
security_groups=[])
- self._check_remote_connectivity(ssh_client, dest=new_net_dhcp,
- nic=spoof_nic, should_succeed=True)
+ self.check_remote_connectivity(ssh_client, dest=peer_address,
+ nic=spoof_nic, should_succeed=True)
diff --git a/tempest/scenario/test_network_v6.py b/tempest/scenario/test_network_v6.py
index 40c7680..e4e39c3 100644
--- a/tempest/scenario/test_network_v6.py
+++ b/tempest/scenario/test_network_v6.py
@@ -12,18 +12,13 @@
# 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 functools
-
-from oslo_log import log as logging
-import six
-
+from tempest.common import utils
from tempest import config
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
from tempest.scenario import manager
-from tempest import test
-
CONF = config.CONF
-LOG = logging.getLogger(__name__)
class TestGettingAddress(manager.NetworkScenarioTest):
@@ -43,17 +38,19 @@
@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.tenant_networks_reachable
- or CONF.network.public_network_id):
- msg = ('Either tenant_networks_reachable must be "true", or '
+ 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)
- if CONF.baremetal.driver_enabled:
- msg = ('Baremetal does not currently support network isolation')
+ if CONF.network.shared_physical_network:
+ msg = 'Deployment uses a shared physical network'
raise cls.skipException(msg)
+ if not CONF.network_feature_enabled.floating_ips:
+ raise cls.skipException("Floating ips are not available")
@classmethod
def setup_credentials(cls):
@@ -64,7 +61,7 @@
def setUp(self):
super(TestGettingAddress, self).setUp()
self.keypair = self.create_keypair()
- self.sec_grp = self._create_security_group(tenant_id=self.tenant_id)
+ self.sec_grp = self._create_security_group()
def prepare_network(self, address6_mode, n_subnets6=1, dualnet=False):
"""Prepare network
@@ -75,37 +72,45 @@
if dualnet - create IPv6 subnets on a different network
:return: list of created networks
"""
- self.network = self._create_network(tenant_id=self.tenant_id)
+ network = self._create_network()
if dualnet:
- self.network_v6 = self._create_network(tenant_id=self.tenant_id)
+ network_v6 = self._create_network()
- sub4 = self._create_subnet(network=self.network,
- namestart='sub4',
- ip_version=4)
+ sub4 = self.create_subnet(network=network,
+ namestart='sub4',
+ ip_version=4)
- router = self._get_router(tenant_id=self.tenant_id)
- sub4.add_to_router(router_id=router['id'])
- self.addCleanup(sub4.delete)
+ router = self._get_router()
+ self.routers_client.add_router_interface(router['id'],
+ subnet_id=sub4['id'])
+
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.routers_client.remove_router_interface,
+ router['id'], subnet_id=sub4['id'])
self.subnets_v6 = []
for _ in range(n_subnets6):
- net6 = self.network_v6 if dualnet else self.network
- sub6 = self._create_subnet(network=net6,
- namestart='sub6',
- ip_version=6,
- ipv6_ra_mode=address6_mode,
- ipv6_address_mode=address6_mode)
+ net6 = network_v6 if dualnet else network
+ sub6 = self.create_subnet(network=net6,
+ namestart='sub6',
+ ip_version=6,
+ ipv6_ra_mode=address6_mode,
+ ipv6_address_mode=address6_mode)
- sub6.add_to_router(router_id=router['id'])
- self.addCleanup(sub6.delete)
+ self.routers_client.add_router_interface(router['id'],
+ subnet_id=sub6['id'])
+
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.routers_client.remove_router_interface,
+ router['id'], subnet_id=sub6['id'])
+
self.subnets_v6.append(sub6)
-
- return [self.network, self.network_v6] if dualnet else [self.network]
+ return [network, network_v6] if dualnet else [network]
@staticmethod
def define_server_ips(srv):
ips = {'4': None, '6': []}
- for net_name, nics in six.iteritems(srv['addresses']):
+ for nics in srv['addresses'].values():
for nic in nics:
if nic['version'] == 6:
ips['6'].append(nic['addr'])
@@ -116,21 +121,18 @@
def prepare_server(self, networks=None):
username = CONF.validation.image_ssh_user
- networks = networks or [self.network]
-
srv = self.create_server(
key_name=self.keypair['name'],
security_groups=[{'name': self.sec_grp['name']}],
- networks=[{'uuid': n.id} for n in networks],
- wait_until='ACTIVE')
+ networks=[{'uuid': n['id']} for n in networks])
fip = self.create_floating_ip(thing=srv)
ips = self.define_server_ips(srv=srv)
ssh = self.get_remote_client(
- server_or_ip=fip.floating_ip_address,
- username=username)
- return ssh, ips, srv["id"]
+ ip_address=fip['floating_ip_address'],
+ username=username, server=srv)
+ return ssh, ips, srv
- def turn_nic6_on(self, ssh, sid):
+ def turn_nic6_on(self, ssh, sid, network_id):
"""Turns the IPv6 vNIC on
Required because guest images usually set only the first vNIC on boot.
@@ -138,111 +140,119 @@
@param ssh: RemoteClient ssh instance to server
@param sid: server uuid
+ @param network_id: the network id the NIC is connected to
"""
- ports = [p["mac_address"] for p in
- self._list_ports(device_id=sid,
- network_id=self.network_v6.id)]
+ ports = [
+ p["mac_address"] for p in
+ self.os_admin.ports_client.list_ports(
+ device_id=sid, network_id=network_id)['ports']
+ ]
+
self.assertEqual(1, len(ports),
message=("Multiple IPv6 ports found on network %s. "
"ports: %s")
- % (self.network_v6, ports))
+ % (network_id, ports))
mac6 = ports[0]
- ssh.set_nic_state(ssh.get_nic_name(mac6))
+ nic = ssh.get_nic_name_by_mac(mac6)
+ ssh.exec_command("sudo ip link set %s up" % nic)
def _prepare_and_test(self, address6_mode, n_subnets6=1, dualnet=False):
net_list = self.prepare_network(address6_mode=address6_mode,
n_subnets6=n_subnets6,
dualnet=dualnet)
- sshv4_1, ips_from_api_1, sid1 = self.prepare_server(networks=net_list)
- sshv4_2, ips_from_api_2, sid2 = self.prepare_server(networks=net_list)
+ sshv4_1, ips_from_api_1, srv1 = self.prepare_server(networks=net_list)
+ sshv4_2, ips_from_api_2, srv2 = self.prepare_server(networks=net_list)
def guest_has_address(ssh, addr):
- return addr in ssh.get_ip_list()
+ return addr in ssh.exec_command("ip address")
# Turn on 2nd NIC for Cirros when dualnet
if dualnet:
- self.turn_nic6_on(sshv4_1, sid1)
- self.turn_nic6_on(sshv4_2, sid2)
+ _, network_v6 = net_list
+ self.turn_nic6_on(sshv4_1, srv1['id'], network_v6['id'])
+ self.turn_nic6_on(sshv4_2, srv2['id'], network_v6['id'])
# get addresses assigned to vNIC as reported by 'ip address' utility
- ips_from_ip_1 = sshv4_1.get_ip_list()
- ips_from_ip_2 = sshv4_2.get_ip_list()
+ ips_from_ip_1 = sshv4_1.exec_command("ip address")
+ ips_from_ip_2 = sshv4_2.exec_command("ip address")
self.assertIn(ips_from_api_1['4'], ips_from_ip_1)
self.assertIn(ips_from_api_2['4'], ips_from_ip_2)
for i in range(n_subnets6):
# v6 should be configured since the image supports it
# It can take time for ipv6 automatic address to get assigned
- srv1_v6_addr_assigned = functools.partial(
- guest_has_address, sshv4_1, ips_from_api_1['6'][i])
+ for srv, ssh, ips in (
+ (srv1, sshv4_1, ips_from_api_1),
+ (srv2, sshv4_2, ips_from_api_2)):
+ ip = ips['6'][i]
+ result = test_utils.call_until_true(
+ guest_has_address,
+ CONF.validation.ping_timeout, 1, ssh, ip)
+ if not result:
+ self._log_console_output(servers=[srv])
+ self.fail(
+ 'Address %s not configured for instance %s, '
+ 'ip address output is\n%s' %
+ (ip, srv['id'], ssh.exec_command("ip address")))
- srv2_v6_addr_assigned = functools.partial(
- guest_has_address, sshv4_2, ips_from_api_2['6'][i])
-
- self.assertTrue(test.call_until_true(srv1_v6_addr_assigned,
- CONF.validation.ping_timeout, 1))
-
- self.assertTrue(test.call_until_true(srv2_v6_addr_assigned,
- CONF.validation.ping_timeout, 1))
-
- self._check_connectivity(sshv4_1, ips_from_api_2['4'])
- self._check_connectivity(sshv4_2, ips_from_api_1['4'])
+ self.check_remote_connectivity(sshv4_1, ips_from_api_2['4'])
+ self.check_remote_connectivity(sshv4_2, ips_from_api_1['4'])
for i in range(n_subnets6):
- self._check_connectivity(sshv4_1,
- ips_from_api_2['6'][i])
- self._check_connectivity(sshv4_1,
- self.subnets_v6[i].gateway_ip)
- self._check_connectivity(sshv4_2,
- ips_from_api_1['6'][i])
- self._check_connectivity(sshv4_2,
- self.subnets_v6[i].gateway_ip)
+ self.check_remote_connectivity(sshv4_1,
+ ips_from_api_2['6'][i])
+ self.check_remote_connectivity(sshv4_1,
+ self.subnets_v6[i]['gateway_ip'])
+ self.check_remote_connectivity(sshv4_2,
+ ips_from_api_1['6'][i])
+ self.check_remote_connectivity(sshv4_2,
+ self.subnets_v6[i]['gateway_ip'])
- def _check_connectivity(self, source, dest):
- self.assertTrue(
- self._check_remote_connectivity(source, dest),
- "Timed out waiting for %s to become reachable from %s" %
- (dest, source.ssh_client.host)
- )
-
- @test.idempotent_id('2c92df61-29f0-4eaa-bee3-7c65bef62a43')
- @test.services('compute', 'network')
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('2c92df61-29f0-4eaa-bee3-7c65bef62a43')
+ @utils.services('compute', 'network')
def test_slaac_from_os(self):
self._prepare_and_test(address6_mode='slaac')
- @test.idempotent_id('d7e1f858-187c-45a6-89c9-bdafde619a9f')
- @test.services('compute', 'network')
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('d7e1f858-187c-45a6-89c9-bdafde619a9f')
+ @utils.services('compute', 'network')
def test_dhcp6_stateless_from_os(self):
self._prepare_and_test(address6_mode='dhcpv6-stateless')
- @test.idempotent_id('7ab23f41-833b-4a16-a7c9-5b42fe6d4123')
- @test.services('compute', 'network')
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('7ab23f41-833b-4a16-a7c9-5b42fe6d4123')
+ @utils.services('compute', 'network')
def test_multi_prefix_dhcpv6_stateless(self):
self._prepare_and_test(address6_mode='dhcpv6-stateless', n_subnets6=2)
- @test.idempotent_id('dec222b1-180c-4098-b8c5-cc1b8342d611')
- @test.services('compute', 'network')
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('dec222b1-180c-4098-b8c5-cc1b8342d611')
+ @utils.services('compute', 'network')
def test_multi_prefix_slaac(self):
self._prepare_and_test(address6_mode='slaac', n_subnets6=2)
- @test.idempotent_id('b6399d76-4438-4658-bcf5-0d6c8584fde2')
- @test.services('compute', 'network')
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('b6399d76-4438-4658-bcf5-0d6c8584fde2')
+ @utils.services('compute', 'network')
def test_dualnet_slaac_from_os(self):
self._prepare_and_test(address6_mode='slaac', dualnet=True)
- @test.idempotent_id('76f26acd-9688-42b4-bc3e-cd134c4cb09e')
- @test.services('compute', 'network')
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('76f26acd-9688-42b4-bc3e-cd134c4cb09e')
+ @utils.services('compute', 'network')
def test_dualnet_dhcp6_stateless_from_os(self):
self._prepare_and_test(address6_mode='dhcpv6-stateless', dualnet=True)
- @test.idempotent_id('cf1c4425-766b-45b8-be35-e2959728eb00')
- @test.services('compute', 'network')
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('cf1c4425-766b-45b8-be35-e2959728eb00')
+ @utils.services('compute', 'network')
def test_dualnet_multi_prefix_dhcpv6_stateless(self):
self._prepare_and_test(address6_mode='dhcpv6-stateless', n_subnets6=2,
dualnet=True)
- @test.idempotent_id('9178ad42-10e4-47e9-8987-e02b170cc5cd')
- @test.services('compute', 'network')
+ @decorators.idempotent_id('9178ad42-10e4-47e9-8987-e02b170cc5cd')
+ @utils.services('compute', 'network')
def test_dualnet_multi_prefix_slaac(self):
self._prepare_and_test(address6_mode='slaac', n_subnets6=2,
dualnet=True)
diff --git a/tempest/scenario/test_object_storage_basic_ops.py b/tempest/scenario/test_object_storage_basic_ops.py
index 98dd705..cbe321e 100644
--- a/tempest/scenario/test_object_storage_basic_ops.py
+++ b/tempest/scenario/test_object_storage_basic_ops.py
@@ -13,35 +13,27 @@
# License for the specific language governing permissions and limitations
# under the License.
-from oslo_log import log as logging
-
-from tempest import config
+from tempest.common import utils
+from tempest.lib import decorators
from tempest.scenario import manager
-from tempest import test
-
-CONF = config.CONF
-
-LOG = logging.getLogger(__name__)
class TestObjectStorageBasicOps(manager.ObjectStorageScenarioTest):
- """Test swift basic ops.
-
- * get swift stat.
- * create container.
- * upload a file to the created container.
- * list container's objects and assure that the uploaded file is present.
- * download the object and check the content
- * delete object from container.
- * list container's objects and assure that the deleted file is gone.
- * delete a container.
- * list containers and assure that the deleted container is gone.
- * change ACL of the container and make sure it works successfully
- """
-
- @test.idempotent_id('b920faf1-7b8a-4657-b9fe-9c4512bfb381')
- @test.services('object_storage')
+ @decorators.idempotent_id('b920faf1-7b8a-4657-b9fe-9c4512bfb381')
+ @utils.services('object_storage')
def test_swift_basic_ops(self):
+ """Test swift basic ops.
+
+ * get swift stat.
+ * create container.
+ * upload a file to the created container.
+ * list container's objects and assure that the uploaded file is
+ present.
+ * download the object and check the content
+ * delete object from container.
+ * list container's objects and assure that the deleted file is gone.
+ * delete a container.
+ """
self.get_swift_stat()
container_name = self.create_container()
obj_name, obj_data = self.upload_object_to_container(container_name)
@@ -53,8 +45,9 @@
not_present_obj=[obj_name])
self.delete_container(container_name)
- @test.idempotent_id('916c7111-cb1f-44b2-816d-8f760e4ea910')
- @test.services('object_storage')
+ @decorators.idempotent_id('916c7111-cb1f-44b2-816d-8f760e4ea910')
+ @decorators.attr(type='slow')
+ @utils.services('object_storage')
def test_swift_acl_anonymous_download(self):
"""This test will cover below steps:
@@ -65,12 +58,18 @@
5. Delete the object and container
"""
container_name = self.create_container()
- obj_name, _ = self.upload_object_to_container(container_name)
+ obj_name, obj_data = self.upload_object_to_container(container_name)
obj_url = '%s/%s/%s' % (self.object_client.base_url,
container_name, obj_name)
resp, _ = self.object_client.raw_request(obj_url, 'GET')
self.assertEqual(resp.status, 401)
-
- self.change_container_acl(container_name, '.r:*')
- resp, _ = self.object_client.raw_request(obj_url, 'GET')
+ metadata_param = {'X-Container-Read': '.r:*'}
+ self.container_client.create_update_or_delete_container_metadata(
+ container_name, create_update_metadata=metadata_param,
+ create_update_metadata_prefix='')
+ resp, _ = self.container_client.list_container_metadata(container_name)
+ self.assertEqual(metadata_param['X-Container-Read'],
+ resp['x-container-read'])
+ resp, data = self.object_client.raw_request(obj_url, 'GET')
self.assertEqual(resp.status, 200)
+ self.assertEqual(obj_data, data)
diff --git a/tempest/scenario/test_object_storage_telemetry_middleware.py b/tempest/scenario/test_object_storage_telemetry_middleware.py
deleted file mode 100644
index eee4d3d..0000000
--- a/tempest/scenario/test_object_storage_telemetry_middleware.py
+++ /dev/null
@@ -1,102 +0,0 @@
-# Copyright 2014 Red Hat
-#
-# 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_log import log as logging
-
-from tempest import config
-from tempest.scenario import manager
-from tempest import test
-
-CONF = config.CONF
-
-LOG = logging.getLogger(__name__)
-
-# Loop for up to 120 seconds waiting on notifications
-# NOTE(chdent): The choice of 120 seconds is fairly
-# arbitrary: Long enough to give the notifications the
-# chance to travel across a highly latent bus but not
-# so long as to allow excessive latency to never be visible.
-# TODO(chdent): Ideally this value would come from configuration.
-NOTIFICATIONS_WAIT = 120
-NOTIFICATIONS_SLEEP = 1
-
-
-class TestObjectStorageTelemetry(manager.ObjectStorageScenarioTest):
- """Test that swift uses the ceilometer middleware.
-
- * create container.
- * upload a file to the created container.
- * retrieve the file from the created container.
- * wait for notifications from ceilometer.
- """
-
- @classmethod
- def skip_checks(cls):
- super(TestObjectStorageTelemetry, cls).skip_checks()
- if not CONF.service_available.ceilometer:
- skip_msg = ("%s skipped as ceilometer is not available" %
- cls.__name__)
- raise cls.skipException(skip_msg)
-
- @classmethod
- def setup_clients(cls):
- super(TestObjectStorageTelemetry, cls).setup_clients()
- cls.telemetry_client = cls.os_operator.telemetry_client
-
- def _confirm_notifications(self, container_name, obj_name):
- # NOTE: Loop seeking for appropriate notifications about the containers
- # and objects sent to swift.
-
- def _check_samples():
- # NOTE: Return True only if we have notifications about some
- # containers and some objects and the notifications are about
- # the expected containers and objects.
- # Otherwise returning False will case _check_samples to be
- # called again.
- results = self.telemetry_client.list_samples(
- 'storage.objects.incoming.bytes')
- LOG.debug('got samples %s', results)
-
- # Extract container info from samples.
- containers, objects = [], []
- for sample in results:
- meta = sample['resource_metadata']
- if meta.get('container') and meta['container'] != 'None':
- containers.append(meta['container'])
- elif (meta.get('target.metadata:container') and
- meta['target.metadata:container'] != 'None'):
- containers.append(meta['target.metadata:container'])
-
- if meta.get('object') and meta['object'] != 'None':
- objects.append(meta['object'])
- elif (meta.get('target.metadata:object') and
- meta['target.metadata:object'] != 'None'):
- objects.append(meta['target.metadata:object'])
-
- return (container_name in containers and obj_name in objects)
-
- self.assertTrue(test.call_until_true(_check_samples,
- NOTIFICATIONS_WAIT,
- NOTIFICATIONS_SLEEP),
- 'Correct notifications were not received after '
- '%s seconds.' % NOTIFICATIONS_WAIT)
-
- @test.idempotent_id('6d6b88e5-3e38-41bc-b34a-79f713a6cb84')
- @test.services('object_storage', 'telemetry')
- def test_swift_middleware_notifies(self):
- container_name = self.create_container()
- obj_name, _ = self.upload_object_to_container(container_name)
- self._confirm_notifications(container_name, obj_name)
diff --git a/tempest/scenario/test_security_groups_basic_ops.py b/tempest/scenario/test_security_groups_basic_ops.py
index 783b740..e39afe0 100644
--- a/tempest/scenario/test_security_groups_basic_ops.py
+++ b/tempest/scenario/test_security_groups_basic_ops.py
@@ -12,18 +12,19 @@
# 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_log import log
+import testtools
-from oslo_log import log as logging
-
-from tempest import clients
-from tempest.common.utils import data_utils
+from tempest.common import compute
+from tempest.common import utils
+from tempest.common.utils import net_info
from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
from tempest.scenario import manager
-from tempest import test
CONF = config.CONF
-
-LOG = logging.getLogger(__name__)
+LOG = log.getLogger(__name__)
class TestSecurityGroupsBasicOps(manager.NetworkScenarioTest):
@@ -47,6 +48,12 @@
success - ping returns
failure - ping_timeout reached
+ multi-node:
+ Multi-Node mode is enabled when CONF.compute.min_compute_nodes > 1.
+ Tests connectivity between servers on different compute nodes.
+ When enabled, test will boot each new server to different
+ compute nodes.
+
setup:
for primary tenant:
1. create a network&subnet
@@ -107,15 +114,16 @@
access point
"""
- def __init__(self, credentials):
- self.manager = clients.Manager(credentials)
+ def __init__(self, clients):
# Credentials from manager are filled with both names and IDs
+ self.manager = clients
self.creds = self.manager.credentials
self.network = None
self.subnet = None
self.router = None
self.security_groups = {}
self.servers = list()
+ self.access_point = None
def set_network(self, network, subnet, router):
self.network = network
@@ -125,44 +133,58 @@
@classmethod
def skip_checks(cls):
super(TestSecurityGroupsBasicOps, cls).skip_checks()
- if CONF.baremetal.driver_enabled:
- msg = ('Not currently supported by baremetal.')
- raise cls.skipException(msg)
if CONF.network.port_vnic_type in ['direct', 'macvtap']:
msg = ('Not currently supported when using vnic_type'
' direct or macvtap')
raise cls.skipException(msg)
- if not (CONF.network.tenant_networks_reachable or
+ if not (CONF.network.project_networks_reachable or
CONF.network.public_network_id):
- msg = ('Either tenant_networks_reachable must be "true", or '
+ msg = ('Either project_networks_reachable must be "true", or '
'public_network_id must be defined.')
raise cls.skipException(msg)
+ if not utils.is_extension_enabled('security-group', 'network'):
+ msg = "security-group extension not enabled."
+ raise cls.skipException(msg)
+ if CONF.network.shared_physical_network:
+ msg = ('Deployment uses a shared physical network, security '
+ 'groups not supported')
+ raise cls.skipException(msg)
+ if not CONF.network_feature_enabled.floating_ips:
+ raise cls.skipException("Floating ips are not available")
@classmethod
def setup_credentials(cls):
# Create no network resources for these tests.
cls.set_network_resources()
super(TestSecurityGroupsBasicOps, cls).setup_credentials()
- # TODO(mnewby) Consider looking up entities as needed instead
- # of storing them as collections on the class.
-
- # Credentials from the manager are filled with both IDs and Names
- cls.alt_creds = cls.alt_manager.credentials
@classmethod
def resource_setup(cls):
super(TestSecurityGroupsBasicOps, cls).resource_setup()
+
+ cls.multi_node = CONF.compute.min_compute_nodes > 1 and \
+ compute.is_scheduler_filter_enabled("DifferentHostFilter")
+ if cls.multi_node:
+ LOG.info("Working in Multi Node mode")
+ else:
+ LOG.info("Working in Single Node mode")
+
cls.floating_ips = {}
cls.tenants = {}
- creds = cls.manager.credentials
- cls.primary_tenant = cls.TenantProperties(creds)
- cls.alt_tenant = cls.TenantProperties(cls.alt_creds)
+ cls.primary_tenant = cls.TenantProperties(cls.os_primary)
+ cls.alt_tenant = cls.TenantProperties(cls.os_alt)
for tenant in [cls.primary_tenant, cls.alt_tenant]:
cls.tenants[tenant.creds.tenant_id] = tenant
cls.floating_ip_access = not CONF.network.public_router_id
def setUp(self):
+ """Set up a single tenant with an accessible server.
+
+ If multi-host is enabled, save created server uuids.
+ """
+ self.servers = []
+
super(TestSecurityGroupsBasicOps, self).setUp()
self._deploy_tenant(self.primary_tenant)
self._verify_network_details(self.primary_tenant)
@@ -179,7 +201,7 @@
client=tenant.manager.security_groups_client
)
- # don't use default secgroup since it allows in-tenant traffic
+ # don't use default secgroup since it allows in-project traffic
def_sg = self._create_empty_security_group(
namestart='secgroup_general-',
tenant_id=tenant.creds.tenant_id,
@@ -192,61 +214,85 @@
port_range_max=22,
direction='ingress',
)
- self._create_security_group_rule(secgroup=access_sg,
- client=tenant.manager.network_client,
- **ssh_rule)
+ sec_group_rules_client = tenant.manager.security_group_rules_client
+ self._create_security_group_rule(
+ secgroup=access_sg,
+ sec_group_rules_client=sec_group_rules_client,
+ **ssh_rule)
def _verify_network_details(self, tenant):
# Checks that we see the newly created network/subnet/router via
# checking the result of list_[networks,routers,subnets]
# Check that (router, subnet) couple exist in port_list
- seen_nets = self._list_networks()
- seen_names = [n['name'] for n in seen_nets]
- seen_ids = [n['id'] for n in seen_nets]
+ seen_nets = self.os_admin.networks_client.list_networks()
+ seen_names = [n['name'] for n in seen_nets['networks']]
+ seen_ids = [n['id'] for n in seen_nets['networks']]
- self.assertIn(tenant.network.name, seen_names)
- self.assertIn(tenant.network.id, seen_ids)
+ self.assertIn(tenant.network['name'], seen_names)
+ self.assertIn(tenant.network['id'], seen_ids)
- seen_subnets = [(n['id'], n['cidr'], n['network_id'])
- for n in self._list_subnets()]
- mysubnet = (tenant.subnet.id, tenant.subnet.cidr, tenant.network.id)
+ seen_subnets = [
+ (n['id'], n['cidr'], n['network_id']) for n in
+ self.os_admin.subnets_client.list_subnets()['subnets']
+ ]
+ mysubnet = (tenant.subnet['id'], tenant.subnet['cidr'],
+ tenant.network['id'])
self.assertIn(mysubnet, seen_subnets)
- seen_routers = self._list_routers()
- seen_router_ids = [n['id'] for n in seen_routers]
- seen_router_names = [n['name'] for n in seen_routers]
+ seen_routers = self.os_admin.routers_client.list_routers()
+ seen_router_ids = [n['id'] for n in seen_routers['routers']]
+ seen_router_names = [n['name'] for n in seen_routers['routers']]
- self.assertIn(tenant.router.name, seen_router_names)
- self.assertIn(tenant.router.id, seen_router_ids)
+ self.assertIn(tenant.router['name'], seen_router_names)
+ self.assertIn(tenant.router['id'], seen_router_ids)
- myport = (tenant.router.id, tenant.subnet.id)
- router_ports = [(i['device_id'], i['fixed_ips'][0]['subnet_id']) for i
- in self._list_ports()
- if self._is_router_port(i)]
+ myport = (tenant.router['id'], tenant.subnet['id'])
+ router_ports = [
+ (i['device_id'], f['subnet_id'])
+ for i in self.os_admin.ports_client.list_ports(
+ device_id=tenant.router['id'])['ports']
+ if net_info.is_router_interface_port(i)
+ for f in i['fixed_ips']
+ ]
self.assertIn(myport, router_ports)
- def _is_router_port(self, port):
- """Return True if port is a router interface."""
- # NOTE(armando-migliaccio): match device owner for both centralized
- # and distributed routers; 'device_owner' is "" by default.
- return port['device_owner'].startswith('network:router_interface')
+ def _create_server(self, name, tenant, security_groups, **kwargs):
+ """Creates a server and assigns it to security group.
- def _create_server(self, name, tenant, security_groups=None):
- """creates a server and assigns to security group"""
- if security_groups is None:
- security_groups = [tenant.security_groups['default']]
+ If multi-host is enabled, Ensures servers are created on different
+ compute nodes, by storing created servers' ids and uses different_host
+ as scheduler_hints on creation.
+ Validates servers are created as requested, using admin client.
+ """
security_groups_names = [{'name': s['name']} for s in security_groups]
+ if self.multi_node:
+ kwargs["scheduler_hints"] = {'different_host': self.servers}
server = self.create_server(
name=name,
- networks=[{'uuid': tenant.network.id}],
+ networks=[{'uuid': tenant.network["id"]}],
key_name=tenant.keypair['name'],
security_groups=security_groups_names,
- wait_until='ACTIVE',
- clients=tenant.manager)
- self.assertEqual(
- sorted([s['name'] for s in security_groups]),
- sorted([s['name'] for s in server['security_groups']]))
+ clients=tenant.manager,
+ **kwargs)
+ if 'security_groups' in server:
+ self.assertEqual(
+ sorted([s['name'] for s in security_groups]),
+ sorted([s['name'] for s in server['security_groups']]))
+
+ # Verify servers are on different compute nodes
+ if self.multi_node:
+ adm_get_server = self.os_admin.servers_client.show_server
+ new_host = adm_get_server(server["id"])["server"][
+ "OS-EXT-SRV-ATTR:host"]
+ host_list = [adm_get_server(s)["server"]["OS-EXT-SRV-ATTR:host"]
+ for s in self.servers]
+ self.assertNotIn(new_host, host_list,
+ message="Failed to boot servers on different "
+ "Compute nodes.")
+
+ self.servers.append(server["id"])
+
return server
def _create_tenant_servers(self, tenant, num=1):
@@ -256,12 +302,13 @@
num=i
)
name = data_utils.rand_name(name)
- server = self._create_server(name, tenant)
+ server = self._create_server(name, tenant,
+ [tenant.security_groups['default']])
tenant.servers.append(server)
def _set_access_point(self, tenant):
# creates a server in a secgroup with rule allowing external ssh
- # in order to access tenant internal network
+ # in order to access project internal network
# workaround ip namespace
secgroups = tenant.security_groups.values()
name = 'server-{tenant}-access_point'.format(
@@ -279,11 +326,12 @@
client=tenant.manager.floating_ips_client)
self.floating_ips.setdefault(server['id'], floating_ip)
- def _create_tenant_network(self, tenant):
+ def _create_tenant_network(self, tenant, port_security_enabled=True):
network, subnet, router = self.create_networks(
- client=tenant.manager.network_client,
networks_client=tenant.manager.networks_client,
- subnets_client=tenant.manager.subnets_client)
+ routers_client=tenant.manager.routers_client,
+ subnets_client=tenant.manager.subnets_client,
+ port_security_enabled=port_security_enabled)
tenant.set_network(network, subnet, router)
def _deploy_tenant(self, tenant_or_id):
@@ -307,10 +355,10 @@
def _get_server_ip(self, server, floating=False):
"""returns the ip (floating/internal) of a server"""
if floating:
- server_ip = self.floating_ips[server['id']].floating_ip_address
+ server_ip = self.floating_ips[server['id']]['floating_ip_address']
else:
server_ip = None
- network_name = self.tenants[server['tenant_id']].network.name
+ network_name = self.tenants[server['tenant_id']].network['name']
if network_name in server['addresses']:
server_ip = server['addresses'][network_name][0]['addr']
return server_ip
@@ -318,41 +366,34 @@
def _connect_to_access_point(self, tenant):
"""create ssh connection to tenant access point"""
access_point_ssh = \
- self.floating_ips[tenant.access_point['id']].floating_ip_address
+ self.floating_ips[tenant.access_point['id']]['floating_ip_address']
private_key = tenant.keypair['private_key']
- access_point_ssh = self._ssh_to_server(access_point_ssh,
- private_key=private_key)
+ access_point_ssh = self.get_remote_client(
+ access_point_ssh, private_key=private_key)
return access_point_ssh
- def _check_connectivity(self, access_point, ip, should_succeed=True):
- if should_succeed:
- msg = "Timed out waiting for %s to become reachable" % ip
- else:
- msg = "%s is reachable" % ip
- self.assertTrue(self._check_remote_connectivity(access_point, ip,
- should_succeed), msg)
-
def _test_in_tenant_block(self, tenant):
access_point_ssh = self._connect_to_access_point(tenant)
for server in tenant.servers:
- self._check_connectivity(access_point=access_point_ssh,
- ip=self._get_server_ip(server),
- should_succeed=False)
+ self.check_remote_connectivity(source=access_point_ssh,
+ dest=self._get_server_ip(server),
+ should_succeed=False)
def _test_in_tenant_allow(self, tenant):
ruleset = dict(
protocol='icmp',
- remote_group_id=tenant.security_groups['default'].id,
+ remote_group_id=tenant.security_groups['default']['id'],
direction='ingress'
)
self._create_security_group_rule(
secgroup=tenant.security_groups['default'],
+ security_groups_client=tenant.manager.security_groups_client,
**ruleset
)
access_point_ssh = self._connect_to_access_point(tenant)
for server in tenant.servers:
- self._check_connectivity(access_point=access_point_ssh,
- ip=self._get_server_ip(server))
+ self.check_remote_connectivity(source=access_point_ssh,
+ dest=self._get_server_ip(server))
def _test_cross_tenant_block(self, source_tenant, dest_tenant):
# if public router isn't defined, then dest_tenant access is via
@@ -360,8 +401,8 @@
access_point_ssh = self._connect_to_access_point(source_tenant)
ip = self._get_server_ip(dest_tenant.access_point,
floating=self.floating_ip_access)
- self._check_connectivity(access_point=access_point_ssh, ip=ip,
- should_succeed=False)
+ self.check_remote_connectivity(source=access_point_ssh, dest=ip,
+ should_succeed=False)
def _test_cross_tenant_allow(self, source_tenant, dest_tenant):
"""check for each direction:
@@ -372,30 +413,34 @@
protocol='icmp',
direction='ingress'
)
+ sec_group_rules_client = (
+ dest_tenant.manager.security_group_rules_client)
self._create_security_group_rule(
secgroup=dest_tenant.security_groups['default'],
- client=dest_tenant.manager.network_client,
+ sec_group_rules_client=sec_group_rules_client,
**ruleset
)
access_point_ssh = self._connect_to_access_point(source_tenant)
ip = self._get_server_ip(dest_tenant.access_point,
floating=self.floating_ip_access)
- self._check_connectivity(access_point_ssh, ip)
+ self.check_remote_connectivity(access_point_ssh, ip)
# test that reverse traffic is still blocked
self._test_cross_tenant_block(dest_tenant, source_tenant)
# allow reverse traffic and check
+ sec_group_rules_client = (
+ source_tenant.manager.security_group_rules_client)
self._create_security_group_rule(
secgroup=source_tenant.security_groups['default'],
- client=source_tenant.manager.network_client,
+ sec_group_rules_client=sec_group_rules_client,
**ruleset
)
access_point_ssh_2 = self._connect_to_access_point(dest_tenant)
ip = self._get_server_ip(source_tenant.access_point,
floating=self.floating_ip_access)
- self._check_connectivity(access_point_ssh_2, ip)
+ self.check_remote_connectivity(access_point_ssh_2, ip)
def _verify_mac_addr(self, tenant):
"""Verify that VM has the same ip, mac as listed in port"""
@@ -405,7 +450,8 @@
mac_addr = mac_addr.strip().lower()
# Get the fixed_ips and mac_address fields of all ports. Select
# only those two columns to reduce the size of the response.
- port_list = self._list_ports(fields=['fixed_ips', 'mac_address'])
+ port_list = self.os_admin.ports_client.list_ports(
+ fields=['fixed_ips', 'mac_address'])['ports']
port_detail_list = [
(port['fixed_ips'][0]['subnet_id'],
port['fixed_ips'][0]['ip_address'],
@@ -413,16 +459,24 @@
for port in port_list if port['fixed_ips']
]
server_ip = self._get_server_ip(tenant.access_point)
- subnet_id = tenant.subnet.id
+ subnet_id = tenant.subnet['id']
self.assertIn((subnet_id, server_ip, mac_addr), port_detail_list)
- @test.idempotent_id('e79f879e-debb-440c-a7e4-efeda05b6848')
- @test.services('compute', 'network')
+ def _log_console_output_for_all_tenants(self):
+ for tenant in self.tenants.values():
+ client = tenant.manager.servers_client
+ self._log_console_output(servers=tenant.servers, client=client)
+ if tenant.access_point is not None:
+ self._log_console_output(
+ servers=[tenant.access_point], client=client)
+
+ @decorators.idempotent_id('e79f879e-debb-440c-a7e4-efeda05b6848')
+ @utils.services('compute', 'network')
def test_cross_tenant_traffic(self):
if not self.credentials_provider.is_multi_tenant():
raise self.skipException("No secondary tenant defined")
try:
- # deploy new tenant
+ # deploy new project
self._deploy_tenant(self.alt_tenant)
self._verify_network_details(self.alt_tenant)
self._verify_mac_addr(self.alt_tenant)
@@ -433,12 +487,11 @@
self._test_cross_tenant_block(source_tenant, dest_tenant)
self._test_cross_tenant_allow(source_tenant, dest_tenant)
except Exception:
- for tenant in self.tenants.values():
- self._log_console_output(servers=tenant.servers)
+ self._log_console_output_for_all_tenants()
raise
- @test.idempotent_id('63163892-bbf6-4249-aa12-d5ea1f8f421b')
- @test.services('compute', 'network')
+ @decorators.idempotent_id('63163892-bbf6-4249-aa12-d5ea1f8f421b')
+ @utils.services('compute', 'network')
def test_in_tenant_traffic(self):
try:
self._create_tenant_servers(self.primary_tenant, num=1)
@@ -447,12 +500,12 @@
self._test_in_tenant_block(self.primary_tenant)
self._test_in_tenant_allow(self.primary_tenant)
except Exception:
- for tenant in self.tenants.values():
- self._log_console_output(servers=tenant.servers)
+ self._log_console_output_for_all_tenants()
raise
- @test.idempotent_id('f4d556d7-1526-42ad-bafb-6bebf48568f6')
- @test.services('compute', 'network')
+ @decorators.idempotent_id('f4d556d7-1526-42ad-bafb-6bebf48568f6')
+ @decorators.attr(type='slow')
+ @utils.services('compute', 'network')
def test_port_update_new_security_group(self):
"""Verifies the traffic after updating the vm port
@@ -469,9 +522,10 @@
protocol='icmp',
direction='ingress',
)
+ sec_group_rules_client = new_tenant.manager.security_group_rules_client
self._create_security_group_rule(
secgroup=new_sg,
- client=new_tenant.manager.network_client,
+ sec_group_rules_client=sec_group_rules_client,
**icmp_rule)
new_tenant.security_groups.update(new_sg=new_sg)
@@ -480,30 +534,32 @@
tenant=new_tenant.creds.tenant_name
)
name = data_utils.rand_name(name)
- server = self._create_server(name, new_tenant)
+ server = self._create_server(name, new_tenant,
+ [new_tenant.security_groups['default']])
# Check connectivity failure with default security group
try:
access_point_ssh = self._connect_to_access_point(new_tenant)
- self._check_connectivity(access_point=access_point_ssh,
- ip=self._get_server_ip(server),
- should_succeed=False)
+ self.check_remote_connectivity(source=access_point_ssh,
+ dest=self._get_server_ip(server),
+ should_succeed=False)
server_id = server['id']
- port_id = self._list_ports(device_id=server_id)[0]['id']
+ port_id = self.os_admin.ports_client.list_ports(
+ device_id=server_id)['ports'][0]['id']
# update port with new security group and check connectivity
self.ports_client.update_port(port_id, security_groups=[
- new_tenant.security_groups['new_sg'].id])
- self._check_connectivity(
- access_point=access_point_ssh,
- ip=self._get_server_ip(server))
+ new_tenant.security_groups['new_sg']['id']])
+ self.check_remote_connectivity(
+ source=access_point_ssh,
+ dest=self._get_server_ip(server))
except Exception:
- for tenant in self.tenants.values():
- self._log_console_output(servers=tenant.servers)
+ self._log_console_output_for_all_tenants()
raise
- @test.idempotent_id('d2f77418-fcc4-439d-b935-72eca704e293')
- @test.services('compute', 'network')
+ @decorators.idempotent_id('d2f77418-fcc4-439d-b935-72eca704e293')
+ @decorators.attr(type='slow')
+ @utils.services('compute', 'network')
def test_multiple_security_groups(self):
"""Verify multiple security groups and checks that rules
@@ -534,9 +590,10 @@
private_key=private_key,
should_connect=True)
- @test.requires_ext(service='network', extension='port-security')
- @test.idempotent_id('7c811dcc-263b-49a3-92d2-1b4d8405f50c')
- @test.services('compute', 'network')
+ @decorators.attr(type='slow')
+ @utils.requires_ext(service='network', extension='port-security')
+ @decorators.idempotent_id('7c811dcc-263b-49a3-92d2-1b4d8405f50c')
+ @utils.services('compute', 'network')
def test_port_security_disable_security_group(self):
"""Verify the default security group rules is disabled."""
new_tenant = self.primary_tenant
@@ -546,28 +603,57 @@
tenant=new_tenant.creds.tenant_name
)
name = data_utils.rand_name(name)
- server = self._create_server(name, new_tenant)
+ server = self._create_server(name, new_tenant,
+ [new_tenant.security_groups['default']])
access_point_ssh = self._connect_to_access_point(new_tenant)
server_id = server['id']
- port_id = self._list_ports(device_id=server_id)[0]['id']
+ port_id = self.os_admin.ports_client.list_ports(
+ device_id=server_id)['ports'][0]['id']
# Flip the port's port security and check connectivity
try:
self.ports_client.update_port(port_id,
port_security_enabled=True,
security_groups=[])
- self._check_connectivity(access_point=access_point_ssh,
- ip=self._get_server_ip(server),
- should_succeed=False)
+ self.check_remote_connectivity(source=access_point_ssh,
+ dest=self._get_server_ip(server),
+ should_succeed=False)
self.ports_client.update_port(port_id,
port_security_enabled=False,
security_groups=[])
- self._check_connectivity(
- access_point=access_point_ssh,
- ip=self._get_server_ip(server))
+ self.check_remote_connectivity(
+ source=access_point_ssh,
+ dest=self._get_server_ip(server))
except Exception:
- for tenant in self.tenants.values():
- self._log_console_output(servers=tenant.servers)
+ self._log_console_output_for_all_tenants()
raise
+
+ @decorators.attr(type='slow')
+ @utils.requires_ext(service='network', extension='port-security')
+ @decorators.idempotent_id('13ccf253-e5ad-424b-9c4a-97b88a026699')
+ # TODO(mriedem): We shouldn't actually need to check this since neutron
+ # disables the port_security extension by default, but the problem is nova
+ # assumes port_security_enabled=True if it's not set on the network
+ # resource, which will mean nova may attempt to apply a security group on
+ # a port on that network which would fail. This is really a bug in nova.
+ @testtools.skipUnless(
+ CONF.network_feature_enabled.port_security,
+ 'Port security must be enabled.')
+ @utils.services('compute', 'network')
+ def test_boot_into_disabled_port_security_network_without_secgroup(self):
+ tenant = self.primary_tenant
+ self._create_tenant_network(tenant, port_security_enabled=False)
+ self.assertFalse(tenant.network['port_security_enabled'])
+ name = data_utils.rand_name('server-smoke')
+ sec_groups = []
+ server = self._create_server(name, tenant, sec_groups)
+ server_id = server['id']
+ ports = self.os_admin.ports_client.list_ports(
+ device_id=server_id)['ports']
+ self.assertEqual(1, len(ports))
+ for port in ports:
+ self.assertEmpty(port['security_groups'],
+ "Neutron shouldn't even use it's default sec "
+ "group.")
diff --git a/tempest/scenario/test_server_advanced_ops.py b/tempest/scenario/test_server_advanced_ops.py
index 4b932ce..8aa729b 100644
--- a/tempest/scenario/test_server_advanced_ops.py
+++ b/tempest/scenario/test_server_advanced_ops.py
@@ -16,10 +16,11 @@
from oslo_log import log as logging
import testtools
+from tempest.common import utils
from tempest.common import waiters
from tempest import config
+from tempest.lib import decorators
from tempest.scenario import manager
-from tempest import test
CONF = config.CONF
@@ -31,74 +32,30 @@
"""The test suite for server advanced operations
This test case stresses some advanced server instance operations:
- * Resizing an instance
* Sequence suspend resume
"""
@classmethod
- def skip_checks(cls):
- super(TestServerAdvancedOps, cls).skip_checks()
- if CONF.compute.flavor_ref_alt == CONF.compute.flavor_ref:
- msg = "Skipping test - flavor_ref and flavor_ref_alt are identical"
- raise cls.skipException(msg)
-
- @classmethod
def setup_credentials(cls):
cls.set_network_resources()
super(TestServerAdvancedOps, cls).setup_credentials()
- @test.idempotent_id('e6c28180-7454-4b59-b188-0257af08a63b')
- @testtools.skipUnless(CONF.compute_feature_enabled.resize,
- 'Resize is not available.')
- @test.services('compute')
- def test_resize_server_confirm(self):
- # We create an instance for use in this test
- instance = self.create_server(wait_until='ACTIVE')
- instance_id = instance['id']
- resize_flavor = CONF.compute.flavor_ref_alt
- LOG.debug("Resizing instance %s from flavor %s to flavor %s",
- instance['id'], instance['flavor']['id'], resize_flavor)
- self.servers_client.resize_server(instance_id, resize_flavor)
- waiters.wait_for_server_status(self.servers_client, instance_id,
- 'VERIFY_RESIZE')
-
- LOG.debug("Confirming resize of instance %s", instance_id)
- self.servers_client.confirm_resize_server(instance_id)
-
- waiters.wait_for_server_status(self.servers_client, instance_id,
- 'ACTIVE')
-
- @test.idempotent_id('949da7d5-72c8-4808-8802-e3d70df98e2c')
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('949da7d5-72c8-4808-8802-e3d70df98e2c')
@testtools.skipUnless(CONF.compute_feature_enabled.suspend,
'Suspend is not available.')
- @test.services('compute')
+ @utils.services('compute')
def test_server_sequence_suspend_resume(self):
# We create an instance for use in this test
- instance = self.create_server(wait_until='ACTIVE')
- instance_id = instance['id']
- LOG.debug("Suspending instance %s. Current status: %s",
- instance_id, instance['status'])
- self.servers_client.suspend_server(instance_id)
- waiters.wait_for_server_status(self.servers_client, instance_id,
- 'SUSPENDED')
- fetched_instance = (self.servers_client.show_server(instance_id)
- ['server'])
- LOG.debug("Resuming instance %s. Current status: %s",
- instance_id, fetched_instance['status'])
- self.servers_client.resume_server(instance_id)
- waiters.wait_for_server_status(self.servers_client, instance_id,
- 'ACTIVE')
- fetched_instance = (self.servers_client.show_server(instance_id)
- ['server'])
- LOG.debug("Suspending instance %s. Current status: %s",
- instance_id, fetched_instance['status'])
- self.servers_client.suspend_server(instance_id)
- waiters.wait_for_server_status(self.servers_client, instance_id,
- 'SUSPENDED')
- fetched_instance = (self.servers_client.show_server(instance_id)
- ['server'])
- LOG.debug("Resuming instance %s. Current status: %s",
- instance_id, fetched_instance['status'])
- self.servers_client.resume_server(instance_id)
- waiters.wait_for_server_status(self.servers_client, instance_id,
- 'ACTIVE')
+ instance_id = self.create_server()['id']
+
+ for _ in range(2):
+ LOG.debug("Suspending instance %s", instance_id)
+ self.servers_client.suspend_server(instance_id)
+ waiters.wait_for_server_status(self.servers_client, instance_id,
+ 'SUSPENDED')
+
+ LOG.debug("Resuming instance %s", instance_id)
+ self.servers_client.resume_server(instance_id)
+ waiters.wait_for_server_status(self.servers_client, instance_id,
+ 'ACTIVE')
diff --git a/tempest/scenario/test_server_basic_ops.py b/tempest/scenario/test_server_basic_ops.py
index 239e120..1671216 100644
--- a/tempest/scenario/test_server_basic_ops.py
+++ b/tempest/scenario/test_server_basic_ops.py
@@ -15,20 +15,17 @@
import json
-from oslo_log import log as logging
-
+from tempest.common import utils
+from tempest.common import waiters
from tempest import config
-from tempest import exceptions
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions
from tempest.scenario import manager
-from tempest.scenario import utils as test_utils
-from tempest import test
CONF = config.CONF
-LOG = logging.getLogger(__name__)
-
-load_tests = test_utils.load_tests_input_scenario_utils
-
class TestServerBasicOps(manager.ScenarioTest):
@@ -47,40 +44,25 @@
def setUp(self):
super(TestServerBasicOps, self).setUp()
- # Setup image and flavor the test instance
- # Support both configured and injected values
- if not hasattr(self, 'image_ref'):
- self.image_ref = CONF.compute.image_ref
- if not hasattr(self, 'flavor_ref'):
- self.flavor_ref = CONF.compute.flavor_ref
- self.image_utils = test_utils.ImageUtils(self.manager)
- if not self.image_utils.is_flavor_enough(self.flavor_ref,
- self.image_ref):
- raise self.skipException(
- '{image} does not fit in {flavor}'.format(
- image=self.image_ref, flavor=self.flavor_ref
- )
- )
- self.run_ssh = CONF.validation.run_validation and \
- self.image_utils.is_sshable_image(self.image_ref)
- self.ssh_user = self.image_utils.ssh_user(self.image_ref)
- LOG.debug('Starting test for i:{image}, f:{flavor}. '
- 'Run ssh: {ssh}, user: {ssh_user}'.format(
- image=self.image_ref, flavor=self.flavor_ref,
- ssh=self.run_ssh, ssh_user=self.ssh_user))
+ self.run_ssh = CONF.validation.run_validation
+ self.ssh_user = CONF.validation.image_ssh_user
- def add_keypair(self):
- self.keypair = self.create_keypair()
-
- def verify_ssh(self):
+ def verify_ssh(self, keypair):
if self.run_ssh:
- # Obtain a floating IP
- self.fip = self.create_floating_ip(self.instance)['ip']
+ # Obtain a floating IP if floating_ips is enabled
+ if (CONF.network_feature_enabled.floating_ips and
+ CONF.network.floating_network_name):
+ self.ip = self.create_floating_ip(self.instance)['ip']
+ else:
+ server = self.servers_client.show_server(
+ self.instance['id'])['server']
+ self.ip = self.get_server_ip(server)
# Check ssh
self.ssh_client = self.get_remote_client(
- server_or_ip=self.fip,
- username=self.image_utils.ssh_user(self.image_ref),
- private_key=self.keypair['private_key'])
+ ip_address=self.ip,
+ username=self.ssh_user,
+ private_key=keypair['private_key'],
+ server=self.instance)
def verify_metadata(self):
if self.run_ssh and CONF.compute_feature_enabled.metadata_service:
@@ -92,50 +74,68 @@
result = self.ssh_client.exec_command(cmd)
if result:
msg = ('Failed while verifying metadata on server. Result '
- 'of command "%s" is NOT "%s".' % (cmd, self.fip))
- self.assertEqual(self.fip, result, msg)
+ 'of command "%s" is NOT "%s".' % (cmd, self.ip))
+ self.assertEqual(self.ip, result, msg)
return 'Verification is successful!'
- if not test.call_until_true(exec_cmd_and_verify_output,
- CONF.compute.build_timeout,
- CONF.compute.build_interval):
+ if not test_utils.call_until_true(exec_cmd_and_verify_output,
+ CONF.compute.build_timeout,
+ CONF.compute.build_interval):
raise exceptions.TimeoutException('Timed out while waiting to '
'verify metadata on server. '
'%s is empty.' % md_url)
+ # Also, test a POST
+ md_url = 'http://169.254.169.254/openstack/2013-10-17/password'
+ data = data_utils.arbitrary_string(100)
+ cmd = 'curl -X POST -d ' + data + ' ' + md_url
+ self.ssh_client.exec_command(cmd)
+ result = self.servers_client.show_password(self.instance['id'])
+ self.assertEqual(data, result['password'])
+
def verify_metadata_on_config_drive(self):
if self.run_ssh and CONF.compute_feature_enabled.config_drive:
# Verify metadata on config_drive
- cmd_blkid = 'blkid -t LABEL=config-2 -o device'
- dev_name = self.ssh_client.exec_command(cmd_blkid)
- dev_name = dev_name.rstrip()
- self.ssh_client.exec_command('sudo mount %s /mnt' % dev_name)
+ self.ssh_client.mount_config_drive()
cmd_md = 'sudo cat /mnt/openstack/latest/meta_data.json'
result = self.ssh_client.exec_command(cmd_md)
- self.ssh_client.exec_command('sudo umount /mnt')
+ self.ssh_client.unmount_config_drive()
result = json.loads(result)
self.assertIn('meta', result)
msg = ('Failed while verifying metadata on config_drive on server.'
' Result of command "%s" is NOT "%s".' % (cmd_md, self.md))
self.assertEqual(self.md, result['meta'], msg)
- @test.idempotent_id('7fff3fb3-91d8-4fd0-bd7d-0204f1f180ba')
- @test.attr(type='smoke')
- @test.services('compute', 'network')
- def test_server_basicops(self):
- self.add_keypair()
- self.security_group = self._create_security_group()
- security_groups = [{'name': self.security_group['name']}]
+ def verify_networkdata_on_config_drive(self):
+ if self.run_ssh and CONF.compute_feature_enabled.config_drive:
+ # Verify network data on config_drive
+ self.ssh_client.mount_config_drive()
+ cmd_md = 'sudo cat /mnt/openstack/latest/network_data.json'
+ result = self.ssh_client.exec_command(cmd_md)
+ self.ssh_client.unmount_config_drive()
+ result = json.loads(result)
+ self.assertIn('services', result)
+ self.assertIn('links', result)
+ self.assertIn('networks', result)
+ # TODO(clarkb) construct network_data from known network
+ # instance info and do direct comparison.
+
+ @decorators.idempotent_id('7fff3fb3-91d8-4fd0-bd7d-0204f1f180ba')
+ @decorators.attr(type='smoke')
+ @utils.services('compute', 'network')
+ def test_server_basic_ops(self):
+ keypair = self.create_keypair()
+ security_group = self._create_security_group()
self.md = {'meta1': 'data1', 'meta2': 'data2', 'metaN': 'dataN'}
self.instance = self.create_server(
- image_id=self.image_ref,
- flavor=self.flavor_ref,
- key_name=self.keypair['name'],
- security_groups=security_groups,
+ key_name=keypair['name'],
+ security_groups=[{'name': security_group['name']}],
config_drive=CONF.compute_feature_enabled.config_drive,
- metadata=self.md,
- wait_until='ACTIVE')
- self.verify_ssh()
+ metadata=self.md)
+ self.verify_ssh(keypair)
self.verify_metadata()
self.verify_metadata_on_config_drive()
+ self.verify_networkdata_on_config_drive()
self.servers_client.delete_server(self.instance['id'])
+ waiters.wait_for_server_termination(
+ self.servers_client, self.instance['id'], ignore_error=False)
diff --git a/tempest/scenario/test_server_multinode.py b/tempest/scenario/test_server_multinode.py
index 7e0e41c..fdf875c 100644
--- a/tempest/scenario/test_server_multinode.py
+++ b/tempest/scenario/test_server_multinode.py
@@ -13,40 +13,42 @@
# License for the specific language governing permissions and limitations
# under the License.
-
-from oslo_log import log as logging
-
+from tempest.common import utils
from tempest import config
-from tempest import exceptions
+from tempest.lib import decorators
+from tempest.lib import exceptions
from tempest.scenario import manager
-from tempest import test
CONF = config.CONF
-LOG = logging.getLogger(__name__)
-
class TestServerMultinode(manager.ScenarioTest):
"""This is a set of tests specific to multinode testing."""
credentials = ['primary', 'admin']
@classmethod
- def setup_clients(cls):
- super(TestServerMultinode, cls).setup_clients()
- # Use admin client by default
- cls.manager = cls.admin_manager
- # this is needed so that we can use the availability_zone:host
- # scheduler hint, which is admin_only by default
- cls.servers_client = cls.admin_manager.servers_client
- super(TestServerMultinode, cls).resource_setup()
+ def skip_checks(cls):
+ super(TestServerMultinode, cls).skip_checks()
- @test.idempotent_id('9cecbe35-b9d4-48da-a37e-7ce70aa43d30')
- @test.attr(type='smoke')
- @test.services('compute', 'network')
+ if CONF.compute.min_compute_nodes < 2:
+ raise cls.skipException(
+ "Less than 2 compute nodes, skipping multinode tests.")
+
+ @decorators.idempotent_id('9cecbe35-b9d4-48da-a37e-7ce70aa43d30')
+ @decorators.attr(type='smoke')
+ @utils.services('compute', 'network')
def test_schedule_to_all_nodes(self):
- host_client = self.manager.hosts_client
- hosts = host_client.list_hosts()['hosts']
- hosts = [x for x in hosts if x['service'] == 'compute']
+ available_zone = \
+ self.os_admin.availability_zone_client.list_availability_zones(
+ detail=True)['availabilityZoneInfo']
+ hosts = []
+ for zone in available_zone:
+ if zone['zoneState']['available']:
+ for host in zone['hosts']:
+ if 'nova-compute' in zone['hosts'][host] and \
+ zone['hosts'][host]['nova-compute']['available']:
+ hosts.append({'zone': zone['zoneName'],
+ 'host_name': host})
# ensure we have at least as many compute hosts as we expect
if len(hosts) < CONF.compute.min_compute_nodes:
@@ -62,10 +64,15 @@
for host in hosts[:CONF.compute.min_compute_nodes]:
# by getting to active state here, this means this has
# landed on the host in question.
+ # in order to use the availability_zone:host scheduler hint,
+ # admin client is need here.
inst = self.create_server(
- availability_zone='%(zone)s:%(host_name)s' % host,
- wait_until='ACTIVE')
- server = self.servers_client.show_server(inst['id'])['server']
+ clients=self.os_admin,
+ availability_zone='%(zone)s:%(host_name)s' % host)
+ server = self.os_admin.servers_client.show_server(
+ inst['id'])['server']
+ # ensure server is located on the requested host
+ self.assertEqual(host['host_name'], server['OS-EXT-SRV-ATTR:host'])
servers.append(server)
# make sure we really have the number of servers we think we should
diff --git a/tempest/scenario/test_shelve_instance.py b/tempest/scenario/test_shelve_instance.py
index 378ae9d..68f18d1 100644
--- a/tempest/scenario/test_shelve_instance.py
+++ b/tempest/scenario/test_shelve_instance.py
@@ -13,18 +13,17 @@
# License for the specific language governing permissions and limitations
# under the License.
-from oslo_log import log
import testtools
+from tempest.common import compute
+from tempest.common import utils
from tempest.common import waiters
from tempest import config
+from tempest.lib import decorators
from tempest.scenario import manager
-from tempest import test
CONF = config.CONF
-LOG = log.getLogger(__name__)
-
class TestShelveInstance(manager.ScenarioTest):
"""This test shelves then unshelves a Nova instance
@@ -37,19 +36,16 @@
"""
+ @classmethod
+ def skip_checks(cls):
+ super(TestShelveInstance, cls).skip_checks()
+ if not CONF.compute_feature_enabled.shelve:
+ raise cls.skipException("Shelve is not available.")
+
def _shelve_then_unshelve_server(self, server):
- self.servers_client.shelve_server(server['id'])
- offload_time = CONF.compute.shelved_offload_time
- if offload_time >= 0:
- waiters.wait_for_server_status(self.servers_client, server['id'],
- 'SHELVED_OFFLOADED',
- extra_timeout=offload_time)
- else:
- waiters.wait_for_server_status(self.servers_client,
- server['id'], 'SHELVED')
- self.servers_client.shelve_offload_server(server['id'])
- waiters.wait_for_server_status(self.servers_client, server['id'],
- 'SHELVED_OFFLOADED')
+ compute.shelve_server(self.servers_client, server['id'],
+ force_shelve_offload=True)
+
self.servers_client.unshelve_server(server['id'])
waiters.wait_for_server_status(self.servers_client, server['id'],
'ACTIVE')
@@ -60,27 +56,12 @@
security_group = self._create_security_group()
security_groups = [{'name': security_group['name']}]
- if boot_from_volume:
- volume = self.create_volume(size=CONF.volume.volume_size,
- imageRef=CONF.compute.image_ref)
- bd_map = [{
- 'device_name': 'vda',
- 'volume_id': volume['id'],
- 'delete_on_termination': '0'}]
+ server = self.create_server(
+ key_name=keypair['name'],
+ security_groups=security_groups,
+ volume_backed=boot_from_volume)
- server = self.create_server(
- key_name=keypair['name'],
- security_groups=security_groups,
- block_device_mapping=bd_map,
- wait_until='ACTIVE')
- else:
- server = self.create_server(
- image_id=CONF.compute.image_ref,
- key_name=keypair['name'],
- security_groups=security_groups,
- wait_until='ACTIVE')
-
- instance_ip = self.get_server_or_ip(server)
+ instance_ip = self.get_server_ip(server)
timestamp = self.create_timestamp(instance_ip,
private_key=keypair['private_key'])
@@ -93,16 +74,18 @@
private_key=keypair['private_key'])
self.assertEqual(timestamp, timestamp2)
- @test.idempotent_id('1164e700-0af0-4a4c-8792-35909a88743c')
- @testtools.skipUnless(CONF.compute_feature_enabled.shelve,
- 'Shelve is not available.')
- @test.services('compute', 'network', 'image')
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('1164e700-0af0-4a4c-8792-35909a88743c')
+ @testtools.skipUnless(CONF.network.public_network_id,
+ 'The public_network_id option must be specified.')
+ @utils.services('compute', 'network', 'image')
def test_shelve_instance(self):
self._create_server_then_shelve_and_unshelve()
- @test.idempotent_id('c1b6318c-b9da-490b-9c67-9339b627271f')
- @testtools.skipUnless(CONF.compute_feature_enabled.shelve,
- 'Shelve is not available.')
- @test.services('compute', 'volume', 'network', 'image')
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('c1b6318c-b9da-490b-9c67-9339b627271f')
+ @testtools.skipUnless(CONF.network.public_network_id,
+ 'The public_network_id option must be specified.')
+ @utils.services('compute', 'volume', 'network', 'image')
def test_shelve_volume_backed_instance(self):
self._create_server_then_shelve_and_unshelve(boot_from_volume=True)
diff --git a/tempest/scenario/test_snapshot_pattern.py b/tempest/scenario/test_snapshot_pattern.py
index f3b6558..b51a781 100644
--- a/tempest/scenario/test_snapshot_pattern.py
+++ b/tempest/scenario/test_snapshot_pattern.py
@@ -13,17 +13,15 @@
# License for the specific language governing permissions and limitations
# under the License.
-from oslo_log import log
import testtools
+from tempest.common import utils
from tempest import config
+from tempest.lib import decorators
from tempest.scenario import manager
-from tempest import test
CONF = config.CONF
-LOG = log.getLogger(__name__)
-
class TestSnapshotPattern(manager.ScenarioTest):
"""This test is for snapshotting an instance and booting with it.
@@ -36,10 +34,17 @@
"""
- @test.idempotent_id('608e604b-1d63-4a82-8e3e-91bc665c90b4')
- @testtools.skipUnless(CONF.compute_feature_enabled.snapshot,
- 'Snapshotting is not available.')
- @test.services('compute', 'network', 'image')
+ @classmethod
+ def skip_checks(cls):
+ super(TestSnapshotPattern, cls).skip_checks()
+ if not CONF.compute_feature_enabled.snapshot:
+ raise cls.skipException("Snapshotting is not available.")
+
+ @decorators.idempotent_id('608e604b-1d63-4a82-8e3e-91bc665c90b4')
+ @decorators.attr(type='slow')
+ @testtools.skipUnless(CONF.network.public_network_id,
+ 'The public_network_id option must be specified.')
+ @utils.services('compute', 'network', 'image')
def test_snapshot_pattern(self):
# prepare for booting an instance
keypair = self.create_keypair()
@@ -47,12 +52,10 @@
# boot an instance and create a timestamp file in it
server = self.create_server(
- image_id=CONF.compute.image_ref,
key_name=keypair['name'],
- security_groups=[{'name': security_group['name']}],
- wait_until='ACTIVE')
+ security_groups=[{'name': security_group['name']}])
- instance_ip = self.get_server_or_ip(server)
+ instance_ip = self.get_server_ip(server)
timestamp = self.create_timestamp(instance_ip,
private_key=keypair['private_key'])
@@ -63,11 +66,10 @@
server_from_snapshot = self.create_server(
image_id=snapshot_image['id'],
key_name=keypair['name'],
- security_groups=[{'name': security_group['name']}],
- wait_until='ACTIVE')
+ security_groups=[{'name': security_group['name']}])
# check the existence of the timestamp file in the second instance
- server_from_snapshot_ip = self.get_server_or_ip(server_from_snapshot)
+ server_from_snapshot_ip = self.get_server_ip(server_from_snapshot)
timestamp2 = self.get_timestamp(server_from_snapshot_ip,
private_key=keypair['private_key'])
self.assertEqual(timestamp, timestamp2)
diff --git a/tempest/scenario/test_stamp_pattern.py b/tempest/scenario/test_stamp_pattern.py
index faae800..ef369d6 100644
--- a/tempest/scenario/test_stamp_pattern.py
+++ b/tempest/scenario/test_stamp_pattern.py
@@ -13,22 +13,17 @@
# License for the specific language governing permissions and limitations
# under the License.
-import time
-
from oslo_log import log as logging
-from tempest_lib import decorators
-from tempest_lib import exceptions as lib_exc
import testtools
-from tempest.common.utils import data_utils
+from tempest.common import utils
from tempest import config
-from tempest import exceptions
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
from tempest.scenario import manager
-from tempest import test
-import tempest.test
CONF = config.CONF
-
LOG = logging.getLogger(__name__)
@@ -60,111 +55,71 @@
if not CONF.volume_feature_enabled.snapshot:
raise cls.skipException("Cinder volume snapshots are disabled")
- def _wait_for_volume_snapshot_status(self, volume_snapshot, status):
- self.snapshots_client.wait_for_snapshot_status(volume_snapshot['id'],
- status)
-
- def _create_volume_snapshot(self, volume):
- snapshot_name = data_utils.rand_name('scenario-snapshot')
- snapshot = self.snapshots_client.create_snapshot(
- volume['id'], display_name=snapshot_name)['snapshot']
-
- def cleaner():
- self.snapshots_client.delete_snapshot(snapshot['id'])
- try:
- while self.snapshots_client.show_snapshot(
- snapshot['id'])['snapshot']:
- time.sleep(1)
- except lib_exc.NotFound:
- pass
- self.addCleanup(cleaner)
- self._wait_for_volume_status(volume, 'available')
- self.snapshots_client.wait_for_snapshot_status(snapshot['id'],
- 'available')
- self.assertEqual(snapshot_name, snapshot['display_name'])
- return snapshot
-
- def _wait_for_volume_status(self, volume, status):
- self.volumes_client.wait_for_volume_status(volume['id'], status)
-
- def _create_volume(self, snapshot_id=None):
- return self.create_volume(snapshot_id=snapshot_id)
-
- def _attach_volume(self, server, volume):
- attached_volume = self.servers_client.attach_volume(
- server['id'], volumeId=volume['id'], device='/dev/%s'
- % CONF.compute.volume_device_name)['volumeAttachment']
- self.assertEqual(volume['id'], attached_volume['id'])
- self._wait_for_volume_status(attached_volume, 'in-use')
-
- def _detach_volume(self, server, volume):
- self.servers_client.detach_volume(server['id'], volume['id'])
- self._wait_for_volume_status(volume, 'available')
-
- def _wait_for_volume_available_on_the_system(self, server_or_ip,
+ def _wait_for_volume_available_on_the_system(self, ip_address,
private_key):
- ssh = self.get_remote_client(server_or_ip, private_key=private_key)
+ ssh = self.get_remote_client(ip_address, private_key=private_key)
def _func():
- part = ssh.get_partitions()
- LOG.debug("Partitions:%s" % part)
- return CONF.compute.volume_device_name in part
+ disks = ssh.get_disks()
+ LOG.debug("Disks: %s", disks)
+ return CONF.compute.volume_device_name in disks
- if not tempest.test.call_until_true(_func,
- CONF.compute.build_timeout,
- CONF.compute.build_interval):
- raise exceptions.TimeoutException
+ if not test_utils.call_until_true(_func,
+ CONF.compute.build_timeout,
+ CONF.compute.build_interval):
+ raise lib_exc.TimeoutException
- @decorators.skip_because(bug="1205344")
- @test.idempotent_id('10fd234a-515c-41e5-b092-8323060598c5')
+ @decorators.attr(type='slow')
+ @decorators.skip_because(bug="1664793")
+ @decorators.idempotent_id('10fd234a-515c-41e5-b092-8323060598c5')
@testtools.skipUnless(CONF.compute_feature_enabled.snapshot,
'Snapshotting is not available.')
- @tempest.test.services('compute', 'network', 'volume', 'image')
+ @testtools.skipUnless(CONF.network.public_network_id,
+ 'The public_network_id option must be specified.')
+ @utils.services('compute', 'network', 'volume', 'image')
def test_stamp_pattern(self):
# prepare for booting an instance
keypair = self.create_keypair()
security_group = self._create_security_group()
# boot an instance and create a timestamp file in it
- volume = self._create_volume()
+ volume = self.create_volume()
server = self.create_server(
- image_id=CONF.compute.image_ref,
key_name=keypair['name'],
- security_groups=security_group,
- wait_until='ACTIVE')
+ security_groups=[{'name': security_group['name']}])
# create and add floating IP to server1
- ip_for_server = self.get_server_or_ip(server)
+ ip_for_server = self.get_server_ip(server)
- self._attach_volume(server, volume)
+ self.nova_volume_attach(server, volume)
self._wait_for_volume_available_on_the_system(ip_for_server,
keypair['private_key'])
timestamp = self.create_timestamp(ip_for_server,
CONF.compute.volume_device_name,
private_key=keypair['private_key'])
- self._detach_volume(server, volume)
+ self.nova_volume_detach(server, volume)
# snapshot the volume
- volume_snapshot = self._create_volume_snapshot(volume)
+ volume_snapshot = self.create_volume_snapshot(volume['id'])
# snapshot the instance
snapshot_image = self.create_server_snapshot(server=server)
# create second volume from the snapshot(volume2)
- volume_from_snapshot = self._create_volume(
+ volume_from_snapshot = self.create_volume(
snapshot_id=volume_snapshot['id'])
# boot second instance from the snapshot(instance2)
server_from_snapshot = self.create_server(
image_id=snapshot_image['id'],
key_name=keypair['name'],
- security_groups=security_group)
+ security_groups=[{'name': security_group['name']}])
# create and add floating IP to server_from_snapshot
- ip_for_snapshot = self.get_server_or_ip(server_from_snapshot)
+ ip_for_snapshot = self.get_server_ip(server_from_snapshot)
# attach volume2 to instance2
- self._attach_volume(server_from_snapshot, volume_from_snapshot)
+ self.nova_volume_attach(server_from_snapshot, volume_from_snapshot)
self._wait_for_volume_available_on_the_system(ip_for_snapshot,
keypair['private_key'])
diff --git a/tempest/scenario/test_volume_backup_restore.py b/tempest/scenario/test_volume_backup_restore.py
new file mode 100644
index 0000000..8a8c54e
--- /dev/null
+++ b/tempest/scenario/test_volume_backup_restore.py
@@ -0,0 +1,94 @@
+# 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.common import waiters
+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'])
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume['id'], 'available')
+
+ # 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 81ecda0..1564f25 100644
--- a/tempest/scenario/test_volume_boot_pattern.py
+++ b/tempest/scenario/test_volume_boot_pattern.py
@@ -10,57 +10,46 @@
# License for the specific language governing permissions and limitations
# under the License.
-from oslo_log import log
+from oslo_log import log as logging
+import testtools
-from tempest.common.utils import data_utils
+from tempest.common import utils
from tempest.common import waiters
from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
from tempest.scenario import manager
-from tempest import test
CONF = config.CONF
-
-LOG = log.getLogger(__name__)
+LOG = logging.getLogger(__name__)
-class TestVolumeBootPattern(manager.ScenarioTest):
+class TestVolumeBootPattern(manager.EncryptionScenarioTest):
- """This test case attempts to reproduce the following steps:
-
- * Create in Cinder some bootable volume importing a Glance image
- * Boot an instance from the bootable volume
- * Write content to the volume
- * Delete an instance and Boot a new instance from the volume
- * Check written content in the instance
- * Create a volume snapshot while the instance is running
- * Boot an additional instance from the new snapshot based volume
- * Check written content in the instance booted from snapshot
- """
- @classmethod
- def skip_checks(cls):
- super(TestVolumeBootPattern, cls).skip_checks()
- if not CONF.volume_feature_enabled.snapshot:
- raise cls.skipException("Cinder volume snapshots are disabled")
+ # Boot from volume scenario is quite slow, and needs extra
+ # breathing room to get through deletes in the time allotted.
+ TIMEOUT_SCALING_FACTOR = 2
def _create_volume_from_image(self):
img_uuid = CONF.compute.image_ref
- vol_name = data_utils.rand_name('volume-origin')
+ vol_name = data_utils.rand_name(
+ self.__class__.__name__ + '-volume-origin')
return self.create_volume(name=vol_name, imageRef=img_uuid)
- def _get_bdm(self, vol_id, delete_on_termination=False):
- # NOTE(gfidente): the syntax for block_device_mapping is
- # dev_name=id:type:size:delete_on_terminate
- # where type needs to be "snap" if the server is booted
- # from a snapshot, size instead can be safely left empty
- bd_map = [{
- 'device_name': 'vda',
- 'volume_id': vol_id,
- 'delete_on_termination': str(int(delete_on_termination))}]
- return {'block_device_mapping': bd_map}
+ def _get_bdm(self, source_id, source_type, delete_on_termination=False):
+ bd_map_v2 = [{
+ 'uuid': source_id,
+ 'source_type': source_type,
+ 'destination_type': 'volume',
+ 'boot_index': 0,
+ 'delete_on_termination': delete_on_termination}]
+ return {'block_device_mapping_v2': bd_map_v2}
- def _boot_instance_from_volume(self, vol_id, keypair=None,
- security_group=None,
- delete_on_termination=False):
+ def _boot_instance_from_resource(self, source_id,
+ source_type,
+ keypair=None,
+ security_group=None,
+ delete_on_termination=False):
create_kwargs = dict()
if keypair:
create_kwargs['key_name'] = keypair['name']
@@ -68,113 +57,211 @@
create_kwargs['security_groups'] = [
{'name': security_group['name']}]
create_kwargs.update(self._get_bdm(
- vol_id, delete_on_termination=delete_on_termination))
- return self.create_server(
- image='',
- wait_until='ACTIVE',
- **create_kwargs)
+ source_id,
+ source_type,
+ delete_on_termination=delete_on_termination))
- def _create_snapshot_from_volume(self, vol_id):
- snap_name = data_utils.rand_name('snapshot')
- snap = self.snapshots_client.create_snapshot(
- volume_id=vol_id,
- force=True,
- display_name=snap_name)['snapshot']
- self.addCleanup(
- self.snapshots_client.wait_for_resource_deletion, snap['id'])
- self.addCleanup(self.snapshots_client.delete_snapshot, snap['id'])
- self.snapshots_client.wait_for_snapshot_status(snap['id'], 'available')
-
- # NOTE(e0ne): Cinder API v2 uses name instead of display_name
- if 'display_name' in snap:
- self.assertEqual(snap_name, snap['display_name'])
- else:
- self.assertEqual(snap_name, snap['name'])
-
- return snap
-
- def _create_volume_from_snapshot(self, snap_id):
- vol_name = data_utils.rand_name('volume')
- return self.create_volume(name=vol_name, snapshot_id=snap_id)
+ return self.create_server(image_id='', **create_kwargs)
def _delete_server(self, server):
self.servers_client.delete_server(server['id'])
waiters.wait_for_server_termination(self.servers_client, server['id'])
- @test.idempotent_id('557cd2c2-4eb8-4dce-98be-f86765ff311b')
- @test.attr(type='smoke')
- @test.services('compute', 'volume', 'image')
+ @decorators.idempotent_id('557cd2c2-4eb8-4dce-98be-f86765ff311b')
+ # Note: This test is being skipped based on 'public_network_id'.
+ # It is being used in create_floating_ip() method which gets called
+ # from get_server_ip() method
+ @testtools.skipUnless(CONF.network.public_network_id,
+ 'The public_network_id option must be specified.')
+ @testtools.skipUnless(CONF.volume_feature_enabled.snapshot,
+ 'Cinder volume snapshots are disabled')
+ @utils.services('compute', 'volume', 'image')
def test_volume_boot_pattern(self):
+
+ """This test case attempts to reproduce the following steps:
+
+ * Create in Cinder some bootable volume importing a Glance image
+ * Boot an instance from the bootable volume
+ * Write content to the volume
+ * Delete an instance and Boot a new instance from the volume
+ * Check written content in the instance
+ * Create a volume snapshot while the instance is running
+ * Boot an additional instance from the new snapshot based volume
+ * Check written content in the instance booted from snapshot
+ """
+
+ LOG.info("Creating keypair and security group")
keypair = self.create_keypair()
security_group = self._create_security_group()
# create an instance from volume
+ LOG.info("Booting instance 1 from volume")
volume_origin = self._create_volume_from_image()
- instance_1st = self._boot_instance_from_volume(volume_origin['id'],
- keypair, security_group)
+ instance_1st = self._boot_instance_from_resource(
+ source_id=volume_origin['id'],
+ source_type='volume',
+ keypair=keypair,
+ security_group=security_group)
+ LOG.info("Booted first instance: %s", instance_1st)
# write content to volume on instance
- ip_instance_1st = self.get_server_or_ip(instance_1st)
+ LOG.info("Setting timestamp in instance %s", instance_1st)
+ ip_instance_1st = self.get_server_ip(instance_1st)
timestamp = self.create_timestamp(ip_instance_1st,
private_key=keypair['private_key'])
# delete instance
+ LOG.info("Deleting first instance: %s", instance_1st)
self._delete_server(instance_1st)
# create a 2nd instance from volume
- instance_2nd = self._boot_instance_from_volume(volume_origin['id'],
- keypair, security_group)
+ instance_2nd = self._boot_instance_from_resource(
+ source_id=volume_origin['id'],
+ source_type='volume',
+ keypair=keypair,
+ security_group=security_group)
+ LOG.info("Booted second instance %s", instance_2nd)
# check the content of written file
- ip_instance_2nd = self.get_server_or_ip(instance_2nd)
+ LOG.info("Getting timestamp in instance %s", instance_2nd)
+ ip_instance_2nd = self.get_server_ip(instance_2nd)
timestamp2 = self.get_timestamp(ip_instance_2nd,
private_key=keypair['private_key'])
self.assertEqual(timestamp, timestamp2)
# snapshot a volume
- snapshot = self._create_snapshot_from_volume(volume_origin['id'])
+ LOG.info("Creating snapshot from volume: %s", volume_origin['id'])
+ snapshot = self.create_volume_snapshot(volume_origin['id'], force=True)
# create a 3rd instance from snapshot
- volume = self._create_volume_from_snapshot(snapshot['id'])
+ LOG.info("Creating third instance from snapshot: %s", snapshot['id'])
+ volume = self.create_volume(snapshot_id=snapshot['id'],
+ size=snapshot['size'])
+ LOG.info("Booting third instance from snapshot")
server_from_snapshot = (
- self._boot_instance_from_volume(volume['id'],
- keypair, security_group))
+ self._boot_instance_from_resource(source_id=volume['id'],
+ source_type='volume',
+ keypair=keypair,
+ security_group=security_group))
+ LOG.info("Booted third instance %s", server_from_snapshot)
# check the content of written file
- server_from_snapshot_ip = self.get_server_or_ip(server_from_snapshot)
+ LOG.info("Logging into third instance to get timestamp: %s",
+ server_from_snapshot)
+ server_from_snapshot_ip = self.get_server_ip(server_from_snapshot)
timestamp3 = self.get_timestamp(server_from_snapshot_ip,
private_key=keypair['private_key'])
self.assertEqual(timestamp, timestamp3)
- @test.idempotent_id('36c34c67-7b54-4b59-b188-02a2f458a63b')
- @test.services('compute', 'volume', 'image')
- def test_create_ebs_image_and_check_boot(self):
- # create an instance from volume
+ @decorators.idempotent_id('05795fb2-b2a7-4c9f-8fac-ff25aedb1489')
+ @decorators.attr(type='slow')
+ @testtools.skipUnless(CONF.volume_feature_enabled.snapshot,
+ 'Cinder volume snapshots are disabled')
+ @utils.services('compute', 'image', 'volume')
+ def test_create_server_from_volume_snapshot(self):
+ # Create a volume from an image
+ boot_volume = self._create_volume_from_image()
+
+ # Create a snapshot
+ boot_snapshot = self.create_volume_snapshot(boot_volume['id'])
+
+ # Create a server from a volume snapshot
+ server = self._boot_instance_from_resource(
+ source_id=boot_snapshot['id'],
+ source_type='snapshot',
+ delete_on_termination=True)
+
+ server_info = self.servers_client.show_server(server['id'])['server']
+
+ # The created volume when creating a server from a snapshot
+ created_volume = server_info['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 snapshot
+ self.assertEqual(
+ boot_volume['volume_image_metadata']['image_id'],
+ created_volume_info['volume_image_metadata']['image_id'])
+ self.assertEqual(boot_snapshot['id'],
+ created_volume_info['snapshot_id'])
+ self.assertEqual(server['id'],
+ created_volume_info['attachments'][0]['server_id'])
+ self.assertEqual(created_volume[0]['id'],
+ created_volume_info['attachments'][0]['volume_id'])
+
+ @decorators.idempotent_id('36c34c67-7b54-4b59-b188-02a2f458a63b')
+ @testtools.skipUnless(CONF.volume_feature_enabled.snapshot,
+ 'Cinder volume snapshots are disabled')
+ @utils.services('compute', 'volume', 'image')
+ 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_volume(volume_origin['id'],
- delete_on_termination=True)
- # create EBS image
- name = data_utils.rand_name('image')
- image = self.create_server_snapshot(instance, name=name)
+ instance = self._boot_instance_from_resource(
+ source_id=volume_origin['id'],
+ source_type='volume',
+ delete_on_termination=True)
+ # 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
- instance = self.create_server(
- image_id=image['id'])
- # just ensure that instance booted
+ # 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'])
- # delete instance
+ # 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['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['volume_image_metadata']['image_id'])
+
+ # 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'])
-class TestVolumeBootPatternV2(TestVolumeBootPattern):
- def _get_bdm(self, vol_id, delete_on_termination=False):
- bd_map_v2 = [{
- 'uuid': vol_id,
- 'source_type': 'volume',
- 'destination_type': 'volume',
- 'boot_index': 0,
- 'delete_on_termination': delete_on_termination}]
- return {'block_device_mapping_v2': bd_map_v2}
+ @decorators.idempotent_id('cb78919a-e553-4bab-b73b-10cf4d2eb125')
+ @testtools.skipUnless(CONF.compute_feature_enabled.attach_encrypted_volume,
+ 'Encrypted volume attach is not supported')
+ @utils.services('compute', 'volume')
+ def test_boot_server_from_encrypted_volume_luks(self):
+ # Create an encrypted volume
+ volume = self.create_encrypted_volume('nova.volume.encryptors.'
+ 'luks.LuksEncryptor',
+ volume_type='luks')
+
+ self.volumes_client.set_bootable_volume(volume['id'], bootable=True)
+
+ # Boot a server from the encrypted volume
+ server = self._boot_instance_from_resource(
+ source_id=volume['id'],
+ source_type='volume',
+ delete_on_termination=False)
+
+ server_info = self.servers_client.show_server(server['id'])['server']
+ created_volume = server_info['os-extended-volumes:volumes_attached']
+ self.assertEqual(volume['id'], created_volume[0]['id'])
diff --git a/tempest/scenario/test_volume_migrate_attached.py b/tempest/scenario/test_volume_migrate_attached.py
new file mode 100644
index 0000000..ff7996a
--- /dev/null
+++ b/tempest/scenario/test_volume_migrate_attached.py
@@ -0,0 +1,129 @@
+# 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_log import log as logging
+
+from tempest.common import utils
+from tempest.common import waiters
+from tempest import config
+from tempest.lib import decorators
+from tempest.scenario import manager
+
+CONF = config.CONF
+LOG = logging.getLogger(__name__)
+
+
+class TestVolumeMigrateRetypeAttached(manager.ScenarioTest):
+
+ """This test case attempts to reproduce the following steps:
+
+ * Create 2 volume types representing 2 different backends
+ * Create in Cinder some bootable volume importing a Glance image using
+ * volume_type_1
+ * Boot an instance from the bootable volume
+ * Write to the volume
+ * Perform a cinder retype --on-demand of the volume to type of backend #2
+ * Check written content of migrated volume
+ """
+
+ credentials = ['primary', 'admin']
+
+ @classmethod
+ def setup_clients(cls):
+ super(TestVolumeMigrateRetypeAttached, cls).setup_clients()
+ cls.admin_volumes_client = cls.os_admin.volumes_v2_client
+
+ @classmethod
+ def skip_checks(cls):
+ super(TestVolumeMigrateRetypeAttached, cls).skip_checks()
+ if not CONF.volume_feature_enabled.multi_backend:
+ raise cls.skipException("Cinder multi-backend feature disabled")
+
+ if len(set(CONF.volume.backend_names)) < 2:
+ raise cls.skipException("Requires at least two different "
+ "backend names")
+
+ def _boot_instance_from_volume(self, vol_id, keypair, security_group):
+
+ key_name = keypair['name']
+ security_groups = [{'name': security_group['name']}]
+ block_device_mapping = [{'device_name': 'vda', 'volume_id': vol_id,
+ 'delete_on_termination': False}]
+
+ return self.create_server(image_id='',
+ key_name=key_name,
+ security_groups=security_groups,
+ block_device_mapping=block_device_mapping)
+
+ def _create_volume_types(self):
+ backend_names = CONF.volume.backend_names
+
+ backend_source = backend_names[0]
+ backend_dest = backend_names[1]
+
+ source_body = self.create_volume_type(backend_name=backend_source)
+ dest_body = self.create_volume_type(backend_name=backend_dest)
+
+ LOG.info("Created Volume types: %(src)s -> %(src_backend)s, %(dst)s "
+ "-> %(dst_backend)s", {'src': source_body['name'],
+ 'src_backend': backend_source,
+ 'dst': dest_body['name'],
+ 'dst_backend': backend_dest})
+ return source_body['name'], dest_body['name']
+
+ def _volume_retype_with_migration(self, volume_id, new_volume_type):
+ # NOTE: The 'on-demand' migration requires admin operation, so
+ # admin_volumes_client() should be used here.
+ migration_policy = 'on-demand'
+ self.admin_volumes_client.retype_volume(
+ volume_id, new_type=new_volume_type,
+ migration_policy=migration_policy)
+ waiters.wait_for_volume_retype(self.volumes_client,
+ volume_id, new_volume_type)
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('deadd2c2-beef-4dce-98be-f86765ff311b')
+ @utils.services('compute', 'volume')
+ def test_volume_migrate_attached(self):
+ LOG.info("Creating keypair and security group")
+ keypair = self.create_keypair()
+ security_group = self._create_security_group()
+
+ # create volume types
+ LOG.info("Creating Volume types")
+ source_type, dest_type = self._create_volume_types()
+
+ # create an instance from volume
+ LOG.info("Booting instance from volume")
+ volume_origin = self.create_volume(imageRef=CONF.compute.image_ref,
+ volume_type=source_type)
+
+ instance = self._boot_instance_from_volume(volume_origin['id'],
+ keypair, security_group)
+
+ # write content to volume on instance
+ LOG.info("Setting timestamp in instance %s", instance['id'])
+ ip_instance = self.get_server_ip(instance)
+ timestamp = self.create_timestamp(ip_instance,
+ private_key=keypair['private_key'])
+
+ # retype volume with migration from backend #1 to backend #2
+ LOG.info("Retyping Volume %s to new type %s", volume_origin['id'],
+ dest_type)
+ self._volume_retype_with_migration(volume_origin['id'], dest_type)
+
+ # check the content of written file
+ LOG.info("Getting timestamp in postmigrated instance %s",
+ instance['id'])
+ timestamp2 = self.get_timestamp(ip_instance,
+ private_key=keypair['private_key'])
+ self.assertEqual(timestamp, timestamp2)
diff --git a/tempest/scenario/utils.py b/tempest/scenario/utils.py
deleted file mode 100644
index fa7c0c9..0000000
--- a/tempest/scenario/utils.py
+++ /dev/null
@@ -1,184 +0,0 @@
-# Copyright 2013 Hewlett-Packard, Ltd.
-#
-# 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 re
-import string
-import unicodedata
-
-from oslo_serialization import jsonutils as json
-from tempest_lib.common.utils import misc
-from tempest_lib import exceptions as exc_lib
-import testscenarios
-import testtools
-
-from tempest import clients
-from tempest.common import credentials_factory as credentials
-from tempest import config
-
-CONF = config.CONF
-
-
-class ImageUtils(object):
-
- default_ssh_user = 'root'
-
- def __init__(self, os):
- # Load configuration items
- self.ssh_users = json.loads(CONF.input_scenario.ssh_user_regex)
- self.non_ssh_image_pattern = \
- CONF.input_scenario.non_ssh_image_regex
- # Setup clients
- self.images_client = os.images_client
- self.flavors_client = os.flavors_client
-
- def ssh_user(self, image_id):
- _image = self.images_client.show_image(image_id)['image']
- for regex, user in self.ssh_users:
- # First match wins
- if re.match(regex, _image['name']) is not None:
- return user
- else:
- return self.default_ssh_user
-
- def _is_sshable_image(self, image):
- return not re.search(pattern=self.non_ssh_image_pattern,
- string=str(image['name']))
-
- def is_sshable_image(self, image_id):
- _image = self.images_client.show_image(image_id)['image']
- return self._is_sshable_image(_image)
-
- def _is_flavor_enough(self, flavor, image):
- return image['minDisk'] <= flavor['disk']
-
- def is_flavor_enough(self, flavor_id, image_id):
- _image = self.images_client.show_image(image_id)['image']
- _flavor = self.flavors_client.show_flavor(flavor_id)['flavor']
- return self._is_flavor_enough(_flavor, _image)
-
-
-@misc.singleton
-class InputScenarioUtils(object):
-
- """Example usage:
-
- import testscenarios
- (...)
- load_tests = testscenarios.load_tests_apply_scenarios
-
-
- class TestInputScenario(manager.ScenarioTest):
-
- scenario_utils = utils.InputScenarioUtils()
- scenario_flavor = scenario_utils.scenario_flavors
- scenario_image = scenario_utils.scenario_images
- scenarios = testscenarios.multiply_scenarios(scenario_image,
- scenario_flavor)
-
- def test_create_server_metadata(self):
- name = rand_name('instance')
- self.servers_client.create_server(name=name,
- flavorRef=self.flavor_ref,
- imageRef=self.image_ref)
- """
- validchars = "-_.{ascii}{digit}".format(ascii=string.ascii_letters,
- digit=string.digits)
-
- def __init__(self):
- network_resources = {
- 'network': False,
- 'router': False,
- 'subnet': False,
- 'dhcp': False,
- }
- self.cred_provider = credentials.get_credentials_provider(
- name='InputScenarioUtils',
- identity_version=CONF.identity.auth_version,
- network_resources=network_resources)
- os = clients.Manager(self.cred_provider.get_primary_creds())
- self.images_client = os.images_client
- self.flavors_client = os.flavors_client
- self.image_pattern = CONF.input_scenario.image_regex
- self.flavor_pattern = CONF.input_scenario.flavor_regex
-
- def _normalize_name(self, name):
- nname = unicodedata.normalize('NFKD', name).encode('ASCII', 'ignore')
- nname = ''.join(c for c in nname if c in self.validchars)
- return nname
-
- def clear_creds(self):
- self.cred_provider.clear_creds()
-
- @property
- def scenario_images(self):
- """:return: a scenario with name and uuid of images"""
- if not CONF.service_available.glance:
- return []
- if not hasattr(self, '_scenario_images'):
- try:
- images = self.images_client.list_images()['images']
- self._scenario_images = [
- (self._normalize_name(i['name']), dict(image_ref=i['id']))
- for i in images if re.search(self.image_pattern,
- str(i['name']))
- ]
- except Exception:
- self._scenario_images = []
- return self._scenario_images
-
- @property
- def scenario_flavors(self):
- """:return: a scenario with name and uuid of flavors"""
- if not hasattr(self, '_scenario_flavors'):
- try:
- flavors = self.flavors_client.list_flavors()['flavors']
- self._scenario_flavors = [
- (self._normalize_name(f['name']), dict(flavor_ref=f['id']))
- for f in flavors if re.search(self.flavor_pattern,
- str(f['name']))
- ]
- except Exception:
- self._scenario_flavors = []
- return self._scenario_flavors
-
-
-def load_tests_input_scenario_utils(*args):
- """Wrapper for testscenarios to set the scenarios
-
- The purpose is to avoid running a getattr on the CONF object at import.
- """
-
- if getattr(args[0], 'suiteClass', None) is not None:
- loader, standard_tests, pattern = args
- else:
- standard_tests, module, loader = args
- output = None
- scenario_utils = None
- try:
- scenario_utils = InputScenarioUtils()
- scenario_flavor = scenario_utils.scenario_flavors
- scenario_image = scenario_utils.scenario_images
- except (exc_lib.InvalidCredentials, TypeError):
- output = standard_tests
- finally:
- if scenario_utils:
- scenario_utils.clear_creds()
- if output is not None:
- return output
- for test in testtools.iterate_tests(standard_tests):
- setattr(test, 'scenarios', testscenarios.multiply_scenarios(
- scenario_image,
- scenario_flavor))
- return testscenarios.load_tests_apply_scenarios(*args)
diff --git a/tempest/services/baremetal/__init__.py b/tempest/services/baremetal/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/services/baremetal/__init__.py
+++ /dev/null
diff --git a/tempest/services/baremetal/base.py b/tempest/services/baremetal/base.py
deleted file mode 100644
index d8cb99d..0000000
--- a/tempest/services/baremetal/base.py
+++ /dev/null
@@ -1,205 +0,0 @@
-# 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 functools
-
-from oslo_serialization import jsonutils as json
-import six
-from six.moves.urllib import parse as urllib
-
-from tempest.common import service_client
-
-
-def handle_errors(f):
- """A decorator that allows to ignore certain types of errors."""
-
- @functools.wraps(f)
- def wrapper(*args, **kwargs):
- param_name = 'ignore_errors'
- ignored_errors = kwargs.get(param_name, tuple())
-
- if param_name in kwargs:
- del kwargs[param_name]
-
- try:
- return f(*args, **kwargs)
- except ignored_errors:
- # Silently ignore errors
- pass
-
- return wrapper
-
-
-class BaremetalClient(service_client.ServiceClient):
- """Base Tempest REST client for Ironic API."""
-
- uri_prefix = ''
-
- def serialize(self, object_dict):
- """Serialize an Ironic object."""
-
- return json.dumps(object_dict)
-
- def deserialize(self, object_str):
- """Deserialize an Ironic object."""
-
- return json.loads(object_str)
-
- def _get_uri(self, resource_name, uuid=None, permanent=False):
- """Get URI for a specific resource or object.
-
- :param resource_name: The name of the REST resource, e.g., 'nodes'.
- :param uuid: The unique identifier of an object in UUID format.
- :returns: Relative URI for the resource or object.
-
- """
- prefix = self.uri_prefix if not permanent else ''
-
- return '{pref}/{res}{uuid}'.format(pref=prefix,
- res=resource_name,
- uuid='/%s' % uuid if uuid else '')
-
- def _make_patch(self, allowed_attributes, **kwargs):
- """Create a JSON patch according to RFC 6902.
-
- :param allowed_attributes: An iterable object that contains a set of
- allowed attributes for an object.
- :param **kwargs: Attributes and new values for them.
- :returns: A JSON path that sets values of the specified attributes to
- the new ones.
-
- """
- def get_change(kwargs, path='/'):
- for name, value in six.iteritems(kwargs):
- if isinstance(value, dict):
- for ch in get_change(value, path + '%s/' % name):
- yield ch
- else:
- if value is None:
- yield {'path': path + name,
- 'op': 'remove'}
- else:
- yield {'path': path + name,
- 'value': value,
- 'op': 'replace'}
-
- patch = [ch for ch in get_change(kwargs)
- if ch['path'].lstrip('/') in allowed_attributes]
-
- return patch
-
- def _list_request(self, resource, permanent=False, **kwargs):
- """Get the list of objects of the specified type.
-
- :param resource: The name of the REST resource, e.g., 'nodes'.
- :param **kwargs: Parameters for the request.
- :returns: A tuple with the server response and deserialized JSON list
- of objects
-
- """
- uri = self._get_uri(resource, permanent=permanent)
- if kwargs:
- uri += "?%s" % urllib.urlencode(kwargs)
-
- resp, body = self.get(uri)
- self.expected_success(200, resp['status'])
-
- return resp, self.deserialize(body)
-
- def _show_request(self, resource, uuid, permanent=False, **kwargs):
- """Gets a specific object of the specified type.
-
- :param uuid: Unique identifier of the object in UUID format.
- :returns: Serialized object as a dictionary.
-
- """
- if 'uri' in kwargs:
- uri = kwargs['uri']
- else:
- uri = self._get_uri(resource, uuid=uuid, permanent=permanent)
- resp, body = self.get(uri)
- self.expected_success(200, resp['status'])
-
- return resp, self.deserialize(body)
-
- def _create_request(self, resource, object_dict):
- """Create an object of the specified type.
-
- :param resource: The name of the REST resource, e.g., 'nodes'.
- :param object_dict: A Python dict that represents an object of the
- specified type.
- :returns: A tuple with the server response and the deserialized created
- object.
-
- """
- body = self.serialize(object_dict)
- uri = self._get_uri(resource)
-
- resp, body = self.post(uri, body=body)
- self.expected_success(201, resp['status'])
-
- return resp, self.deserialize(body)
-
- def _delete_request(self, resource, uuid):
- """Delete specified object.
-
- :param resource: The name of the REST resource, e.g., 'nodes'.
- :param uuid: The unique identifier of an object in UUID format.
- :returns: A tuple with the server response and the response body.
-
- """
- uri = self._get_uri(resource, uuid)
-
- resp, body = self.delete(uri)
- self.expected_success(204, resp['status'])
- return resp, body
-
- def _patch_request(self, resource, uuid, patch_object):
- """Update specified object with JSON-patch.
-
- :param resource: The name of the REST resource, e.g., 'nodes'.
- :param uuid: The unique identifier of an object in UUID format.
- :returns: A tuple with the server response and the serialized patched
- object.
-
- """
- uri = self._get_uri(resource, uuid)
- patch_body = json.dumps(patch_object)
-
- resp, body = self.patch(uri, body=patch_body)
- self.expected_success(200, resp['status'])
- return resp, self.deserialize(body)
-
- @handle_errors
- def get_api_description(self):
- """Retrieves all versions of the Ironic API."""
-
- return self._list_request('', permanent=True)
-
- @handle_errors
- def get_version_description(self, version='v1'):
- """Retrieves the desctription of the API.
-
- :param version: The version of the API. Default: 'v1'.
- :returns: Serialized description of API resources.
-
- """
- return self._list_request(version, permanent=True)
-
- def _put_request(self, resource, put_object):
- """Update specified object with JSON-patch."""
- uri = self._get_uri(resource)
- put_body = json.dumps(put_object)
-
- resp, body = self.put(uri, body=put_body)
- self.expected_success(202, resp['status'])
- return resp, body
diff --git a/tempest/services/baremetal/v1/__init__.py b/tempest/services/baremetal/v1/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/services/baremetal/v1/__init__.py
+++ /dev/null
diff --git a/tempest/services/baremetal/v1/json/__init__.py b/tempest/services/baremetal/v1/json/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/services/baremetal/v1/json/__init__.py
+++ /dev/null
diff --git a/tempest/services/baremetal/v1/json/baremetal_client.py b/tempest/services/baremetal/v1/json/baremetal_client.py
deleted file mode 100644
index f24ef68..0000000
--- a/tempest/services/baremetal/v1/json/baremetal_client.py
+++ /dev/null
@@ -1,349 +0,0 @@
-# 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.services.baremetal import base
-
-
-class BaremetalClient(base.BaremetalClient):
- """Base Tempest REST client for Ironic API v1."""
- version = '1'
- uri_prefix = 'v1'
-
- @base.handle_errors
- def list_nodes(self, **kwargs):
- """List all existing nodes."""
- return self._list_request('nodes', **kwargs)
-
- @base.handle_errors
- def list_chassis(self):
- """List all existing chassis."""
- return self._list_request('chassis')
-
- @base.handle_errors
- def list_chassis_nodes(self, chassis_uuid):
- """List all nodes associated with a chassis."""
- return self._list_request('/chassis/%s/nodes' % chassis_uuid)
-
- @base.handle_errors
- def list_ports(self, **kwargs):
- """List all existing ports."""
- return self._list_request('ports', **kwargs)
-
- @base.handle_errors
- def list_node_ports(self, uuid):
- """List all ports associated with the node."""
- return self._list_request('/nodes/%s/ports' % uuid)
-
- @base.handle_errors
- def list_nodestates(self, uuid):
- """List all existing states."""
- return self._list_request('/nodes/%s/states' % uuid)
-
- @base.handle_errors
- def list_ports_detail(self, **kwargs):
- """Details list all existing ports."""
- return self._list_request('/ports/detail', **kwargs)
-
- @base.handle_errors
- def list_drivers(self):
- """List all existing drivers."""
- return self._list_request('drivers')
-
- @base.handle_errors
- def show_node(self, uuid):
- """Gets a specific node.
-
- :param uuid: Unique identifier of the node in UUID format.
- :return: Serialized node as a dictionary.
-
- """
- return self._show_request('nodes', uuid)
-
- @base.handle_errors
- def show_node_by_instance_uuid(self, instance_uuid):
- """Gets a node associated with given instance uuid.
-
- :param uuid: Unique identifier of the node in UUID format.
- :return: Serialized node as a dictionary.
-
- """
- uri = '/nodes/detail?instance_uuid=%s' % instance_uuid
-
- return self._show_request('nodes',
- uuid=None,
- uri=uri)
-
- @base.handle_errors
- def show_chassis(self, uuid):
- """Gets a specific chassis.
-
- :param uuid: Unique identifier of the chassis in UUID format.
- :return: Serialized chassis as a dictionary.
-
- """
- return self._show_request('chassis', uuid)
-
- @base.handle_errors
- def show_port(self, uuid):
- """Gets a specific port.
-
- :param uuid: Unique identifier of the port in UUID format.
- :return: Serialized port as a dictionary.
-
- """
- return self._show_request('ports', uuid)
-
- @base.handle_errors
- def show_port_by_address(self, address):
- """Gets a specific port by address.
-
- :param address: MAC address of the port.
- :return: Serialized port as a dictionary.
-
- """
- uri = '/ports/detail?address=%s' % address
-
- return self._show_request('ports', uuid=None, uri=uri)
-
- def show_driver(self, driver_name):
- """Gets a specific driver.
-
- :param driver_name: Name of driver.
- :return: Serialized driver as a dictionary.
- """
- return self._show_request('drivers', driver_name)
-
- @base.handle_errors
- def create_node(self, chassis_id=None, **kwargs):
- """Create a baremetal node with the specified parameters.
-
- :param cpu_arch: CPU architecture of the node. Default: x86_64.
- :param cpus: Number of CPUs. Default: 8.
- :param local_gb: Disk size. Default: 1024.
- :param memory_mb: Available RAM. Default: 4096.
- :param driver: Driver name. Default: "fake"
- :return: A tuple with the server response and the created node.
-
- """
- node = {'chassis_uuid': chassis_id,
- 'properties': {'cpu_arch': kwargs.get('cpu_arch', 'x86_64'),
- 'cpus': kwargs.get('cpus', 8),
- 'local_gb': kwargs.get('local_gb', 1024),
- 'memory_mb': kwargs.get('memory_mb', 4096)},
- 'driver': kwargs.get('driver', 'fake')}
-
- return self._create_request('nodes', node)
-
- @base.handle_errors
- def create_chassis(self, **kwargs):
- """Create a chassis with the specified parameters.
-
- :param description: The description of the chassis.
- Default: test-chassis
- :return: A tuple with the server response and the created chassis.
-
- """
- chassis = {'description': kwargs.get('description', 'test-chassis')}
-
- return self._create_request('chassis', chassis)
-
- @base.handle_errors
- def create_port(self, node_id, **kwargs):
- """Create a port with the specified parameters.
-
- :param node_id: The ID of the node which owns the port.
- :param address: MAC address of the port.
- :param extra: Meta data of the port. Default: {'foo': 'bar'}.
- :param uuid: UUID of the port.
- :return: A tuple with the server response and the created port.
-
- """
- port = {'extra': kwargs.get('extra', {'foo': 'bar'}),
- 'uuid': kwargs['uuid']}
-
- if node_id is not None:
- port['node_uuid'] = node_id
-
- if kwargs['address'] is not None:
- port['address'] = kwargs['address']
-
- return self._create_request('ports', port)
-
- @base.handle_errors
- def delete_node(self, uuid):
- """Deletes a node having the specified UUID.
-
- :param uuid: The unique identifier of the node.
- :return: A tuple with the server response and the response body.
-
- """
- return self._delete_request('nodes', uuid)
-
- @base.handle_errors
- def delete_chassis(self, uuid):
- """Deletes a chassis having the specified UUID.
-
- :param uuid: The unique identifier of the chassis.
- :return: A tuple with the server response and the response body.
-
- """
- return self._delete_request('chassis', uuid)
-
- @base.handle_errors
- def delete_port(self, uuid):
- """Deletes a port having the specified UUID.
-
- :param uuid: The unique identifier of the port.
- :return: A tuple with the server response and the response body.
-
- """
- return self._delete_request('ports', uuid)
-
- @base.handle_errors
- def update_node(self, uuid, **kwargs):
- """Update the specified node.
-
- :param uuid: The unique identifier of the node.
- :return: A tuple with the server response and the updated node.
-
- """
- node_attributes = ('properties/cpu_arch',
- 'properties/cpus',
- 'properties/local_gb',
- 'properties/memory_mb',
- 'driver',
- 'instance_uuid')
-
- patch = self._make_patch(node_attributes, **kwargs)
-
- return self._patch_request('nodes', uuid, patch)
-
- @base.handle_errors
- def update_chassis(self, uuid, **kwargs):
- """Update the specified chassis.
-
- :param uuid: The unique identifier of the chassis.
- :return: A tuple with the server response and the updated chassis.
-
- """
- chassis_attributes = ('description',)
- patch = self._make_patch(chassis_attributes, **kwargs)
-
- return self._patch_request('chassis', uuid, patch)
-
- @base.handle_errors
- def update_port(self, uuid, patch):
- """Update the specified port.
-
- :param uuid: The unique identifier of the port.
- :param patch: List of dicts representing json patches.
- :return: A tuple with the server response and the updated port.
-
- """
-
- return self._patch_request('ports', uuid, patch)
-
- @base.handle_errors
- def set_node_power_state(self, node_uuid, state):
- """Set power state of the specified node.
-
- :param node_uuid: The unique identifier of the node.
- :state: desired state to set (on/off/reboot).
-
- """
- target = {'target': state}
- return self._put_request('nodes/%s/states/power' % node_uuid,
- target)
-
- @base.handle_errors
- def validate_driver_interface(self, node_uuid):
- """Get all driver interfaces of a specific node.
-
- :param uuid: Unique identifier of the node in UUID format.
-
- """
-
- uri = '{pref}/{res}/{uuid}/{postf}'.format(pref=self.uri_prefix,
- res='nodes',
- uuid=node_uuid,
- postf='validate')
-
- return self._show_request('nodes', node_uuid, uri=uri)
-
- @base.handle_errors
- def set_node_boot_device(self, node_uuid, boot_device, persistent=False):
- """Set the boot device of the specified node.
-
- :param node_uuid: The unique identifier of the node.
- :param boot_device: The boot device name.
- :param persistent: Boolean value. True if the boot device will
- persist to all future boots, False if not.
- Default: False.
-
- """
- request = {'boot_device': boot_device, 'persistent': persistent}
- resp, body = self._put_request('nodes/%s/management/boot_device' %
- node_uuid, request)
- self.expected_success(204, resp.status)
- return body
-
- @base.handle_errors
- def get_node_boot_device(self, node_uuid):
- """Get the current boot device of the specified node.
-
- :param node_uuid: The unique identifier of the node.
-
- """
- path = 'nodes/%s/management/boot_device' % node_uuid
- resp, body = self._list_request(path)
- self.expected_success(200, resp.status)
- return body
-
- @base.handle_errors
- def get_node_supported_boot_devices(self, node_uuid):
- """Get the supported boot devices of the specified node.
-
- :param node_uuid: The unique identifier of the node.
-
- """
- path = 'nodes/%s/management/boot_device/supported' % node_uuid
- resp, body = self._list_request(path)
- self.expected_success(200, resp.status)
- return body
-
- @base.handle_errors
- def get_console(self, node_uuid):
- """Get connection information about the console.
-
- :param node_uuid: Unique identifier of the node in UUID format.
-
- """
-
- resp, body = self._show_request('nodes/states/console', node_uuid)
- self.expected_success(200, resp.status)
- return resp, body
-
- @base.handle_errors
- def set_console_mode(self, node_uuid, enabled):
- """Start and stop the node console.
-
- :param node_uuid: Unique identifier of the node in UUID format.
- :param enabled: Boolean value; whether to enable or disable the
- console.
-
- """
-
- enabled = {'enabled': enabled}
- resp, body = self._put_request('nodes/%s/states/console' % node_uuid,
- enabled)
- self.expected_success(202, resp.status)
- return resp, body
diff --git a/tempest/services/compute/__init__.py b/tempest/services/compute/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/services/compute/__init__.py
+++ /dev/null
diff --git a/tempest/services/compute/json/__init__.py b/tempest/services/compute/json/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/services/compute/json/__init__.py
+++ /dev/null
diff --git a/tempest/services/compute/json/base.py b/tempest/services/compute/json/base.py
deleted file mode 100644
index ea1a696..0000000
--- a/tempest/services/compute/json/base.py
+++ /dev/null
@@ -1,28 +0,0 @@
-# Copyright 2015 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.common import rest_client
-
-
-class BaseComputeClient(rest_client.RestClient):
- api_microversion = None
-
- def get_headers(self):
- headers = super(BaseComputeClient, self).get_headers()
- if self.api_microversion:
- headers['X-OpenStack-Nova-API-Version'] = self.api_microversion
- return headers
-
- def set_api_microversion(self, microversion):
- self.api_microversion = microversion
diff --git a/tempest/services/compute/json/keypairs_client.py b/tempest/services/compute/json/keypairs_client.py
deleted file mode 100644
index 2e22bc6..0000000
--- a/tempest/services/compute/json/keypairs_client.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# 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.api_schema.response.compute.v2_1 import keypairs as schema
-from tempest.common import service_client
-
-
-class KeyPairsClient(service_client.ServiceClient):
-
- def list_keypairs(self):
- resp, body = self.get("os-keypairs")
- body = json.loads(body)
- self.validate_response(schema.list_keypairs, resp, body)
- return service_client.ResponseBody(resp, body)
-
- def show_keypair(self, keypair_name):
- resp, body = self.get("os-keypairs/%s" % keypair_name)
- body = json.loads(body)
- self.validate_response(schema.get_keypair, resp, body)
- return service_client.ResponseBody(resp, body)
-
- def create_keypair(self, **kwargs):
- post_body = json.dumps({'keypair': kwargs})
- resp, body = self.post("os-keypairs", body=post_body)
- body = json.loads(body)
- self.validate_response(schema.create_keypair, resp, body)
- return service_client.ResponseBody(resp, body)
-
- def delete_keypair(self, keypair_name):
- resp, body = self.delete("os-keypairs/%s" % keypair_name)
- self.validate_response(schema.delete_keypair, resp, body)
- return service_client.ResponseBody(resp, body)
diff --git a/tempest/services/data_processing/__init__.py b/tempest/services/data_processing/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/services/data_processing/__init__.py
+++ /dev/null
diff --git a/tempest/services/data_processing/v1_1/__init__.py b/tempest/services/data_processing/v1_1/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/services/data_processing/v1_1/__init__.py
+++ /dev/null
diff --git a/tempest/services/data_processing/v1_1/data_processing_client.py b/tempest/services/data_processing/v1_1/data_processing_client.py
deleted file mode 100644
index 5aa2622..0000000
--- a/tempest/services/data_processing/v1_1/data_processing_client.py
+++ /dev/null
@@ -1,280 +0,0 @@
-# Copyright (c) 2013 Mirantis Inc.
-#
-# 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.common import service_client
-
-
-class DataProcessingClient(service_client.ServiceClient):
-
- def _request_and_check_resp(self, request_func, uri, resp_status):
- """Make a request and check response status code.
-
- It returns a ResponseBody.
- """
- resp, body = request_func(uri)
- self.expected_success(resp_status, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def _request_and_check_resp_data(self, request_func, uri, resp_status):
- """Make a request and check response status code.
-
- It returns pair: resp and response data.
- """
- resp, body = request_func(uri)
- self.expected_success(resp_status, resp.status)
- return resp, body
-
- def _request_check_and_parse_resp(self, request_func, uri,
- resp_status, *args, **kwargs):
- """Make a request, check response status code and parse response body.
-
- It returns a ResponseBody.
- """
- headers = {'Content-Type': 'application/json'}
- resp, body = request_func(uri, headers=headers, *args, **kwargs)
- self.expected_success(resp_status, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def list_node_group_templates(self):
- """List all node group templates for a user."""
-
- uri = 'node-group-templates'
- return self._request_check_and_parse_resp(self.get, uri, 200)
-
- def get_node_group_template(self, tmpl_id):
- """Returns the details of a single node group template."""
-
- uri = 'node-group-templates/%s' % tmpl_id
- return self._request_check_and_parse_resp(self.get, uri, 200)
-
- def create_node_group_template(self, name, plugin_name, hadoop_version,
- node_processes, flavor_id,
- node_configs=None, **kwargs):
- """Creates node group template with specified params.
-
- It supports passing additional params using kwargs and returns created
- object.
- """
- uri = 'node-group-templates'
- body = kwargs.copy()
- body.update({
- 'name': name,
- 'plugin_name': plugin_name,
- 'hadoop_version': hadoop_version,
- 'node_processes': node_processes,
- 'flavor_id': flavor_id,
- 'node_configs': node_configs or dict(),
- })
- return self._request_check_and_parse_resp(self.post, uri, 202,
- body=json.dumps(body))
-
- def delete_node_group_template(self, tmpl_id):
- """Deletes the specified node group template by id."""
-
- uri = 'node-group-templates/%s' % tmpl_id
- return self._request_and_check_resp(self.delete, uri, 204)
-
- def list_plugins(self):
- """List all enabled plugins."""
-
- uri = 'plugins'
- return self._request_check_and_parse_resp(self.get, uri, 200)
-
- def get_plugin(self, plugin_name, plugin_version=None):
- """Returns the details of a single plugin."""
-
- uri = 'plugins/%s' % plugin_name
- if plugin_version:
- uri += '/%s' % plugin_version
- return self._request_check_and_parse_resp(self.get, uri, 200)
-
- def list_cluster_templates(self):
- """List all cluster templates for a user."""
-
- uri = 'cluster-templates'
- return self._request_check_and_parse_resp(self.get, uri, 200)
-
- def get_cluster_template(self, tmpl_id):
- """Returns the details of a single cluster template."""
-
- uri = 'cluster-templates/%s' % tmpl_id
- return self._request_check_and_parse_resp(self.get, uri, 200)
-
- def create_cluster_template(self, name, plugin_name, hadoop_version,
- node_groups, cluster_configs=None,
- **kwargs):
- """Creates cluster template with specified params.
-
- It supports passing additional params using kwargs and returns created
- object.
- """
- uri = 'cluster-templates'
- body = kwargs.copy()
- body.update({
- 'name': name,
- 'plugin_name': plugin_name,
- 'hadoop_version': hadoop_version,
- 'node_groups': node_groups,
- 'cluster_configs': cluster_configs or dict(),
- })
- return self._request_check_and_parse_resp(self.post, uri, 202,
- body=json.dumps(body))
-
- def delete_cluster_template(self, tmpl_id):
- """Deletes the specified cluster template by id."""
-
- uri = 'cluster-templates/%s' % tmpl_id
- return self._request_and_check_resp(self.delete, uri, 204)
-
- def list_data_sources(self):
- """List all data sources for a user."""
-
- uri = 'data-sources'
- return self._request_check_and_parse_resp(self.get, uri, 200)
-
- def get_data_source(self, source_id):
- """Returns the details of a single data source."""
-
- uri = 'data-sources/%s' % source_id
- return self._request_check_and_parse_resp(self.get, uri, 200)
-
- def create_data_source(self, name, data_source_type, url, **kwargs):
- """Creates data source with specified params.
-
- It supports passing additional params using kwargs and returns created
- object.
- """
- uri = 'data-sources'
- body = kwargs.copy()
- body.update({
- 'name': name,
- 'type': data_source_type,
- 'url': url
- })
- return self._request_check_and_parse_resp(self.post, uri,
- 202, body=json.dumps(body))
-
- def delete_data_source(self, source_id):
- """Deletes the specified data source by id."""
-
- uri = 'data-sources/%s' % source_id
- return self._request_and_check_resp(self.delete, uri, 204)
-
- def list_job_binary_internals(self):
- """List all job binary internals for a user."""
-
- uri = 'job-binary-internals'
- return self._request_check_and_parse_resp(self.get, uri, 200)
-
- def get_job_binary_internal(self, job_binary_id):
- """Returns the details of a single job binary internal."""
-
- uri = 'job-binary-internals/%s' % job_binary_id
- return self._request_check_and_parse_resp(self.get, uri, 200)
-
- def create_job_binary_internal(self, name, data):
- """Creates job binary internal with specified params."""
-
- uri = 'job-binary-internals/%s' % name
- return self._request_check_and_parse_resp(self.put, uri, 202, data)
-
- def delete_job_binary_internal(self, job_binary_id):
- """Deletes the specified job binary internal by id."""
-
- uri = 'job-binary-internals/%s' % job_binary_id
- return self._request_and_check_resp(self.delete, uri, 204)
-
- def get_job_binary_internal_data(self, job_binary_id):
- """Returns data of a single job binary internal."""
-
- uri = 'job-binary-internals/%s/data' % job_binary_id
- return self._request_and_check_resp_data(self.get, uri, 200)
-
- def list_job_binaries(self):
- """List all job binaries for a user."""
-
- uri = 'job-binaries'
- return self._request_check_and_parse_resp(self.get, uri, 200)
-
- def get_job_binary(self, job_binary_id):
- """Returns the details of a single job binary."""
-
- uri = 'job-binaries/%s' % job_binary_id
- return self._request_check_and_parse_resp(self.get, uri, 200)
-
- def create_job_binary(self, name, url, extra=None, **kwargs):
- """Creates job binary with specified params.
-
- It supports passing additional params using kwargs and returns created
- object.
- """
- uri = 'job-binaries'
- body = kwargs.copy()
- body.update({
- 'name': name,
- 'url': url,
- 'extra': extra or dict(),
- })
- return self._request_check_and_parse_resp(self.post, uri,
- 202, body=json.dumps(body))
-
- def delete_job_binary(self, job_binary_id):
- """Deletes the specified job binary by id."""
-
- uri = 'job-binaries/%s' % job_binary_id
- return self._request_and_check_resp(self.delete, uri, 204)
-
- def get_job_binary_data(self, job_binary_id):
- """Returns data of a single job binary."""
-
- uri = 'job-binaries/%s/data' % job_binary_id
- return self._request_and_check_resp_data(self.get, uri, 200)
-
- def list_jobs(self):
- """List all jobs for a user."""
-
- uri = 'jobs'
- return self._request_check_and_parse_resp(self.get, uri, 200)
-
- def get_job(self, job_id):
- """Returns the details of a single job."""
-
- uri = 'jobs/%s' % job_id
- return self._request_check_and_parse_resp(self.get, uri, 200)
-
- def create_job(self, name, job_type, mains, libs=None, **kwargs):
- """Creates job with specified params.
-
- It supports passing additional params using kwargs and returns created
- object.
- """
- uri = 'jobs'
- body = kwargs.copy()
- body.update({
- 'name': name,
- 'type': job_type,
- 'mains': mains,
- 'libs': libs or list(),
- })
- return self._request_check_and_parse_resp(self.post, uri,
- 202, body=json.dumps(body))
-
- def delete_job(self, job_id):
- """Deletes the specified job by id."""
-
- uri = 'jobs/%s' % job_id
- return self._request_and_check_resp(self.delete, uri, 204)
diff --git a/tempest/services/database/__init__.py b/tempest/services/database/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/services/database/__init__.py
+++ /dev/null
diff --git a/tempest/services/database/json/__init__.py b/tempest/services/database/json/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/services/database/json/__init__.py
+++ /dev/null
diff --git a/tempest/services/database/json/flavors_client.py b/tempest/services/database/json/flavors_client.py
deleted file mode 100644
index 34a91ba..0000000
--- a/tempest/services/database/json/flavors_client.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# 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
-import urllib
-
-from tempest.common import service_client
-
-
-class DatabaseFlavorsClient(service_client.ServiceClient):
-
- def list_db_flavors(self, params=None):
- url = 'flavors'
- if params:
- url += '?%s' % urllib.urlencode(params)
-
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def show_db_flavor(self, db_flavor_id):
- resp, body = self.get("flavors/%s" % db_flavor_id)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
diff --git a/tempest/services/database/json/limits_client.py b/tempest/services/database/json/limits_client.py
deleted file mode 100644
index da495d7..0000000
--- a/tempest/services/database/json/limits_client.py
+++ /dev/null
@@ -1,32 +0,0 @@
-# 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.common import service_client
-
-
-class DatabaseLimitsClient(service_client.ServiceClient):
-
- def list_db_limits(self, params=None):
- """List all limits."""
- url = 'limits'
- if params:
- url += '?%s' % urllib.urlencode(params)
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
diff --git a/tempest/services/database/json/versions_client.py b/tempest/services/database/json/versions_client.py
deleted file mode 100644
index 7a560d9..0000000
--- a/tempest/services/database/json/versions_client.py
+++ /dev/null
@@ -1,48 +0,0 @@
-# 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.common import service_client
-
-
-class DatabaseVersionsClient(service_client.ServiceClient):
-
- def __init__(self, auth_provider, service, region,
- endpoint_type=None, build_interval=None, build_timeout=None,
- disable_ssl_certificate_validation=None, ca_certs=None,
- trace_requests=None):
- dscv = disable_ssl_certificate_validation
- super(DatabaseVersionsClient, self).__init__(
- auth_provider, service, region,
- endpoint_type=endpoint_type,
- build_interval=build_interval,
- build_timeout=build_timeout,
- disable_ssl_certificate_validation=dscv,
- ca_certs=ca_certs,
- trace_requests=trace_requests)
- self.skip_path()
-
- def list_db_versions(self, params=None):
- """List all versions."""
- url = ''
- if params:
- url += '?%s' % urllib.urlencode(params)
-
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
diff --git a/tempest/services/identity/v2/json/__init__.py b/tempest/services/identity/v2/json/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/services/identity/v2/json/__init__.py
+++ /dev/null
diff --git a/tempest/services/identity/v2/json/identity_client.py b/tempest/services/identity/v2/json/identity_client.py
deleted file mode 100644
index ff6c838..0000000
--- a/tempest/services/identity/v2/json/identity_client.py
+++ /dev/null
@@ -1,229 +0,0 @@
-# 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.common import service_client
-
-
-class IdentityClient(service_client.ServiceClient):
- api_version = "v2.0"
-
- def show_api_description(self):
- """Retrieves info about the v2.0 Identity API"""
- url = ''
- resp, body = self.get(url)
- self.expected_success([200, 203], resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def create_user(self, name, password, tenant_id, email, **kwargs):
- """Create a user."""
- post_body = {
- 'name': name,
- 'password': password,
- 'email': email
- }
- if tenant_id is not None:
- post_body['tenantId'] = tenant_id
- if kwargs.get('enabled') is not None:
- post_body['enabled'] = kwargs.get('enabled')
- post_body = json.dumps({'user': post_body})
- resp, body = self.post('users', post_body)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def update_user(self, user_id, **kwargs):
- """Updates a user."""
- put_body = json.dumps({'user': kwargs})
- resp, body = self.put('users/%s' % user_id, put_body)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def show_user(self, user_id):
- """GET a user."""
- resp, body = self.get("users/%s" % user_id)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def delete_user(self, user_id):
- """Delete a user."""
- resp, body = self.delete("users/%s" % user_id)
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def list_users(self):
- """Get the list of users."""
- resp, body = self.get("users")
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def enable_disable_user(self, user_id, **kwargs):
- """Enables or disables a user.
-
- Available params: see http://developer.openstack.org/
- api-ref-identity-v2-ext.html#enableUser
- """
- # NOTE: The URL (users/<id>/enabled) is different from the api-site
- # one (users/<id>/OS-KSADM/enabled) , but they are the same API
- # because of the fact that in keystone/contrib/admin_crud/core.py
- # both api use same action='set_user_enabled'
- put_body = json.dumps({'user': kwargs})
- resp, body = self.put('users/%s/enabled' % user_id, put_body)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def show_token(self, token_id):
- """Get token details."""
- resp, body = self.get("tokens/%s" % token_id)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def delete_token(self, token_id):
- """Delete a token."""
- resp, body = self.delete("tokens/%s" % token_id)
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def create_service(self, name, type, **kwargs):
- """Create a service."""
- post_body = {
- 'name': name,
- 'type': type,
- 'description': kwargs.get('description')
- }
- post_body = json.dumps({'OS-KSADM:service': post_body})
- resp, body = self.post('/OS-KSADM/services', post_body)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def show_service(self, service_id):
- """Get Service."""
- url = '/OS-KSADM/services/%s' % service_id
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def list_services(self):
- """List Service - Returns Services."""
- resp, body = self.get('/OS-KSADM/services')
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def delete_service(self, service_id):
- """Delete Service."""
- url = '/OS-KSADM/services/%s' % service_id
- resp, body = self.delete(url)
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def create_endpoint(self, service_id, region_id, **kwargs):
- """Create an endpoint for service."""
- post_body = {
- 'service_id': service_id,
- 'region': region_id,
- 'publicurl': kwargs.get('publicurl'),
- 'adminurl': kwargs.get('adminurl'),
- 'internalurl': kwargs.get('internalurl')
- }
- post_body = json.dumps({'endpoint': post_body})
- resp, body = self.post('/endpoints', post_body)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def list_endpoints(self):
- """List Endpoints - Returns Endpoints."""
- resp, body = self.get('/endpoints')
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def delete_endpoint(self, endpoint_id):
- """Delete an endpoint."""
- url = '/endpoints/%s' % endpoint_id
- resp, body = self.delete(url)
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def update_user_password(self, user_id, **kwargs):
- """Update User Password."""
- # 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/1524147
- put_body = json.dumps({'user': kwargs})
- resp, body = self.put('users/%s/OS-KSADM/password' % user_id, put_body)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def update_user_own_password(self, user_id, **kwargs):
- """User updates own password"""
- # 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/1524153
- # NOTE: This API is used for updating user password by itself.
- # Ref: http://lists.openstack.org/pipermail/openstack-dev/2015-December
- # /081803.html
- patch_body = json.dumps({'user': kwargs})
- resp, body = self.patch('OS-KSCRUD/users/%s' % user_id, patch_body)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def list_extensions(self):
- """List all the extensions."""
- resp, body = self.get('/extensions')
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def create_user_ec2_credentials(self, user_id, **kwargs):
- # 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.
- post_body = json.dumps(kwargs)
- resp, body = self.post('/users/%s/credentials/OS-EC2' % user_id,
- post_body)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def delete_user_ec2_credentials(self, user_id, access):
- resp, body = self.delete('/users/%s/credentials/OS-EC2/%s' %
- (user_id, access))
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def list_user_ec2_credentials(self, user_id):
- resp, body = self.get('/users/%s/credentials/OS-EC2' % user_id)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def show_user_ec2_credentials(self, user_id, access):
- resp, body = self.get('/users/%s/credentials/OS-EC2/%s' %
- (user_id, access))
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
diff --git a/tempest/services/identity/v2/json/roles_client.py b/tempest/services/identity/v2/json/roles_client.py
deleted file mode 100644
index ef6dfe9..0000000
--- a/tempest/services/identity/v2/json/roles_client.py
+++ /dev/null
@@ -1,74 +0,0 @@
-# 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.common import service_client
-
-
-class RolesClient(service_client.ServiceClient):
- api_version = "v2.0"
-
- def create_role(self, **kwargs):
- """Create a role.
-
- Available params: see http://developer.openstack.org/
- api-ref-identity-v2-ext.html#createRole
- """
- post_body = json.dumps({'role': kwargs})
- resp, body = self.post('OS-KSADM/roles', post_body)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def show_role(self, role_id):
- """Get a role by its id."""
- resp, body = self.get('OS-KSADM/roles/%s' % role_id)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def delete_role(self, role_id):
- """Delete a role."""
- resp, body = self.delete('OS-KSADM/roles/%s' % str(role_id))
- self.expected_success(204, resp.status)
- return resp, body
-
- def list_user_roles(self, tenant_id, user_id):
- """Returns a list of roles assigned to a user for a tenant."""
- url = '/tenants/%s/users/%s/roles' % (tenant_id, user_id)
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def assign_user_role(self, tenant_id, user_id, role_id):
- """Add roles to a user on a tenant."""
- resp, body = self.put('/tenants/%s/users/%s/roles/OS-KSADM/%s' %
- (tenant_id, user_id, role_id), "")
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def delete_user_role(self, tenant_id, user_id, role_id):
- """Removes a role assignment for a user on a tenant."""
- resp, body = self.delete('/tenants/%s/users/%s/roles/OS-KSADM/%s' %
- (tenant_id, user_id, role_id))
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def list_roles(self):
- """Returns roles."""
- resp, body = self.get('OS-KSADM/roles')
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
diff --git a/tempest/services/identity/v2/json/tenants_client.py b/tempest/services/identity/v2/json/tenants_client.py
deleted file mode 100644
index 937ae6f..0000000
--- a/tempest/services/identity/v2/json/tenants_client.py
+++ /dev/null
@@ -1,84 +0,0 @@
-# Copyright 2015 Red Hat, Inc.
-#
-# 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.common import service_client
-
-
-class TenantsClient(service_client.ServiceClient):
- api_version = "v2.0"
-
- def create_tenant(self, name, **kwargs):
- """Create a tenant
-
- name (required): New tenant name
- description: Description of new tenant (default is none)
- enabled <true|false>: Initial tenant status (default is true)
- """
- post_body = {
- 'name': name,
- 'description': kwargs.get('description', ''),
- 'enabled': kwargs.get('enabled', True),
- }
- post_body = json.dumps({'tenant': post_body})
- resp, body = self.post('tenants', post_body)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def delete_tenant(self, tenant_id):
- """Delete a tenant."""
- resp, body = self.delete('tenants/%s' % str(tenant_id))
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def show_tenant(self, tenant_id):
- """Get tenant details."""
- resp, body = self.get('tenants/%s' % str(tenant_id))
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def list_tenants(self):
- """Returns tenants."""
- resp, body = self.get('tenants')
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def update_tenant(self, tenant_id, **kwargs):
- """Updates a tenant."""
- body = self.show_tenant(tenant_id)['tenant']
- name = kwargs.get('name', body['name'])
- desc = kwargs.get('description', body['description'])
- en = kwargs.get('enabled', body['enabled'])
- post_body = {
- 'id': tenant_id,
- 'name': name,
- 'description': desc,
- 'enabled': en,
- }
- post_body = json.dumps({'tenant': post_body})
- resp, body = self.post('tenants/%s' % tenant_id, post_body)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def list_tenant_users(self, tenant_id):
- """List users for a Tenant."""
- resp, body = self.get('/tenants/%s/users' % tenant_id)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
diff --git a/tempest/services/identity/v3/json/__init__.py b/tempest/services/identity/v3/json/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/services/identity/v3/json/__init__.py
+++ /dev/null
diff --git a/tempest/services/identity/v3/json/credentials_client.py b/tempest/services/identity/v3/json/credentials_client.py
deleted file mode 100644
index 753e960..0000000
--- a/tempest/services/identity/v3/json/credentials_client.py
+++ /dev/null
@@ -1,73 +0,0 @@
-# Copyright 2013 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.
-
-"""
-http://developer.openstack.org/api-ref-identity-v3.html#credentials-v3
-"""
-
-from oslo_serialization import jsonutils as json
-
-from tempest.common import service_client
-
-
-class CredentialsClient(service_client.ServiceClient):
- api_version = "v3"
-
- def create_credential(self, **kwargs):
- """Creates a credential.
-
- Available params: see http://developer.openstack.org/
- api-ref-identity-v3.html#createCredential
- """
- post_body = json.dumps({'credential': kwargs})
- resp, body = self.post('credentials', post_body)
- self.expected_success(201, resp.status)
- body = json.loads(body)
- body['credential']['blob'] = json.loads(body['credential']['blob'])
- return service_client.ResponseBody(resp, body)
-
- def update_credential(self, credential_id, **kwargs):
- """Updates a credential.
-
- Available params: see http://developer.openstack.org/
- api-ref-identity-v3.html#updateCredential
- """
- post_body = json.dumps({'credential': kwargs})
- resp, body = self.patch('credentials/%s' % credential_id, post_body)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- body['credential']['blob'] = json.loads(body['credential']['blob'])
- return service_client.ResponseBody(resp, body)
-
- def show_credential(self, credential_id):
- """To GET Details of a credential."""
- resp, body = self.get('credentials/%s' % credential_id)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- body['credential']['blob'] = json.loads(body['credential']['blob'])
- return service_client.ResponseBody(resp, body)
-
- def list_credentials(self):
- """Lists out all the available credentials."""
- resp, body = self.get('credentials')
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def delete_credential(self, credential_id):
- """Deletes a credential."""
- resp, body = self.delete('credentials/%s' % credential_id)
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
diff --git a/tempest/services/identity/v3/json/endpoints_client.py b/tempest/services/identity/v3/json/endpoints_client.py
deleted file mode 100644
index ede5edb..0000000
--- a/tempest/services/identity/v3/json/endpoints_client.py
+++ /dev/null
@@ -1,63 +0,0 @@
-# Copyright 2013 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.
-
-"""
-http://developer.openstack.org/api-ref-identity-v3.html#endpoints-v3
-"""
-
-from oslo_serialization import jsonutils as json
-
-from tempest.common import service_client
-
-
-class EndPointClient(service_client.ServiceClient):
- api_version = "v3"
-
- def list_endpoints(self):
- """GET endpoints."""
- resp, body = self.get('endpoints')
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def create_endpoint(self, **kwargs):
- """Create endpoint.
-
- Available params: see http://developer.openstack.org/
- api-ref-identity-v3.html#createEndpoint
- """
- post_body = json.dumps({'endpoint': kwargs})
- resp, body = self.post('endpoints', post_body)
- self.expected_success(201, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def update_endpoint(self, endpoint_id, **kwargs):
- """Updates an endpoint with given parameters.
-
- Available params: see http://developer.openstack.org/
- api-ref-identity-v3.html#updateEndpoint
- """
- post_body = json.dumps({'endpoint': kwargs})
- resp, body = self.patch('endpoints/%s' % endpoint_id, post_body)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def delete_endpoint(self, endpoint_id):
- """Delete endpoint."""
- resp_header, resp_body = self.delete('endpoints/%s' % endpoint_id)
- self.expected_success(204, resp_header.status)
- return service_client.ResponseBody(resp_header, resp_body)
diff --git a/tempest/services/identity/v3/json/groups_client.py b/tempest/services/identity/v3/json/groups_client.py
deleted file mode 100644
index 70edd23..0000000
--- a/tempest/services/identity/v3/json/groups_client.py
+++ /dev/null
@@ -1,90 +0,0 @@
-# Copyright 2013 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.
-
-"""
-http://developer.openstack.org/api-ref-identity-v3.html#groups-v3
-"""
-
-from oslo_serialization import jsonutils as json
-
-from tempest.common import service_client
-
-
-class GroupsClient(service_client.ServiceClient):
- api_version = "v3"
-
- def create_group(self, **kwargs):
- """Creates a group.
-
- Available params: see http://developer.openstack.org/
- api-ref-identity-v3.html#createGroup
- """
- post_body = json.dumps({'group': kwargs})
- resp, body = self.post('groups', post_body)
- self.expected_success(201, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def show_group(self, group_id):
- """Get group details."""
- resp, body = self.get('groups/%s' % group_id)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def list_groups(self):
- """Lists the groups."""
- resp, body = self.get('groups')
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def update_group(self, group_id, **kwargs):
- """Updates a group.
-
- Available params: see http://developer.openstack.org/
- api-ref-identity-v3.html#updateGroup
- """
- post_body = json.dumps({'group': kwargs})
- resp, body = self.patch('groups/%s' % group_id, post_body)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def delete_group(self, group_id):
- """Delete a group."""
- resp, body = self.delete('groups/%s' % str(group_id))
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def add_group_user(self, group_id, user_id):
- """Add user into group."""
- resp, body = self.put('groups/%s/users/%s' % (group_id, user_id),
- None)
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def list_group_users(self, group_id):
- """List users in group."""
- resp, body = self.get('groups/%s/users' % group_id)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def delete_group_user(self, group_id, user_id):
- """Delete user in group."""
- resp, body = self.delete('groups/%s/users/%s' % (group_id, user_id))
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
diff --git a/tempest/services/identity/v3/json/identity_client.py b/tempest/services/identity/v3/json/identity_client.py
deleted file mode 100644
index ab8ba2a..0000000
--- a/tempest/services/identity/v3/json/identity_client.py
+++ /dev/null
@@ -1,458 +0,0 @@
-# Copyright 2013 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.common import service_client
-
-
-class IdentityV3Client(service_client.ServiceClient):
- api_version = "v3"
-
- def show_api_description(self):
- """Retrieves info about the v3 Identity API"""
- url = ''
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def create_user(self, user_name, password=None, project_id=None,
- email=None, domain_id='default', **kwargs):
- """Creates a user."""
- en = kwargs.get('enabled', True)
- description = kwargs.get('description', None)
- default_project_id = kwargs.get('default_project_id')
- post_body = {
- 'project_id': project_id,
- 'default_project_id': default_project_id,
- 'description': description,
- 'domain_id': domain_id,
- 'email': email,
- 'enabled': en,
- 'name': user_name,
- 'password': password
- }
- post_body = json.dumps({'user': post_body})
- resp, body = self.post('users', post_body)
- self.expected_success(201, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def update_user(self, user_id, name, **kwargs):
- """Updates a user."""
- body = self.show_user(user_id)['user']
- email = kwargs.get('email', body['email'])
- en = kwargs.get('enabled', body['enabled'])
- project_id = kwargs.get('project_id', body['project_id'])
- if 'default_project_id' in body.keys():
- default_project_id = kwargs.get('default_project_id',
- body['default_project_id'])
- else:
- default_project_id = kwargs.get('default_project_id')
- description = kwargs.get('description', body['description'])
- domain_id = kwargs.get('domain_id', body['domain_id'])
- post_body = {
- 'name': name,
- 'email': email,
- 'enabled': en,
- 'project_id': project_id,
- 'default_project_id': default_project_id,
- 'id': user_id,
- 'domain_id': domain_id,
- 'description': description
- }
- post_body = json.dumps({'user': post_body})
- resp, body = self.patch('users/%s' % user_id, post_body)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def update_user_password(self, user_id, **kwargs):
- """Update a user password
-
- Available params: see http://developer.openstack.org/
- api-ref-identity-v3.html#changeUserPassword
- """
- update_user = json.dumps({'user': kwargs})
- resp, _ = self.post('users/%s/password' % user_id, update_user)
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp)
-
- def list_user_projects(self, user_id):
- """Lists the projects on which a user has roles assigned."""
- resp, body = self.get('users/%s/projects' % user_id)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def list_users(self, params=None):
- """Get the list of users."""
- url = 'users'
- if params:
- url += '?%s' % urllib.urlencode(params)
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def show_user(self, user_id):
- """GET a user."""
- resp, body = self.get("users/%s" % user_id)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def delete_user(self, user_id):
- """Deletes a User."""
- resp, body = self.delete("users/%s" % user_id)
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def create_project(self, name, **kwargs):
- """Creates a project."""
- description = kwargs.get('description', None)
- en = kwargs.get('enabled', True)
- domain_id = kwargs.get('domain_id', 'default')
- post_body = {
- 'description': description,
- 'domain_id': domain_id,
- 'enabled': en,
- 'name': name
- }
- post_body = json.dumps({'project': post_body})
- resp, body = self.post('projects', post_body)
- self.expected_success(201, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def list_projects(self, params=None):
- url = "projects"
- if params:
- url += '?%s' % urllib.urlencode(params)
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def update_project(self, project_id, **kwargs):
- body = self.get_project(project_id)['project']
- name = kwargs.get('name', body['name'])
- desc = kwargs.get('description', body['description'])
- en = kwargs.get('enabled', body['enabled'])
- domain_id = kwargs.get('domain_id', body['domain_id'])
- post_body = {
- 'id': project_id,
- 'name': name,
- 'description': desc,
- 'enabled': en,
- 'domain_id': domain_id,
- }
- post_body = json.dumps({'project': post_body})
- resp, body = self.patch('projects/%s' % project_id, post_body)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def get_project(self, project_id):
- """GET a Project."""
- resp, body = self.get("projects/%s" % project_id)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def delete_project(self, project_id):
- """Delete a project."""
- resp, body = self.delete('projects/%s' % str(project_id))
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def create_role(self, **kwargs):
- """Create a Role.
-
- Available params: see http://developer.openstack.org/
- api-ref-identity-v3.html#createRole
- """
- post_body = json.dumps({'role': kwargs})
- resp, body = self.post('roles', post_body)
- self.expected_success(201, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def show_role(self, role_id):
- """GET a Role."""
- resp, body = self.get('roles/%s' % str(role_id))
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def list_roles(self):
- """Get the list of Roles."""
- resp, body = self.get("roles")
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def update_role(self, role_id, **kwargs):
- """Update a Role.
-
- Available params: see http://developer.openstack.org/
- api-ref-identity-v3.html#updateRole
- """
- post_body = json.dumps({'role': kwargs})
- resp, body = self.patch('roles/%s' % str(role_id), post_body)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def delete_role(self, role_id):
- """Delete a role."""
- resp, body = self.delete('roles/%s' % str(role_id))
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def assign_user_role(self, project_id, user_id, role_id):
- """Add roles to a user on a project."""
- resp, body = self.put('projects/%s/users/%s/roles/%s' %
- (project_id, user_id, role_id), None)
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def create_domain(self, name, **kwargs):
- """Creates a domain."""
- description = kwargs.get('description', None)
- en = kwargs.get('enabled', True)
- post_body = {
- 'description': description,
- 'enabled': en,
- 'name': name
- }
- post_body = json.dumps({'domain': post_body})
- resp, body = self.post('domains', post_body)
- self.expected_success(201, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def delete_domain(self, domain_id):
- """Delete a domain."""
- resp, body = self.delete('domains/%s' % str(domain_id))
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def list_domains(self, params=None):
- """List Domains."""
- url = 'domains'
- if params:
- url += '?%s' % urllib.urlencode(params)
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def update_domain(self, domain_id, **kwargs):
- """Updates a domain."""
- body = self.get_domain(domain_id)['domain']
- description = kwargs.get('description', body['description'])
- en = kwargs.get('enabled', body['enabled'])
- name = kwargs.get('name', body['name'])
- post_body = {
- 'description': description,
- 'enabled': en,
- 'name': name
- }
- post_body = json.dumps({'domain': post_body})
- resp, body = self.patch('domains/%s' % domain_id, post_body)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def get_domain(self, domain_id):
- """Get Domain details."""
- resp, body = self.get('domains/%s' % domain_id)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def show_token(self, resp_token):
- """Get token details."""
- headers = {'X-Subject-Token': resp_token}
- resp, body = self.get("auth/tokens", headers=headers)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def delete_token(self, resp_token):
- """Deletes token."""
- headers = {'X-Subject-Token': resp_token}
- resp, body = self.delete("auth/tokens", headers=headers)
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def list_user_groups(self, user_id):
- """Lists groups which a user belongs to."""
- resp, body = self.get('users/%s/groups' % user_id)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def assign_user_role_on_project(self, project_id, user_id, role_id):
- """Add roles to a user on a project."""
- resp, body = self.put('projects/%s/users/%s/roles/%s' %
- (project_id, user_id, role_id), None)
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def assign_user_role_on_domain(self, domain_id, user_id, role_id):
- """Add roles to a user on a domain."""
- resp, body = self.put('domains/%s/users/%s/roles/%s' %
- (domain_id, user_id, role_id), None)
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def list_user_roles_on_project(self, project_id, user_id):
- """list roles of a user on a project."""
- resp, body = self.get('projects/%s/users/%s/roles' %
- (project_id, user_id))
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def list_user_roles_on_domain(self, domain_id, user_id):
- """list roles of a user on a domain."""
- resp, body = self.get('domains/%s/users/%s/roles' %
- (domain_id, user_id))
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def delete_role_from_user_on_project(self, project_id, user_id, role_id):
- """Delete role of a user on a project."""
- resp, body = self.delete('projects/%s/users/%s/roles/%s' %
- (project_id, user_id, role_id))
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def delete_role_from_user_on_domain(self, domain_id, user_id, role_id):
- """Delete role of a user on a domain."""
- resp, body = self.delete('domains/%s/users/%s/roles/%s' %
- (domain_id, user_id, role_id))
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def assign_group_role_on_project(self, project_id, group_id, role_id):
- """Add roles to a user on a project."""
- resp, body = self.put('projects/%s/groups/%s/roles/%s' %
- (project_id, group_id, role_id), None)
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def assign_group_role_on_domain(self, domain_id, group_id, role_id):
- """Add roles to a user on a domain."""
- resp, body = self.put('domains/%s/groups/%s/roles/%s' %
- (domain_id, group_id, role_id), None)
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def list_group_roles_on_project(self, project_id, group_id):
- """list roles of a user on a project."""
- resp, body = self.get('projects/%s/groups/%s/roles' %
- (project_id, group_id))
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def list_group_roles_on_domain(self, domain_id, group_id):
- """list roles of a user on a domain."""
- resp, body = self.get('domains/%s/groups/%s/roles' %
- (domain_id, group_id))
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def delete_role_from_group_on_project(self, project_id, group_id, role_id):
- """Delete role of a user on a project."""
- resp, body = self.delete('projects/%s/groups/%s/roles/%s' %
- (project_id, group_id, role_id))
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def delete_role_from_group_on_domain(self, domain_id, group_id, role_id):
- """Delete role of a user on a domain."""
- resp, body = self.delete('domains/%s/groups/%s/roles/%s' %
- (domain_id, group_id, role_id))
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def create_trust(self, **kwargs):
- """Creates a trust.
-
- Available params: see http://developer.openstack.org/
- api-ref-identity-v3-ext.html#createTrust
- """
- post_body = json.dumps({'trust': kwargs})
- resp, body = self.post('OS-TRUST/trusts', post_body)
- self.expected_success(201, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def delete_trust(self, trust_id):
- """Deletes a trust."""
- resp, body = self.delete("OS-TRUST/trusts/%s" % trust_id)
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def get_trusts(self, trustor_user_id=None, trustee_user_id=None):
- """GET trusts."""
- if trustor_user_id:
- resp, body = self.get("OS-TRUST/trusts?trustor_user_id=%s"
- % trustor_user_id)
- elif trustee_user_id:
- resp, body = self.get("OS-TRUST/trusts?trustee_user_id=%s"
- % trustee_user_id)
- else:
- resp, body = self.get("OS-TRUST/trusts")
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def get_trust(self, trust_id):
- """GET trust."""
- resp, body = self.get("OS-TRUST/trusts/%s" % trust_id)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def get_trust_roles(self, trust_id):
- """GET roles delegated by a trust."""
- resp, body = self.get("OS-TRUST/trusts/%s/roles" % trust_id)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def get_trust_role(self, trust_id, role_id):
- """GET role delegated by a trust."""
- resp, body = self.get("OS-TRUST/trusts/%s/roles/%s"
- % (trust_id, role_id))
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def check_trust_role(self, trust_id, role_id):
- """HEAD Check if role is delegated by a trust."""
- resp, body = self.head("OS-TRUST/trusts/%s/roles/%s"
- % (trust_id, role_id))
- self.expected_success(200, resp.status)
- return service_client.ResponseBody(resp, body)
diff --git a/tempest/services/identity/v3/json/policies_client.py b/tempest/services/identity/v3/json/policies_client.py
deleted file mode 100644
index 639ed6d..0000000
--- a/tempest/services/identity/v3/json/policies_client.py
+++ /dev/null
@@ -1,73 +0,0 @@
-# Copyright 2013 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.
-
-"""
-http://developer.openstack.org/api-ref-identity-v3.html#policies-v3
-"""
-
-from oslo_serialization import jsonutils as json
-
-from tempest.common import service_client
-
-
-class PoliciesClient(service_client.ServiceClient):
- api_version = "v3"
-
- def create_policy(self, **kwargs):
- """Creates a Policy.
-
- Available params: see http://developer.openstack.org/
- api-ref-identity-v3.html#createPolicy
- """
- post_body = json.dumps({'policy': kwargs})
- resp, body = self.post('policies', post_body)
- self.expected_success(201, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def list_policies(self):
- """Lists the policies."""
- resp, body = self.get('policies')
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def show_policy(self, policy_id):
- """Lists out the given policy."""
- url = 'policies/%s' % policy_id
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def update_policy(self, policy_id, **kwargs):
- """Updates a policy.
-
- Available params: see http://developer.openstack.org/
- api-ref-identity-v3.html#updatePolicy
- """
- post_body = json.dumps({'policy': kwargs})
- url = 'policies/%s' % policy_id
- resp, body = self.patch(url, post_body)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def delete_policy(self, policy_id):
- """Deletes the policy."""
- url = "policies/%s" % policy_id
- resp, body = self.delete(url)
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
diff --git a/tempest/services/identity/v3/json/regions_client.py b/tempest/services/identity/v3/json/regions_client.py
deleted file mode 100644
index bc4b7a1..0000000
--- a/tempest/services/identity/v3/json/regions_client.py
+++ /dev/null
@@ -1,84 +0,0 @@
-# Copyright 2014 Hewlett-Packard Development Company, L.P
-# 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.
-
-"""
-http://developer.openstack.org/api-ref-identity-v3.html#regions-v3
-"""
-
-from oslo_serialization import jsonutils as json
-from six.moves.urllib import parse as urllib
-
-from tempest.common import service_client
-
-
-class RegionsClient(service_client.ServiceClient):
- api_version = "v3"
-
- def create_region(self, region_id=None, **kwargs):
- """Create region.
-
- Available params: see http://developer.openstack.org/
- api-ref-identity-v3.html#createRegion
-
- see http://developer.openstack.org/
- api-ref-identity-v3.html#createRegionWithID
- """
- if region_id is not None:
- method = self.put
- url = 'regions/%s' % region_id
- else:
- method = self.post
- url = 'regions'
- req_body = json.dumps({'region': kwargs})
- resp, body = method(url, req_body)
- self.expected_success(201, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def update_region(self, region_id, **kwargs):
- """Updates a region.
-
- Available params: see http://developer.openstack.org/
- api-ref-identity-v3.html#updateRegion
- """
- post_body = json.dumps({'region': kwargs})
- resp, body = self.patch('regions/%s' % region_id, post_body)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def show_region(self, region_id):
- """Get region."""
- url = 'regions/%s' % region_id
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def list_regions(self, params=None):
- """List regions."""
- url = 'regions'
- if params:
- url += '?%s' % urllib.urlencode(params)
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def delete_region(self, region_id):
- """Delete region."""
- resp, body = self.delete('regions/%s' % region_id)
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
diff --git a/tempest/services/identity/v3/json/services_client.py b/tempest/services/identity/v3/json/services_client.py
deleted file mode 100644
index dd65f1d..0000000
--- a/tempest/services/identity/v3/json/services_client.py
+++ /dev/null
@@ -1,70 +0,0 @@
-# Copyright 2013 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.
-
-"""
-http://developer.openstack.org/api-ref-identity-v3.html#service-catalog-v3
-"""
-
-from oslo_serialization import jsonutils as json
-
-from tempest.common import service_client
-
-
-class ServicesClient(service_client.ServiceClient):
- api_version = "v3"
-
- def update_service(self, service_id, **kwargs):
- """Updates a service.
-
- Available params: see http://developer.openstack.org/
- api-ref-identity-v3.html#updateService
- """
- patch_body = json.dumps({'service': kwargs})
- resp, body = self.patch('services/%s' % service_id, patch_body)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def show_service(self, service_id):
- """Get Service."""
- url = 'services/%s' % service_id
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def create_service(self, **kwargs):
- """Creates a service.
-
- Available params: see http://developer.openstack.org/
- api-ref-identity-v3.html#createService
- """
- body = json.dumps({'service': kwargs})
- resp, body = self.post("services", body)
- self.expected_success(201, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def delete_service(self, serv_id):
- url = "services/" + serv_id
- resp, body = self.delete(url)
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def list_services(self):
- resp, body = self.get('services')
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
diff --git a/tempest/services/image/v1/json/__init__.py b/tempest/services/image/v1/json/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/services/image/v1/json/__init__.py
+++ /dev/null
diff --git a/tempest/services/image/v1/json/images_client.py b/tempest/services/image/v1/json/images_client.py
deleted file mode 100644
index 3406db8..0000000
--- a/tempest/services/image/v1/json/images_client.py
+++ /dev/null
@@ -1,321 +0,0 @@
-# Copyright 2013 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.
-
-import copy
-import errno
-import os
-import time
-
-from oslo_log import log as logging
-from oslo_serialization import jsonutils as json
-import six
-from six.moves.urllib import parse as urllib
-from tempest_lib.common.utils import misc as misc_utils
-from tempest_lib import exceptions as lib_exc
-
-from tempest.common import glance_http
-from tempest.common import service_client
-from tempest import exceptions
-
-LOG = logging.getLogger(__name__)
-
-
-class ImagesClient(service_client.ServiceClient):
-
- def __init__(self, auth_provider, catalog_type, region, endpoint_type=None,
- build_interval=None, build_timeout=None,
- disable_ssl_certificate_validation=None,
- ca_certs=None, trace_requests=None):
- super(ImagesClient, self).__init__(
- auth_provider,
- catalog_type,
- region,
- endpoint_type=endpoint_type,
- build_interval=build_interval,
- build_timeout=build_timeout,
- disable_ssl_certificate_validation=(
- disable_ssl_certificate_validation),
- ca_certs=ca_certs,
- trace_requests=trace_requests)
- self._http = None
- self.dscv = disable_ssl_certificate_validation
- self.ca_certs = ca_certs
-
- def _image_meta_from_headers(self, headers):
- meta = {'properties': {}}
- for key, value in six.iteritems(headers):
- if key.startswith('x-image-meta-property-'):
- _key = key[22:]
- meta['properties'][_key] = value
- elif key.startswith('x-image-meta-'):
- _key = key[13:]
- meta[_key] = value
-
- for key in ['is_public', 'protected', 'deleted']:
- if key in meta:
- meta[key] = meta[key].strip().lower() in ('t', 'true', 'yes',
- '1')
- for key in ['size', 'min_ram', 'min_disk']:
- if key in meta:
- try:
- meta[key] = int(meta[key])
- except ValueError:
- pass
- return meta
-
- def _image_meta_to_headers(self, fields):
- headers = {}
- fields_copy = copy.deepcopy(fields)
- copy_from = fields_copy.pop('copy_from', None)
- if copy_from is not None:
- headers['x-glance-api-copy-from'] = copy_from
- for key, value in six.iteritems(fields_copy.pop('properties', {})):
- headers['x-image-meta-property-%s' % key] = str(value)
- for key, value in six.iteritems(fields_copy.pop('api', {})):
- headers['x-glance-api-property-%s' % key] = str(value)
- for key, value in six.iteritems(fields_copy):
- headers['x-image-meta-%s' % key] = str(value)
- return headers
-
- def _get_file_size(self, obj):
- """Analyze file-like object and attempt to determine its size.
-
- :param obj: file-like object, typically redirected from stdin.
- :retval The file's size or None if it cannot be determined.
- """
- # For large images, we need to supply the size of the
- # image file. See LP Bugs #827660 and #845788.
- if hasattr(obj, 'seek') and hasattr(obj, 'tell'):
- try:
- obj.seek(0, os.SEEK_END)
- obj_size = obj.tell()
- obj.seek(0)
- return obj_size
- except IOError as e:
- if e.errno == errno.ESPIPE:
- # Illegal seek. This means the user is trying
- # to pipe image data to the client, e.g.
- # echo testdata | bin/glance add blah..., or
- # that stdin is empty, or that a file-like
- # object which doesn't support 'seek/tell' has
- # been supplied.
- return None
- else:
- raise
- else:
- # Cannot determine size of input image
- return None
-
- def _get_http(self):
- return glance_http.HTTPClient(auth_provider=self.auth_provider,
- filters=self.filters,
- insecure=self.dscv,
- ca_certs=self.ca_certs)
-
- def _create_with_data(self, headers, data):
- resp, body_iter = self.http.raw_request('POST', '/v1/images',
- headers=headers, body=data)
- self._error_checker('POST', '/v1/images', headers, data, resp,
- body_iter)
- body = json.loads(''.join([c for c in body_iter]))
- return service_client.ResponseBody(resp, body)
-
- def _update_with_data(self, image_id, headers, data):
- url = '/v1/images/%s' % image_id
- resp, body_iter = self.http.raw_request('PUT', url, headers=headers,
- body=data)
- self._error_checker('PUT', url, headers, data,
- resp, body_iter)
- body = json.loads(''.join([c for c in body_iter]))
- return service_client.ResponseBody(resp, body)
-
- @property
- def http(self):
- if self._http is None:
- self._http = self._get_http()
- return self._http
-
- def create_image(self, name, container_format, disk_format, **kwargs):
- params = {
- "name": name,
- "container_format": container_format,
- "disk_format": disk_format,
- }
-
- headers = {}
-
- for option in ['is_public', 'location', 'properties',
- 'copy_from', 'min_ram']:
- if option in kwargs:
- params[option] = kwargs.get(option)
-
- headers.update(self._image_meta_to_headers(params))
-
- if 'data' in kwargs:
- return self._create_with_data(headers, kwargs.get('data'))
-
- resp, body = self.post('v1/images', None, headers)
- self.expected_success(201, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def update_image(self, image_id, name=None, container_format=None,
- data=None, properties=None):
- params = {}
- headers = {}
- if name is not None:
- params['name'] = name
-
- if container_format is not None:
- params['container_format'] = container_format
-
- if properties is not None:
- params['properties'] = properties
-
- headers.update(self._image_meta_to_headers(params))
-
- if data is not None:
- return self._update_with_data(image_id, headers, data)
-
- url = 'v1/images/%s' % image_id
- resp, body = self.put(url, data, headers)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def delete_image(self, image_id):
- url = 'v1/images/%s' % image_id
- resp, body = self.delete(url)
- self.expected_success(200, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def list_images(self, detail=False, properties=dict(),
- changes_since=None, **kwargs):
- url = 'v1/images'
-
- if detail:
- url += '/detail'
-
- params = {}
- for key, value in properties.items():
- params['property-%s' % key] = value
-
- kwargs.update(params)
-
- if changes_since is not None:
- kwargs['changes-since'] = changes_since
-
- if len(kwargs) > 0:
- url += '?%s' % urllib.urlencode(kwargs)
-
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def get_image_meta(self, image_id):
- url = 'v1/images/%s' % image_id
- resp, __ = self.head(url)
- self.expected_success(200, resp.status)
- body = self._image_meta_from_headers(resp)
- return service_client.ResponseBody(resp, body)
-
- def show_image(self, image_id):
- url = 'v1/images/%s' % image_id
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- return service_client.ResponseBodyData(resp, body)
-
- def is_resource_deleted(self, id):
- try:
- self.get_image_meta(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 'image_meta'
-
- def list_image_members(self, image_id):
- url = 'v1/images/%s/members' % image_id
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def list_shared_images(self, tenant_id):
- """List shared images with the specified tenant"""
- url = 'v1/shared-images/%s' % tenant_id
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def add_member(self, member_id, image_id, **kwargs):
- """Add a member to an image.
-
- Available params: see http://developer.openstack.org/
- api-ref-image-v1.html#addMember-v1
- """
- url = 'v1/images/%s/members/%s' % (image_id, member_id)
- body = json.dumps({'member': kwargs})
- resp, __ = self.put(url, body)
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp)
-
- def delete_member(self, member_id, image_id):
- url = 'v1/images/%s/members/%s' % (image_id, member_id)
- resp, __ = self.delete(url)
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp)
-
- # NOTE(afazekas): just for the wait function
- def _get_image_status(self, image_id):
- meta = self.get_image_meta(image_id)
- status = meta['status']
- return status
-
- # NOTE(afazkas): Wait reinvented again. It is not in the correct layer
- def wait_for_image_status(self, image_id, status):
- """Waits for a Image to reach a given status."""
- start_time = time.time()
- old_value = value = self._get_image_status(image_id)
- while True:
- dtime = time.time() - start_time
- time.sleep(self.build_interval)
- if value != old_value:
- LOG.info('Value transition from "%s" to "%s"'
- 'in %d second(s).', old_value,
- value, dtime)
- if value == status:
- return value
-
- if value == 'killed':
- raise exceptions.ImageKilledException(image_id=image_id,
- status=status)
- if dtime > self.build_timeout:
- message = ('Time Limit Exceeded! (%ds)'
- 'while waiting for %s, '
- 'but we got %s.' %
- (self.build_timeout, status, value))
- caller = misc_utils.find_test_caller()
- if caller:
- message = '(%s) %s' % (caller, message)
- raise exceptions.TimeoutException(message)
- time.sleep(self.build_interval)
- old_value = value
- value = self._get_image_status(image_id)
diff --git a/tempest/services/image/v2/json/__init__.py b/tempest/services/image/v2/json/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/services/image/v2/json/__init__.py
+++ /dev/null
diff --git a/tempest/services/image/v2/json/images_client.py b/tempest/services/image/v2/json/images_client.py
deleted file mode 100644
index 72b203a..0000000
--- a/tempest/services/image/v2/json/images_client.py
+++ /dev/null
@@ -1,256 +0,0 @@
-# Copyright 2013 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 six.moves.urllib import parse as urllib
-from tempest_lib import exceptions as lib_exc
-
-from tempest.common import glance_http
-from tempest.common import service_client
-
-
-class ImagesClientV2(service_client.ServiceClient):
-
- def __init__(self, auth_provider, catalog_type, region, endpoint_type=None,
- build_interval=None, build_timeout=None,
- disable_ssl_certificate_validation=None, ca_certs=None,
- trace_requests=None):
- super(ImagesClientV2, self).__init__(
- auth_provider,
- catalog_type,
- region,
- endpoint_type=endpoint_type,
- build_interval=build_interval,
- build_timeout=build_timeout,
- disable_ssl_certificate_validation=(
- disable_ssl_certificate_validation),
- ca_certs=ca_certs,
- trace_requests=trace_requests)
- self._http = None
- self.dscv = disable_ssl_certificate_validation
- self.ca_certs = ca_certs
-
- def _get_http(self):
- return glance_http.HTTPClient(auth_provider=self.auth_provider,
- filters=self.filters,
- insecure=self.dscv,
- ca_certs=self.ca_certs)
-
- @property
- def http(self):
- if self._http is None:
- self._http = self._get_http()
- return self._http
-
- def update_image(self, image_id, patch):
- """Update an image.
-
- Available params: see http://developer.openstack.org/
- api-ref-image-v2.html#updateImage-v2
- """
- data = json.dumps(patch)
- headers = {"Content-Type": "application/openstack-images-v2.0"
- "-json-patch"}
- resp, body = self.patch('v2/images/%s' % image_id, data, headers)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def create_image(self, **kwargs):
- """Create an image.
-
- Available params: see http://developer.openstack.org/
- api-ref-image-v2.html#createImage-v2
- """
- data = json.dumps(kwargs)
- resp, body = self.post('v2/images', data)
- self.expected_success(201, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def deactivate_image(self, image_id):
- url = 'v2/images/%s/actions/deactivate' % image_id
- resp, body = self.post(url, None)
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def reactivate_image(self, image_id):
- url = 'v2/images/%s/actions/reactivate' % image_id
- resp, body = self.post(url, None)
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def delete_image(self, image_id):
- url = 'v2/images/%s' % image_id
- resp, _ = self.delete(url)
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp)
-
- def list_images(self, params=None):
- url = 'v2/images'
-
- if params:
- url += '?%s' % urllib.urlencode(params)
-
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def show_image(self, image_id):
- url = 'v2/images/%s' % image_id
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def is_resource_deleted(self, id):
- try:
- self.show_image(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 'image'
-
- def store_image_file(self, image_id, data):
- url = 'v2/images/%s/file' % image_id
- headers = {'Content-Type': 'application/octet-stream'}
- resp, body = self.http.raw_request('PUT', url, headers=headers,
- body=data)
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def show_image_file(self, image_id):
- url = 'v2/images/%s/file' % image_id
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- return service_client.ResponseBodyData(resp, body)
-
- def add_image_tag(self, image_id, tag):
- url = 'v2/images/%s/tags/%s' % (image_id, tag)
- resp, body = self.put(url, body=None)
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def delete_image_tag(self, image_id, tag):
- url = 'v2/images/%s/tags/%s' % (image_id, tag)
- resp, _ = self.delete(url)
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp)
-
- def list_image_members(self, image_id):
- url = 'v2/images/%s/members' % image_id
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def create_image_member(self, image_id, **kwargs):
- """Create an image member.
-
- Available params: see http://developer.openstack.org/
- api-ref-image-v2.html#createImageMember-v2
- """
- url = 'v2/images/%s/members' % image_id
- data = json.dumps(kwargs)
- resp, body = self.post(url, data)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def update_image_member(self, image_id, member_id, **kwargs):
- """Update an image member.
-
- Available params: see http://developer.openstack.org/
- api-ref-image-v2.html#updateImageMember-v2
- """
- url = 'v2/images/%s/members/%s' % (image_id, member_id)
- data = json.dumps(kwargs)
- resp, body = self.put(url, data)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def show_image_member(self, image_id, member_id):
- url = 'v2/images/%s/members/%s' % (image_id, member_id)
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- return service_client.ResponseBody(resp, json.loads(body))
-
- def delete_image_member(self, image_id, member_id):
- url = 'v2/images/%s/members/%s' % (image_id, member_id)
- resp, _ = self.delete(url)
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp)
-
- def show_schema(self, schema):
- url = 'v2/schemas/%s' % schema
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def list_resource_types(self):
- url = '/v2/metadefs/resource_types'
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def create_namespace(self, **kwargs):
- """Create a namespace.
-
- Available params: see http://developer.openstack.org/
- api-ref-image-v2.html#createNamespace-v2
- """
- data = json.dumps(kwargs)
- resp, body = self.post('/v2/metadefs/namespaces', data)
- self.expected_success(201, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def show_namespace(self, namespace):
- url = '/v2/metadefs/namespaces/%s' % namespace
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def update_namespace(self, namespace, **kwargs):
- """Update a namespace.
-
- Available params: see http://developer.openstack.org/
- api-ref-image-v2.html#updateNamespace-v2
- """
- # NOTE: On Glance API, we need to pass namespace on both URI
- # and a request body.
- params = {'namespace': namespace}
- params.update(kwargs)
- data = json.dumps(params)
- url = '/v2/metadefs/namespaces/%s' % namespace
- resp, body = self.put(url, body=data)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return service_client.ResponseBody(resp, body)
-
- def delete_namespace(self, namespace):
- url = '/v2/metadefs/namespaces/%s' % namespace
- resp, _ = self.delete(url)
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp)
diff --git a/tempest/services/messaging/__init__.py b/tempest/services/messaging/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/services/messaging/__init__.py
+++ /dev/null
diff --git a/tempest/services/messaging/json/__init__.py b/tempest/services/messaging/json/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/services/messaging/json/__init__.py
+++ /dev/null
diff --git a/tempest/services/messaging/json/messaging_client.py b/tempest/services/messaging/json/messaging_client.py
deleted file mode 100644
index 5a43841..0000000
--- a/tempest/services/messaging/json/messaging_client.py
+++ /dev/null
@@ -1,176 +0,0 @@
-# Copyright (c) 2014 Rackspace, Inc.
-#
-# 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 uuid
-
-from oslo_serialization import jsonutils as json
-from six.moves.urllib import parse as urllib
-
-from tempest.api_schema.response.messaging.v1 import queues as queues_schema
-from tempest.common import service_client
-
-
-class MessagingClient(service_client.ServiceClient):
-
- def __init__(self, auth_provider, service, region,
- endpoint_type=None, build_interval=None, build_timeout=None,
- disable_ssl_certificate_validation=None, ca_certs=None,
- trace_requests=None):
- dscv = disable_ssl_certificate_validation
- super(MessagingClient, self).__init__(
- auth_provider, service, region,
- endpoint_type=endpoint_type,
- build_interval=build_interval,
- build_timeout=build_timeout,
- disable_ssl_certificate_validation=dscv,
- ca_certs=ca_certs,
- trace_requests=trace_requests)
-
- self.version = '1'
- self.uri_prefix = 'v{0}'.format(self.version)
-
- client_id = uuid.uuid4().hex
- self.headers = {'Client-ID': client_id}
-
- def list_queues(self):
- uri = '{0}/queues'.format(self.uri_prefix)
- resp, body = self.get(uri)
-
- if resp['status'] != '204':
- body = json.loads(body)
- self.validate_response(queues_schema.list_queues, resp, body)
- return resp, body
-
- def create_queue(self, queue_name):
- uri = '{0}/queues/{1}'.format(self.uri_prefix, queue_name)
- resp, body = self.put(uri, body=None)
- self.expected_success(201, resp.status)
- return resp, body
-
- def show_queue(self, queue_name):
- uri = '{0}/queues/{1}'.format(self.uri_prefix, queue_name)
- resp, body = self.get(uri)
- self.expected_success(204, resp.status)
- return resp, body
-
- def head_queue(self, queue_name):
- uri = '{0}/queues/{1}'.format(self.uri_prefix, queue_name)
- resp, body = self.head(uri)
- self.expected_success(204, resp.status)
- return resp, body
-
- def delete_queue(self, queue_name):
- uri = '{0}/queues/{1}'.format(self.uri_prefix, queue_name)
- resp, body = self.delete(uri)
- self.expected_success(204, resp.status)
- return resp, body
-
- def show_queue_stats(self, queue_name):
- uri = '{0}/queues/{1}/stats'.format(self.uri_prefix, queue_name)
- resp, body = self.get(uri)
- body = json.loads(body)
- self.validate_response(queues_schema.queue_stats, resp, body)
- return resp, body
-
- def show_queue_metadata(self, queue_name):
- uri = '{0}/queues/{1}/metadata'.format(self.uri_prefix, queue_name)
- resp, body = self.get(uri)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return resp, body
-
- def set_queue_metadata(self, queue_name, rbody):
- uri = '{0}/queues/{1}/metadata'.format(self.uri_prefix, queue_name)
- resp, body = self.put(uri, body=json.dumps(rbody))
- self.expected_success(204, resp.status)
- return resp, body
-
- def post_messages(self, queue_name, rbody):
- uri = '{0}/queues/{1}/messages'.format(self.uri_prefix, queue_name)
- resp, body = self.post(uri, body=json.dumps(rbody),
- extra_headers=True,
- headers=self.headers)
-
- body = json.loads(body)
- self.validate_response(queues_schema.post_messages, resp, body)
- return resp, body
-
- def list_messages(self, queue_name):
- uri = '{0}/queues/{1}/messages?echo=True'.format(self.uri_prefix,
- queue_name)
- resp, body = self.get(uri, extra_headers=True, headers=self.headers)
-
- if resp['status'] != '204':
- body = json.loads(body)
- self.validate_response(queues_schema.list_messages, resp, body)
-
- return resp, body
-
- def show_single_message(self, message_uri):
- resp, body = self.get(message_uri, extra_headers=True,
- headers=self.headers)
- if resp['status'] != '204':
- body = json.loads(body)
- self.validate_response(queues_schema.get_single_message, resp,
- body)
- return resp, body
-
- def show_multiple_messages(self, message_uri):
- resp, body = self.get(message_uri, extra_headers=True,
- headers=self.headers)
-
- if resp['status'] != '204':
- body = json.loads(body)
- self.validate_response(queues_schema.get_multiple_messages,
- resp,
- body)
-
- return resp, body
-
- def delete_messages(self, message_uri):
- resp, body = self.delete(message_uri)
- self.expected_success(204, resp.status)
- return resp, body
-
- def post_claims(self, queue_name, rbody, url_params=False):
- uri = '{0}/queues/{1}/claims'.format(self.uri_prefix, queue_name)
- if url_params:
- uri += '?%s' % urllib.urlencode(url_params)
-
- resp, body = self.post(uri, body=json.dumps(rbody),
- extra_headers=True,
- headers=self.headers)
-
- body = json.loads(body)
- self.validate_response(queues_schema.claim_messages, resp, body)
- return resp, body
-
- def query_claim(self, claim_uri):
- resp, body = self.get(claim_uri)
-
- if resp['status'] != '204':
- body = json.loads(body)
- self.validate_response(queues_schema.query_claim, resp, body)
- return resp, body
-
- def update_claim(self, claim_uri, rbody):
- resp, body = self.patch(claim_uri, body=json.dumps(rbody))
- self.expected_success(204, resp.status)
- return resp, body
-
- def delete_claim(self, claim_uri):
- resp, body = self.delete(claim_uri)
- self.expected_success(204, resp.status)
- return resp, body
diff --git a/tempest/services/network/json/__init__.py b/tempest/services/network/json/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/services/network/json/__init__.py
+++ /dev/null
diff --git a/tempest/services/network/json/agents_client.py b/tempest/services/network/json/agents_client.py
deleted file mode 100644
index 07f93b2..0000000
--- a/tempest/services/network/json/agents_client.py
+++ /dev/null
@@ -1,68 +0,0 @@
-# Copyright 2015 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.services.network.json import base
-
-
-class AgentsClient(base.BaseNetworkClient):
-
- def update_agent(self, agent_id, **kwargs):
- """Update agent."""
- # 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/1526673
- uri = '/agents/%s' % agent_id
- return self.update_resource(uri, kwargs)
-
- def show_agent(self, agent_id, **fields):
- uri = '/agents/%s' % agent_id
- return self.show_resource(uri, **fields)
-
- def list_agents(self, **filters):
- uri = '/agents'
- return self.list_resources(uri, **filters)
-
- def list_routers_on_l3_agent(self, agent_id):
- uri = '/agents/%s/l3-routers' % agent_id
- return self.list_resources(uri)
-
- def add_router_to_l3_agent(self, agent_id, **kwargs):
- # 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/1526670
- uri = '/agents/%s/l3-routers' % agent_id
- return self.create_resource(uri, kwargs)
-
- def remove_router_from_l3_agent(self, agent_id, router_id):
- uri = '/agents/%s/l3-routers/%s' % (agent_id, router_id)
- return self.delete_resource(uri)
-
- def list_networks_hosted_by_one_dhcp_agent(self, agent_id):
- uri = '/agents/%s/dhcp-networks' % agent_id
- return self.list_resources(uri)
-
- def remove_network_from_dhcp_agent(self, agent_id, network_id):
- uri = '/agents/%s/dhcp-networks/%s' % (agent_id,
- network_id)
- return self.delete_resource(uri)
-
- def add_dhcp_agent_to_network(self, agent_id, **kwargs):
- # 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/1526212
- uri = '/agents/%s/dhcp-networks' % agent_id
- return self.create_resource(uri, kwargs)
diff --git a/tempest/services/network/json/base.py b/tempest/services/network/json/base.py
deleted file mode 100644
index 6ebc245..0000000
--- a/tempest/services/network/json/base.py
+++ /dev/null
@@ -1,71 +0,0 @@
-# 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.common import service_client
-
-
-class BaseNetworkClient(service_client.ServiceClient):
-
- """Base class for Tempest REST clients for Neutron.
-
- Child classes use v2 of the Neutron API, since the V1 API has been
- removed from the code base.
- """
-
- version = '2.0'
- uri_prefix = "v2.0"
-
- def list_resources(self, uri, **filters):
- req_uri = self.uri_prefix + uri
- if filters:
- req_uri += '?' + urllib.urlencode(filters, doseq=1)
- resp, body = self.get(req_uri)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def delete_resource(self, uri):
- req_uri = self.uri_prefix + uri
- resp, body = self.delete(req_uri)
- self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def show_resource(self, uri, **fields):
- # fields is a dict which key is 'fields' and value is a
- # list of field's name. An example:
- # {'fields': ['id', 'name']}
- req_uri = self.uri_prefix + uri
- if fields:
- req_uri += '?' + urllib.urlencode(fields, doseq=1)
- resp, body = self.get(req_uri)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def create_resource(self, uri, post_data):
- req_uri = self.uri_prefix + uri
- req_post_data = json.dumps(post_data)
- resp, body = self.post(req_uri, req_post_data)
- body = json.loads(body)
- self.expected_success(201, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def update_resource(self, uri, post_data):
- req_uri = self.uri_prefix + uri
- req_post_data = json.dumps(post_data)
- resp, body = self.put(req_uri, req_post_data)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return service_client.ResponseBody(resp, body)
diff --git a/tempest/services/network/json/floating_ips_client.py b/tempest/services/network/json/floating_ips_client.py
deleted file mode 100644
index 5c490ed..0000000
--- a/tempest/services/network/json/floating_ips_client.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# 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.services.network.json import base
-
-
-class FloatingIPsClient(base.BaseNetworkClient):
-
- def create_floatingip(self, **kwargs):
- uri = '/floatingips'
- post_data = {'floatingip': kwargs}
- return self.create_resource(uri, post_data)
-
- def update_floatingip(self, floatingip_id, **kwargs):
- uri = '/floatingips/%s' % floatingip_id
- post_data = {'floatingip': kwargs}
- return self.update_resource(uri, post_data)
-
- def show_floatingip(self, floatingip_id, **fields):
- uri = '/floatingips/%s' % floatingip_id
- return self.show_resource(uri, **fields)
-
- def delete_floatingip(self, floatingip_id):
- uri = '/floatingips/%s' % floatingip_id
- return self.delete_resource(uri)
-
- def list_floatingips(self, **filters):
- uri = '/floatingips'
- return self.list_resources(uri, **filters)
diff --git a/tempest/services/network/json/metering_label_rules_client.py b/tempest/services/network/json/metering_label_rules_client.py
deleted file mode 100644
index 374a89c..0000000
--- a/tempest/services/network/json/metering_label_rules_client.py
+++ /dev/null
@@ -1,33 +0,0 @@
-# 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.services.network.json import base
-
-
-class MeteringLabelRulesClient(base.BaseNetworkClient):
-
- def create_metering_label_rule(self, **kwargs):
- uri = '/metering/metering-label-rules'
- post_data = {'metering_label_rule': kwargs}
- return self.create_resource(uri, post_data)
-
- def show_metering_label_rule(self, metering_label_rule_id, **fields):
- uri = '/metering/metering-label-rules/%s' % metering_label_rule_id
- return self.show_resource(uri, **fields)
-
- def delete_metering_label_rule(self, metering_label_rule_id):
- uri = '/metering/metering-label-rules/%s' % metering_label_rule_id
- return self.delete_resource(uri)
-
- def list_metering_label_rules(self, **filters):
- uri = '/metering/metering-label-rules'
- return self.list_resources(uri, **filters)
diff --git a/tempest/services/network/json/metering_labels_client.py b/tempest/services/network/json/metering_labels_client.py
deleted file mode 100644
index 2e5cdae..0000000
--- a/tempest/services/network/json/metering_labels_client.py
+++ /dev/null
@@ -1,33 +0,0 @@
-# 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.services.network.json import base
-
-
-class MeteringLabelsClient(base.BaseNetworkClient):
-
- def create_metering_label(self, **kwargs):
- uri = '/metering/metering-labels'
- post_data = {'metering_label': kwargs}
- return self.create_resource(uri, post_data)
-
- def show_metering_label(self, metering_label_id, **fields):
- uri = '/metering/metering-labels/%s' % metering_label_id
- return self.show_resource(uri, **fields)
-
- def delete_metering_label(self, metering_label_id):
- uri = '/metering/metering-labels/%s' % metering_label_id
- return self.delete_resource(uri)
-
- def list_metering_labels(self, **filters):
- uri = '/metering/metering-labels'
- return self.list_resources(uri, **filters)
diff --git a/tempest/services/network/json/network_client.py b/tempest/services/network/json/network_client.py
deleted file mode 100644
index e8e21d2..0000000
--- a/tempest/services/network/json/network_client.py
+++ /dev/null
@@ -1,257 +0,0 @@
-# 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 time
-
-from tempest_lib.common.utils import misc
-from tempest_lib import exceptions as lib_exc
-
-from tempest import exceptions
-from tempest.services.network.json import base
-
-
-class NetworkClient(base.BaseNetworkClient):
-
- """Tempest REST client for Neutron.
-
- Uses v2 of the Neutron API, since the V1 API has been removed from the
- code base.
-
- Implements create, delete, update, list and show for the basic Neutron
- abstractions (networks, sub-networks, routers, ports and floating IP):
-
- Implements add/remove interface to router using subnet ID / port ID
-
- It also implements list, show, update and reset for OpenStack Networking
- quotas
- """
-
- def create_security_group_rule(self, **kwargs):
- uri = '/security-group-rules'
- post_data = {'security_group_rule': kwargs}
- return self.create_resource(uri, post_data)
-
- def show_security_group_rule(self, security_group_rule_id, **fields):
- uri = '/security-group-rules/%s' % security_group_rule_id
- return self.show_resource(uri, **fields)
-
- def delete_security_group_rule(self, security_group_rule_id):
- uri = '/security-group-rules/%s' % security_group_rule_id
- return self.delete_resource(uri)
-
- def list_security_group_rules(self, **filters):
- uri = '/security-group-rules'
- return self.list_resources(uri, **filters)
-
- def show_extension(self, ext_alias, **fields):
- uri = '/extensions/%s' % ext_alias
- return self.show_resource(uri, **fields)
-
- def list_extensions(self, **filters):
- uri = '/extensions'
- return self.list_resources(uri, **filters)
-
- def create_bulk_network(self, **kwargs):
- """create bulk network
-
- Available params: see http://developer.openstack.org/
- api-ref-networking-v2.html#bulkCreateNetwork
- """
- uri = '/networks'
- return self.create_resource(uri, kwargs)
-
- def create_bulk_subnet(self, **kwargs):
- """create bulk subnet
-
- Available params: see http://developer.openstack.org/
- api-ref-networking-v2.html#bulkCreateSubnet
- """
- uri = '/subnets'
- return self.create_resource(uri, kwargs)
-
- def create_bulk_port(self, **kwargs):
- """create bulk port
-
- Available params: see http://developer.openstack.org/
- api-ref-networking-v2.html#bulkCreatePorts
- """
- uri = '/ports'
- return self.create_resource(uri, kwargs)
-
- def wait_for_resource_deletion(self, resource_type, id, client=None):
- """Waits for a resource to be deleted."""
- start_time = int(time.time())
- while True:
- if self.is_resource_deleted(resource_type, id, client=client):
- return
- if int(time.time()) - start_time >= self.build_timeout:
- raise exceptions.TimeoutException
- time.sleep(self.build_interval)
-
- def is_resource_deleted(self, resource_type, id, client=None):
- if client is None:
- client = self
- method = 'show_' + resource_type
- try:
- getattr(client, method)(id)
- except AttributeError:
- raise Exception("Unknown resource type %s " % resource_type)
- except lib_exc.NotFound:
- return True
- return False
-
- def wait_for_resource_status(self, fetch, status, interval=None,
- timeout=None):
- """Waits for a network resource to reach a status
-
- @param fetch: the callable to be used to query the resource status
- @type fecth: callable that takes no parameters and returns the resource
- @param status: the status that the resource has to reach
- @type status: String
- @param interval: the number of seconds to wait between each status
- query
- @type interval: Integer
- @param timeout: the maximum number of seconds to wait for the resource
- to reach the desired status
- @type timeout: Integer
- """
- if not interval:
- interval = self.build_interval
- if not timeout:
- timeout = self.build_timeout
- start_time = time.time()
-
- while time.time() - start_time <= timeout:
- resource = fetch()
- if resource['status'] == status:
- return
- time.sleep(interval)
-
- # At this point, the wait has timed out
- message = 'Resource %s' % (str(resource))
- message += ' failed to reach status %s' % status
- message += ' (current: %s)' % resource['status']
- message += ' within the required time %s' % timeout
- caller = misc.find_test_caller()
- if caller:
- message = '(%s) %s' % (caller, message)
- raise exceptions.TimeoutException(message)
-
- def create_router(self, name, admin_state_up=True, **kwargs):
- post_body = {'router': kwargs}
- post_body['router']['name'] = name
- post_body['router']['admin_state_up'] = admin_state_up
- uri = '/routers'
- return self.create_resource(uri, post_body)
-
- def _update_router(self, router_id, set_enable_snat, **kwargs):
- uri = '/routers/%s' % router_id
- body = self.show_resource(uri)
- update_body = {}
- update_body['name'] = kwargs.get('name', body['router']['name'])
- update_body['admin_state_up'] = kwargs.get(
- 'admin_state_up', body['router']['admin_state_up'])
- cur_gw_info = body['router']['external_gateway_info']
- if cur_gw_info:
- # TODO(kevinbenton): setting the external gateway info is not
- # allowed for a regular tenant. If the ability to update is also
- # merged, a test case for this will need to be added similar to
- # the SNAT case.
- cur_gw_info.pop('external_fixed_ips', None)
- if not set_enable_snat:
- cur_gw_info.pop('enable_snat', None)
- update_body['external_gateway_info'] = kwargs.get(
- 'external_gateway_info', body['router']['external_gateway_info'])
- if 'distributed' in kwargs:
- update_body['distributed'] = kwargs['distributed']
- update_body = dict(router=update_body)
- return self.update_resource(uri, update_body)
-
- def update_router(self, router_id, **kwargs):
- """Update a router leaving enable_snat to its default value."""
- # If external_gateway_info contains enable_snat the request will fail
- # with 404 unless executed with admin client, and therefore we instruct
- # _update_router to not set this attribute
- # NOTE(salv-orlando): The above applies as long as Neutron's default
- # policy is to restrict enable_snat usage to admins only.
- return self._update_router(router_id, set_enable_snat=False, **kwargs)
-
- def show_router(self, router_id, **fields):
- uri = '/routers/%s' % router_id
- return self.show_resource(uri, **fields)
-
- def delete_router(self, router_id):
- uri = '/routers/%s' % router_id
- return self.delete_resource(uri)
-
- def list_routers(self, **filters):
- uri = '/routers'
- return self.list_resources(uri, **filters)
-
- def update_router_with_snat_gw_info(self, router_id, **kwargs):
- """Update a router passing also the enable_snat attribute.
-
- This method must be execute with admin credentials, otherwise the API
- call will return a 404 error.
- """
- return self._update_router(router_id, set_enable_snat=True, **kwargs)
-
- def add_router_interface_with_subnet_id(self, router_id, subnet_id):
- uri = '/routers/%s/add_router_interface' % router_id
- update_body = {"subnet_id": subnet_id}
- return self.update_resource(uri, update_body)
-
- def add_router_interface_with_port_id(self, router_id, port_id):
- uri = '/routers/%s/add_router_interface' % router_id
- update_body = {"port_id": port_id}
- return self.update_resource(uri, update_body)
-
- def remove_router_interface_with_subnet_id(self, router_id, subnet_id):
- uri = '/routers/%s/remove_router_interface' % router_id
- update_body = {"subnet_id": subnet_id}
- return self.update_resource(uri, update_body)
-
- def remove_router_interface_with_port_id(self, router_id, port_id):
- uri = '/routers/%s/remove_router_interface' % router_id
- update_body = {"port_id": port_id}
- return self.update_resource(uri, update_body)
-
- def list_router_interfaces(self, uuid):
- uri = '/ports?device_id=%s' % uuid
- return self.list_resources(uri)
-
- def list_l3_agents_hosting_router(self, router_id):
- uri = '/routers/%s/l3-agents' % router_id
- return self.list_resources(uri)
-
- def list_dhcp_agent_hosting_network(self, network_id):
- uri = '/networks/%s/dhcp-agents' % network_id
- return self.list_resources(uri)
-
- def update_extra_routes(self, router_id, **kwargs):
- """Update Extra routes.
-
- Available params: see http://developer.openstack.org/
- api-ref-networking-v2-ext.html#updateExtraRoutes
- """
- uri = '/routers/%s' % router_id
- put_body = {'router': kwargs}
- return self.update_resource(uri, put_body)
-
- def delete_extra_routes(self, router_id):
- uri = '/routers/%s' % router_id
- put_body = {
- 'router': {
- 'routes': None
- }
- }
- return self.update_resource(uri, put_body)
diff --git a/tempest/services/network/json/networks_client.py b/tempest/services/network/json/networks_client.py
deleted file mode 100644
index 2907d44..0000000
--- a/tempest/services/network/json/networks_client.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# 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.services.network.json import base
-
-
-class NetworksClient(base.BaseNetworkClient):
-
- def create_network(self, **kwargs):
- uri = '/networks'
- post_data = {'network': kwargs}
- return self.create_resource(uri, post_data)
-
- def update_network(self, network_id, **kwargs):
- uri = '/networks/%s' % network_id
- post_data = {'network': kwargs}
- return self.update_resource(uri, post_data)
-
- def show_network(self, network_id, **fields):
- uri = '/networks/%s' % network_id
- return self.show_resource(uri, **fields)
-
- def delete_network(self, network_id):
- uri = '/networks/%s' % network_id
- return self.delete_resource(uri)
-
- def list_networks(self, **filters):
- uri = '/networks'
- return self.list_resources(uri, **filters)
diff --git a/tempest/services/network/json/ports_client.py b/tempest/services/network/json/ports_client.py
deleted file mode 100644
index d52d65e..0000000
--- a/tempest/services/network/json/ports_client.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# 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.services.network.json import base
-
-
-class PortsClient(base.BaseNetworkClient):
-
- def create_port(self, **kwargs):
- uri = '/ports'
- post_data = {'port': kwargs}
- return self.create_resource(uri, post_data)
-
- def update_port(self, port_id, **kwargs):
- uri = '/ports/%s' % port_id
- post_data = {'port': kwargs}
- return self.update_resource(uri, post_data)
-
- def show_port(self, port_id, **fields):
- uri = '/ports/%s' % port_id
- return self.show_resource(uri, **fields)
-
- def delete_port(self, port_id):
- uri = '/ports/%s' % port_id
- return self.delete_resource(uri)
-
- def list_ports(self, **filters):
- uri = '/ports'
- return self.list_resources(uri, **filters)
diff --git a/tempest/services/network/json/quotas_client.py b/tempest/services/network/json/quotas_client.py
deleted file mode 100644
index 9b65a54..0000000
--- a/tempest/services/network/json/quotas_client.py
+++ /dev/null
@@ -1,35 +0,0 @@
-# Copyright 2015 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.services.network.json import base
-
-
-class QuotasClient(base.BaseNetworkClient):
-
- def update_quotas(self, tenant_id, **kwargs):
- put_body = {'quota': kwargs}
- uri = '/quotas/%s' % tenant_id
- return self.update_resource(uri, put_body)
-
- def reset_quotas(self, tenant_id):
- uri = '/quotas/%s' % tenant_id
- return self.delete_resource(uri)
-
- def show_quotas(self, tenant_id, **fields):
- uri = '/quotas/%s' % tenant_id
- return self.show_resource(uri, **fields)
-
- def list_quotas(self, **filters):
- uri = '/quotas'
- return self.list_resources(uri, **filters)
diff --git a/tempest/services/network/json/security_groups_client.py b/tempest/services/network/json/security_groups_client.py
deleted file mode 100644
index a60d2a6..0000000
--- a/tempest/services/network/json/security_groups_client.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# 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.services.network.json import base
-
-
-class SecurityGroupsClient(base.BaseNetworkClient):
-
- def create_security_group(self, **kwargs):
- uri = '/security-groups'
- post_data = {'security_group': kwargs}
- return self.create_resource(uri, post_data)
-
- def update_security_group(self, security_group_id, **kwargs):
- uri = '/security-groups/%s' % security_group_id
- post_data = {'security_group': kwargs}
- return self.update_resource(uri, post_data)
-
- def show_security_group(self, security_group_id, **fields):
- uri = '/security-groups/%s' % security_group_id
- return self.show_resource(uri, **fields)
-
- def delete_security_group(self, security_group_id):
- uri = '/security-groups/%s' % security_group_id
- return self.delete_resource(uri)
-
- def list_security_groups(self, **filters):
- uri = '/security-groups'
- return self.list_resources(uri, **filters)
diff --git a/tempest/services/network/json/subnetpools_client.py b/tempest/services/network/json/subnetpools_client.py
deleted file mode 100644
index b5aec47..0000000
--- a/tempest/services/network/json/subnetpools_client.py
+++ /dev/null
@@ -1,40 +0,0 @@
-# Copyright 2015 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.services.network.json import base
-
-
-class SubnetpoolsClient(base.BaseNetworkClient):
-
- def list_subnetpools(self, **filters):
- uri = '/subnetpools'
- return self.list_resources(uri, **filters)
-
- def create_subnetpools(self, **kwargs):
- uri = '/subnetpools'
- post_data = {'subnetpool': kwargs}
- return self.create_resource(uri, post_data)
-
- def show_subnetpools(self, subnetpool_id, **fields):
- uri = '/subnetpools/%s' % subnetpool_id
- return self.show_resource(uri, **fields)
-
- def update_subnetpools(self, subnetpool_id, **kwargs):
- uri = '/subnetpools/%s' % subnetpool_id
- post_data = {'subnetpool': kwargs}
- return self.update_resource(uri, post_data)
-
- def delete_subnetpools(self, subnetpool_id):
- uri = '/subnetpools/%s' % subnetpool_id
- return self.delete_resource(uri)
diff --git a/tempest/services/network/json/subnets_client.py b/tempest/services/network/json/subnets_client.py
deleted file mode 100644
index 957b606..0000000
--- a/tempest/services/network/json/subnets_client.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# 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.services.network.json import base
-
-
-class SubnetsClient(base.BaseNetworkClient):
-
- def create_subnet(self, **kwargs):
- uri = '/subnets'
- post_data = {'subnet': kwargs}
- return self.create_resource(uri, post_data)
-
- def update_subnet(self, subnet_id, **kwargs):
- uri = '/subnets/%s' % subnet_id
- post_data = {'subnet': kwargs}
- return self.update_resource(uri, post_data)
-
- def show_subnet(self, subnet_id, **fields):
- uri = '/subnets/%s' % subnet_id
- return self.show_resource(uri, **fields)
-
- def delete_subnet(self, subnet_id):
- uri = '/subnets/%s' % subnet_id
- return self.delete_resource(uri)
-
- def list_subnets(self, **filters):
- uri = '/subnets'
- return self.list_resources(uri, **filters)
diff --git a/tempest/services/network/resources.py b/tempest/services/network/resources.py
deleted file mode 100644
index 10911f7..0000000
--- a/tempest/services/network/resources.py
+++ /dev/null
@@ -1,189 +0,0 @@
-# Copyright 2013 Hewlett-Packard Development Company, L.P.
-# 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 abc
-
-import six
-
-
-class AttributeDict(dict):
- """Provide attribute access (dict.key) to dictionary values."""
-
- def __getattr__(self, name):
- """Allow attribute access for all keys in the dict."""
- if name in self:
- return self[name]
- return super(AttributeDict, self).__getattribute__(name)
-
-
-@six.add_metaclass(abc.ABCMeta)
-class DeletableResource(AttributeDict):
- """Support deletion of neutron resources (networks, subnets)
-
- via a delete() method, as is supported by keystone and nova resources.
- """
-
- def __init__(self, *args, **kwargs):
- self.client = kwargs.pop('client', None)
- self.network_client = kwargs.pop('network_client', None)
- self.networks_client = kwargs.pop('networks_client', None)
- self.subnets_client = kwargs.pop('subnets_client', None)
- self.ports_client = kwargs.pop('ports_client', None)
- super(DeletableResource, self).__init__(*args, **kwargs)
-
- def __str__(self):
- return '<%s id="%s" name="%s">' % (self.__class__.__name__,
- self.id, self.name)
-
- @abc.abstractmethod
- def delete(self):
- return
-
- @abc.abstractmethod
- def refresh(self):
- return
-
- def __hash__(self):
- return hash(self.id)
-
- def wait_for_status(self, status):
- if not hasattr(self, 'status'):
- return
-
- def helper_get():
- self.refresh()
- return self
-
- return self.client.wait_for_resource_status(helper_get, status)
-
-
-class DeletableNetwork(DeletableResource):
-
- def delete(self):
- self.networks_client.delete_network(self.id)
-
-
-class DeletableSubnet(DeletableResource):
-
- def __init__(self, *args, **kwargs):
- super(DeletableSubnet, self).__init__(*args, **kwargs)
- self._router_ids = set()
-
- def update(self, *args, **kwargs):
- result = self.subnets_client.update_subnet(self.id,
- *args,
- **kwargs)
- return super(DeletableSubnet, self).update(**result['subnet'])
-
- def add_to_router(self, router_id):
- self._router_ids.add(router_id)
- self.network_client.add_router_interface_with_subnet_id(
- router_id, subnet_id=self.id)
-
- def delete(self):
- for router_id in self._router_ids.copy():
- self.network_client.remove_router_interface_with_subnet_id(
- router_id,
- subnet_id=self.id)
- self._router_ids.remove(router_id)
- self.subnets_client.delete_subnet(self.id)
-
-
-class DeletableRouter(DeletableResource):
-
- def set_gateway(self, network_id):
- return self.update(external_gateway_info=dict(network_id=network_id))
-
- def unset_gateway(self):
- return self.update(external_gateway_info=dict())
-
- def update(self, *args, **kwargs):
- result = self.client.update_router(self.id,
- *args,
- **kwargs)
- return super(DeletableRouter, self).update(**result['router'])
-
- def delete(self):
- self.unset_gateway()
- self.client.delete_router(self.id)
-
-
-class DeletableFloatingIp(DeletableResource):
-
- def refresh(self, *args, **kwargs):
- result = self.client.show_floatingip(self.id,
- *args,
- **kwargs)
- super(DeletableFloatingIp, self).update(**result['floatingip'])
-
- def update(self, *args, **kwargs):
- result = self.client.update_floatingip(self.id,
- *args,
- **kwargs)
- super(DeletableFloatingIp, self).update(**result['floatingip'])
-
- def __repr__(self):
- return '<%s addr="%s">' % (self.__class__.__name__,
- self.floating_ip_address)
-
- def __str__(self):
- return '<"FloatingIP" addr="%s" id="%s">' % (self.floating_ip_address,
- self.id)
-
- def delete(self):
- self.client.delete_floatingip(self.id)
-
-
-class DeletablePort(DeletableResource):
-
- def delete(self):
- self.ports_client.delete_port(self.id)
-
-
-class DeletableSecurityGroup(DeletableResource):
-
- def delete(self):
- self.client.delete_security_group(self.id)
-
-
-class DeletableSecurityGroupRule(DeletableResource):
-
- def __repr__(self):
- return '<%s id="%s">' % (self.__class__.__name__, self.id)
-
- def delete(self):
- self.client.delete_security_group_rule(self.id)
-
-
-class DeletablePool(DeletableResource):
-
- def delete(self):
- self.client.delete_pool(self.id)
-
-
-class DeletableMember(DeletableResource):
-
- def delete(self):
- self.client.delete_member(self.id)
-
-
-class DeletableVip(DeletableResource):
-
- def delete(self):
- self.client.delete_vip(self.id)
-
- def refresh(self):
- result = self.client.show_vip(self.id)
- super(DeletableVip, self).update(**result['vip'])
diff --git a/tempest/services/object_storage/account_client.py b/tempest/services/object_storage/account_client.py
deleted file mode 100644
index 2c7fe29..0000000
--- a/tempest/services/object_storage/account_client.py
+++ /dev/null
@@ -1,156 +0,0 @@
-# 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 xml.etree import ElementTree as etree
-
-from oslo_serialization import jsonutils as json
-from six.moves.urllib import parse as urllib
-
-from tempest.common import service_client
-
-
-class AccountClient(service_client.ServiceClient):
-
- def create_account(self, data=None,
- params=None,
- metadata=None,
- remove_metadata=None,
- metadata_prefix='X-Account-Meta-',
- remove_metadata_prefix='X-Remove-Account-Meta-'):
- """Create an account."""
- if metadata is None:
- metadata = {}
- if remove_metadata is None:
- remove_metadata = {}
- url = ''
- if params:
- url += '?%s' % urllib.urlencode(params)
-
- headers = {}
- for key in metadata:
- headers[metadata_prefix + key] = metadata[key]
- for key in remove_metadata:
- headers[remove_metadata_prefix + key] = remove_metadata[key]
-
- resp, body = self.put(url, data, headers)
- self.expected_success(200, resp.status)
- return resp, body
-
- def delete_account(self, data=None, params=None):
- """Delete an account."""
- url = ''
- if params:
- url = '?%s%s' % (url, urllib.urlencode(params))
-
- resp, body = self.delete(url, headers={}, body=data)
- self.expected_success(200, resp.status)
- return resp, body
-
- def list_account_metadata(self):
- """HEAD on the storage URL
-
- Returns all account metadata headers
- """
- resp, body = self.head('')
- self.expected_success(204, resp.status)
- return resp, body
-
- def create_account_metadata(self, metadata,
- metadata_prefix='X-Account-Meta-',
- data=None, params=None):
- """Creates an account metadata entry."""
- headers = {}
- if metadata:
- for key in metadata:
- headers[metadata_prefix + key] = metadata[key]
-
- url = ''
- if params:
- url = '?%s%s' % (url, urllib.urlencode(params))
-
- resp, body = self.post(url, headers=headers, body=data)
- self.expected_success([200, 204], resp.status)
- return resp, body
-
- def delete_account_metadata(self, metadata,
- metadata_prefix='X-Remove-Account-Meta-'):
- """Deletes an account metadata entry."""
-
- headers = {}
- for item in metadata:
- headers[metadata_prefix + item] = metadata[item]
- resp, body = self.post('', headers=headers, body=None)
- self.expected_success(204, resp.status)
- return resp, body
-
- def create_and_delete_account_metadata(
- self,
- create_metadata=None,
- delete_metadata=None,
- create_metadata_prefix='X-Account-Meta-',
- delete_metadata_prefix='X-Remove-Account-Meta-'):
- """Creates and deletes an account metadata entry."""
- headers = {}
- for key in create_metadata:
- headers[create_metadata_prefix + key] = create_metadata[key]
- for key in delete_metadata:
- headers[delete_metadata_prefix + key] = delete_metadata[key]
-
- resp, body = self.post('', headers=headers, body=None)
- self.expected_success(204, resp.status)
- return resp, body
-
- def list_account_containers(self, params=None):
- """GET on the (base) storage URL
-
- Given valid X-Auth-Token, returns a list of all containers for the
- account.
-
- Optional Arguments:
- limit=[integer value N]
- Limits the number of results to at most N values
- DEFAULT: 10,000
-
- marker=[string value X]
- Given string value X, return object names greater in value
- than the specified marker.
- DEFAULT: No Marker
-
- format=[string value, either 'json' or 'xml']
- Specify either json or xml to return the respective serialized
- response.
- DEFAULT: Python-List returned in response body
- """
- url = '?%s' % urllib.urlencode(params) if params else ''
-
- resp, body = self.get(url, headers={})
- if params and params.get('format') == 'json':
- body = json.loads(body)
- elif params and params.get('format') == 'xml':
- body = etree.fromstring(body)
- else:
- body = body.strip().splitlines()
- self.expected_success([200, 204], resp.status)
- return resp, body
-
- def list_extensions(self):
- self.skip_path()
- try:
- resp, body = self.get('info')
- finally:
- self.reset_path()
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return resp, body
diff --git a/tempest/services/object_storage/container_client.py b/tempest/services/object_storage/container_client.py
deleted file mode 100644
index 73c25db..0000000
--- a/tempest/services/object_storage/container_client.py
+++ /dev/null
@@ -1,166 +0,0 @@
-# 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 xml.etree import ElementTree as etree
-
-from oslo_serialization import jsonutils as json
-from six.moves.urllib import parse as urllib
-
-from tempest.common import service_client
-
-
-class ContainerClient(service_client.ServiceClient):
-
- def create_container(
- self, container_name,
- metadata=None,
- remove_metadata=None,
- metadata_prefix='X-Container-Meta-',
- remove_metadata_prefix='X-Remove-Container-Meta-'):
- """Creates a container
-
- with optional metadata passed in as a dictionary
- """
- url = str(container_name)
- headers = {}
-
- if metadata is not None:
- for key in metadata:
- headers[metadata_prefix + key] = metadata[key]
- if remove_metadata is not None:
- for key in remove_metadata:
- headers[remove_metadata_prefix + key] = remove_metadata[key]
-
- resp, body = self.put(url, body=None, headers=headers)
- self.expected_success([201, 202], resp.status)
- return resp, body
-
- def delete_container(self, container_name):
- """Deletes the container (if it's empty)."""
- url = str(container_name)
- resp, body = self.delete(url)
- self.expected_success(204, resp.status)
- return resp, body
-
- def update_container_metadata(
- self, container_name,
- metadata=None,
- remove_metadata=None,
- metadata_prefix='X-Container-Meta-',
- remove_metadata_prefix='X-Remove-Container-Meta-'):
- """Updates arbitrary metadata on container."""
- url = str(container_name)
- headers = {}
-
- if metadata is not None:
- for key in metadata:
- headers[metadata_prefix + key] = metadata[key]
- if remove_metadata is not None:
- for key in remove_metadata:
- headers[remove_metadata_prefix + key] = remove_metadata[key]
-
- resp, body = self.post(url, body=None, headers=headers)
- self.expected_success(204, resp.status)
- return resp, body
-
- def delete_container_metadata(self, container_name, metadata,
- metadata_prefix='X-Remove-Container-Meta-'):
- """Deletes arbitrary metadata on container."""
- url = str(container_name)
- headers = {}
-
- if metadata is not None:
- for item in metadata:
- headers[metadata_prefix + item] = metadata[item]
-
- resp, body = self.post(url, body=None, headers=headers)
- self.expected_success(204, resp.status)
- return resp, body
-
- def list_container_metadata(self, container_name):
- """Retrieves container metadata headers"""
- url = str(container_name)
- resp, body = self.head(url)
- self.expected_success(204, resp.status)
- return resp, body
-
- def list_all_container_objects(self, container, params=None):
- """Returns complete list of all objects in the container
-
- even if item count is beyond 10,000 item listing limit.
- Does not require any parameters aside from container name.
- """
- # TODO(dwalleck): Rewrite using json format to avoid newlines at end of
- # obj names. Set limit to API limit - 1 (max returned items = 9999)
- limit = 9999
- if params is not None:
- if 'limit' in params:
- limit = params['limit']
-
- if 'marker' in params:
- limit = params['marker']
-
- resp, objlist = self.list_container_contents(
- container,
- params={'limit': limit, 'format': 'json'})
- self.expected_success(200, resp.status)
- return objlist
-
- def list_container_contents(self, container, params=None):
- """List the objects in a container, given the container name
-
- Returns the container object listing as a plain text list, or as
- xml or json if that option is specified via the 'format' argument.
-
- Optional Arguments:
- limit = integer
- For an integer value n, limits the number of results to at most
- n values.
-
- marker = 'string'
- Given a string value x, return object names greater in value
- than the specified marker.
-
- prefix = 'string'
- For a string value x, causes the results to be limited to names
- beginning with the substring x.
-
- format = 'json' or 'xml'
- Specify either json or xml to return the respective serialized
- response.
- If json, returns a list of json objects
- if xml, returns a string of xml
-
- path = 'string'
- For a string value x, return the object names nested in the
- pseudo path (assuming preconditions are met - see below).
-
- delimiter = 'character'
- For a character c, return all the object names nested in the
- container (without the need for the directory marker objects).
- """
-
- url = str(container)
- if params:
- url += '?'
- url += '&%s' % urllib.urlencode(params)
-
- resp, body = self.get(url, headers={})
- if params and params.get('format') == 'json':
- body = json.loads(body)
- elif params and params.get('format') == 'xml':
- body = etree.fromstring(body)
- self.expected_success([200, 204], resp.status)
- return resp, body
diff --git a/tempest/services/object_storage/object_client.py b/tempest/services/object_storage/object_client.py
deleted file mode 100644
index 5890e33..0000000
--- a/tempest/services/object_storage/object_client.py
+++ /dev/null
@@ -1,258 +0,0 @@
-# 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.
-
-import six
-from six.moves import http_client as httplib
-from six.moves.urllib import parse as urlparse
-
-from tempest.common import service_client
-
-
-class ObjectClient(service_client.ServiceClient):
-
- def create_object(self, container, object_name, data,
- params=None, metadata=None, headers=None):
- """Create storage object."""
-
- if headers is None:
- headers = self.get_headers()
- if not data:
- headers['content-length'] = '0'
- if metadata:
- for key in metadata:
- headers[str(key)] = metadata[key]
- url = "%s/%s" % (str(container), str(object_name))
- if params:
- url += '?%s' % urlparse.urlencode(params)
-
- resp, body = self.put(url, data, headers)
- self.expected_success(201, resp.status)
- return resp, body
-
- def update_object(self, container, object_name, data):
- """Upload data to replace current storage object."""
- resp, body = self.create_object(container, object_name, data)
- self.expected_success(201, resp.status)
- return resp, body
-
- def delete_object(self, container, object_name, params=None):
- """Delete storage object."""
- url = "%s/%s" % (str(container), str(object_name))
- if params:
- url += '?%s' % urlparse.urlencode(params)
- resp, body = self.delete(url, headers={})
- self.expected_success([200, 204], resp.status)
- return resp, body
-
- def update_object_metadata(self, container, object_name, metadata,
- metadata_prefix='X-Object-Meta-'):
- """Add, remove, or change X-Object-Meta metadata for storage object."""
-
- headers = {}
- for key in metadata:
- headers["%s%s" % (str(metadata_prefix), str(key))] = metadata[key]
-
- url = "%s/%s" % (str(container), str(object_name))
- resp, body = self.post(url, None, headers=headers)
- self.expected_success(202, resp.status)
- return resp, body
-
- def list_object_metadata(self, container, object_name):
- """List all storage object X-Object-Meta- metadata."""
-
- url = "%s/%s" % (str(container), str(object_name))
- resp, body = self.head(url)
- self.expected_success(200, resp.status)
- return resp, body
-
- def get_object(self, container, object_name, metadata=None):
- """Retrieve object's data."""
-
- headers = {}
- if metadata:
- for key in metadata:
- headers[str(key)] = metadata[key]
-
- url = "{0}/{1}".format(container, object_name)
- resp, body = self.get(url, headers=headers)
- self.expected_success([200, 206], resp.status)
- return resp, body
-
- def copy_object_in_same_container(self, container, src_object_name,
- dest_object_name, metadata=None):
- """Copy storage object's data to the new object using PUT."""
-
- url = "{0}/{1}".format(container, dest_object_name)
- headers = {}
- headers['X-Copy-From'] = "%s/%s" % (str(container),
- str(src_object_name))
- headers['content-length'] = '0'
- if metadata:
- for key in metadata:
- headers[str(key)] = metadata[key]
-
- resp, body = self.put(url, None, headers=headers)
- self.expected_success(201, resp.status)
- return resp, body
-
- def copy_object_across_containers(self, src_container, src_object_name,
- dst_container, dst_object_name,
- metadata=None):
- """Copy storage object's data to the new object using PUT."""
-
- url = "{0}/{1}".format(dst_container, dst_object_name)
- headers = {}
- headers['X-Copy-From'] = "%s/%s" % (str(src_container),
- str(src_object_name))
- headers['content-length'] = '0'
- if metadata:
- for key in metadata:
- headers[str(key)] = metadata[key]
-
- resp, body = self.put(url, None, headers=headers)
- self.expected_success(201, resp.status)
- return resp, body
-
- def copy_object_2d_way(self, container, src_object_name, dest_object_name,
- metadata=None):
- """Copy storage object's data to the new object using COPY."""
-
- url = "{0}/{1}".format(container, src_object_name)
- headers = {}
- headers['Destination'] = "%s/%s" % (str(container),
- str(dest_object_name))
- if metadata:
- for key in metadata:
- headers[str(key)] = metadata[key]
-
- resp, body = self.copy(url, headers=headers)
- self.expected_success(201, resp.status)
- return resp, body
-
- def create_object_segments(self, container, object_name, segment, data):
- """Creates object segments."""
- url = "{0}/{1}/{2}".format(container, object_name, segment)
- resp, body = self.put(url, data)
- self.expected_success(201, resp.status)
- return resp, body
-
- def put_object_with_chunk(self, container, name, contents, chunk_size):
- """Put an object with Transfer-Encoding header"""
- if self.base_url is None:
- self._set_auth()
-
- headers = {'Transfer-Encoding': 'chunked'}
- if self.token:
- headers['X-Auth-Token'] = self.token
-
- conn = put_object_connection(self.base_url, container, name, contents,
- chunk_size, headers)
-
- resp = conn.getresponse()
- body = resp.read()
-
- resp_headers = {}
- for header, value in resp.getheaders():
- resp_headers[header.lower()] = value
-
- self._error_checker('PUT', None, headers, contents, resp, body)
- self.expected_success(201, resp.status)
- return resp.status, resp.reason, resp_headers
-
- def create_object_continue(self, container, object_name,
- data, metadata=None):
- """Create storage object."""
- headers = {}
- if metadata:
- for key in metadata:
- headers[str(key)] = metadata[key]
-
- if not data:
- headers['content-length'] = '0'
-
- if self.base_url is None:
- self._set_auth()
- headers['X-Auth-Token'] = self.token
-
- conn = put_object_connection(self.base_url, str(container),
- str(object_name), data, None, headers)
-
- response = conn.response_class(conn.sock,
- strict=conn.strict,
- method=conn._method)
- version, status, reason = response._read_status()
- resp = {'version': version,
- 'status': str(status),
- 'reason': reason}
-
- return resp
-
-
-def put_object_connection(base_url, container, name, contents=None,
- chunk_size=65536, headers=None, query_string=None):
- """Helper function to make connection to put object with httplib
-
- :param base_url: base_url of an object client
- :param container: container name that the object is in
- :param name: object name to put
- :param contents: a string or a file like object to read object data
- from; if None, a zero-byte put will be done
- :param chunk_size: chunk size of data to write; it defaults to 65536;
- used only if the the contents object has a 'read'
- method, eg. file-like objects, ignored otherwise
- :param headers: additional headers to include in the request, if any
- :param query_string: if set will be appended with '?' to generated path
- """
- parsed = urlparse.urlparse(base_url)
- if parsed.scheme == 'https':
- conn = httplib.HTTPSConnection(parsed.netloc)
- else:
- conn = httplib.HTTPConnection(parsed.netloc)
- path = str(parsed.path) + "/"
- path += "%s/%s" % (str(container), str(name))
-
- if query_string:
- path += '?' + query_string
- if headers:
- headers = dict(headers)
- else:
- headers = {}
- if hasattr(contents, 'read'):
- conn.putrequest('PUT', path)
- for header, value in six.iteritems(headers):
- conn.putheader(header, value)
- if 'Content-Length' not in headers:
- if 'Transfer-Encoding' not in headers:
- conn.putheader('Transfer-Encoding', 'chunked')
- conn.endheaders()
- chunk = contents.read(chunk_size)
- while chunk:
- conn.send('%x\r\n%s\r\n' % (len(chunk), chunk))
- chunk = contents.read(chunk_size)
- conn.send('0\r\n\r\n')
- else:
- conn.endheaders()
- left = headers['Content-Length']
- while left > 0:
- size = chunk_size
- if size > left:
- size = left
- chunk = contents.read(size)
- conn.send(chunk)
- left -= len(chunk)
- else:
- conn.request('PUT', path, contents, headers)
-
- return conn
diff --git a/tempest/services/orchestration/__init__.py b/tempest/services/orchestration/__init__.py
index e69de29..5a1ffcc 100644
--- a/tempest/services/orchestration/__init__.py
+++ b/tempest/services/orchestration/__init__.py
@@ -0,0 +1,18 @@
+# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P.
+#
+# 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.services.orchestration.json.orchestration_client import \
+ OrchestrationClient
+
+__all__ = ['OrchestrationClient']
diff --git a/tempest/services/orchestration/json/orchestration_client.py b/tempest/services/orchestration/json/orchestration_client.py
index 22e53f5..9fec548 100644
--- a/tempest/services/orchestration/json/orchestration_client.py
+++ b/tempest/services/orchestration/json/orchestration_client.py
@@ -18,13 +18,13 @@
from oslo_serialization import jsonutils as json
from six.moves.urllib import parse as urllib
-from tempest_lib import exceptions as lib_exc
-from tempest.common import service_client
from tempest import exceptions
+from tempest.lib.common import rest_client
+from tempest.lib import exceptions as lib_exc
-class OrchestrationClient(service_client.ServiceClient):
+class OrchestrationClient(rest_client.RestClient):
def list_stacks(self, params=None):
"""Lists all stacks for a user."""
@@ -36,7 +36,7 @@
resp, body = self.get(uri)
self.expected_success(200, resp.status)
body = json.loads(body)
- return service_client.ResponseBody(resp, body)
+ return rest_client.ResponseBody(resp, body)
def create_stack(self, name, disable_rollback=True, parameters=None,
timeout_mins=60, template=None, template_url=None,
@@ -56,7 +56,7 @@
resp, body = self.post(uri, headers=headers, body=body)
self.expected_success(201, resp.status)
body = json.loads(body)
- return service_client.ResponseBody(resp, body)
+ return rest_client.ResponseBody(resp, body)
def update_stack(self, stack_identifier, name, disable_rollback=True,
parameters=None, timeout_mins=60, template=None,
@@ -75,7 +75,7 @@
uri = "stacks/%s" % stack_identifier
resp, body = self.put(uri, headers=headers, body=body)
self.expected_success(202, resp.status)
- return service_client.ResponseBody(resp, body)
+ return rest_client.ResponseBody(resp, body)
def _prepare_update_create(self, name, disable_rollback=True,
parameters=None, timeout_mins=60,
@@ -111,7 +111,7 @@
resp, body = self.get(url)
self.expected_success(200, resp.status)
body = json.loads(body)
- return service_client.ResponseBody(resp, body)
+ return rest_client.ResponseBody(resp, body)
def suspend_stack(self, stack_identifier):
"""Suspend a stack."""
@@ -119,7 +119,7 @@
body = {'suspend': None}
resp, body = self.post(url, json.dumps(body))
self.expected_success(200, resp.status)
- return service_client.ResponseBody(resp)
+ return rest_client.ResponseBody(resp)
def resume_stack(self, stack_identifier):
"""Resume a stack."""
@@ -127,7 +127,7 @@
body = {'resume': None}
resp, body = self.post(url, json.dumps(body))
self.expected_success(200, resp.status)
- return service_client.ResponseBody(resp)
+ return rest_client.ResponseBody(resp)
def list_resources(self, stack_identifier):
"""Returns the details of a single resource."""
@@ -135,7 +135,7 @@
resp, body = self.get(url)
self.expected_success(200, resp.status)
body = json.loads(body)
- return service_client.ResponseBody(resp, body)
+ return rest_client.ResponseBody(resp, body)
def show_resource(self, stack_identifier, resource_name):
"""Returns the details of a single resource."""
@@ -143,49 +143,13 @@
resp, body = self.get(url)
self.expected_success(200, resp.status)
body = json.loads(body)
- return service_client.ResponseBody(resp, body)
+ return rest_client.ResponseBody(resp, body)
def delete_stack(self, stack_identifier):
"""Deletes the specified Stack."""
resp, _ = self.delete("stacks/%s" % str(stack_identifier))
self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp)
-
- def wait_for_resource_status(self, stack_identifier, resource_name,
- status, failure_pattern='^.*_FAILED$'):
- """Waits for a Resource to reach a given status."""
- start = int(time.time())
- fail_regexp = re.compile(failure_pattern)
-
- while True:
- try:
- body = self.show_resource(
- stack_identifier, resource_name)['resource']
- except lib_exc.NotFound:
- # ignore this, as the resource may not have
- # been created yet
- pass
- else:
- resource_name = body['resource_name']
- resource_status = body['resource_status']
- if resource_status == status:
- return
- if fail_regexp.search(resource_status):
- raise exceptions.StackResourceBuildErrorException(
- resource_name=resource_name,
- stack_identifier=stack_identifier,
- resource_status=resource_status,
- resource_status_reason=body['resource_status_reason'])
-
- if int(time.time()) - start >= self.build_timeout:
- message = ('Resource %s failed to reach %s status '
- '(current %s) within the required time (%s s).' %
- (resource_name,
- status,
- resource_status,
- self.build_timeout))
- raise exceptions.TimeoutException(message)
- time.sleep(self.build_interval)
+ return rest_client.ResponseBody(resp)
def wait_for_stack_status(self, stack_identifier, status,
failure_pattern='^.*_FAILED$'):
@@ -214,7 +178,7 @@
'within the required time (%s s).' %
(stack_name, status, stack_status,
self.build_timeout))
- raise exceptions.TimeoutException(message)
+ raise lib_exc.TimeoutException(message)
time.sleep(self.build_interval)
def show_resource_metadata(self, stack_identifier, resource_name):
@@ -224,7 +188,7 @@
resp, body = self.get(url)
self.expected_success(200, resp.status)
body = json.loads(body)
- return service_client.ResponseBody(resp, body)
+ return rest_client.ResponseBody(resp, body)
def list_events(self, stack_identifier):
"""Returns list of all events for a stack."""
@@ -232,7 +196,7 @@
resp, body = self.get(url)
self.expected_success(200, resp.status)
body = json.loads(body)
- return service_client.ResponseBody(resp, body)
+ return rest_client.ResponseBody(resp, body)
def list_resource_events(self, stack_identifier, resource_name):
"""Returns list of all events for a resource from stack."""
@@ -241,7 +205,7 @@
resp, body = self.get(url)
self.expected_success(200, resp.status)
body = json.loads(body)
- return service_client.ResponseBody(resp, body)
+ return rest_client.ResponseBody(resp, body)
def show_event(self, stack_identifier, resource_name, event_id):
"""Returns the details of a single stack's event."""
@@ -250,7 +214,7 @@
resp, body = self.get(url)
self.expected_success(200, resp.status)
body = json.loads(body)
- return service_client.ResponseBody(resp, body)
+ return rest_client.ResponseBody(resp, body)
def show_template(self, stack_identifier):
"""Returns the template for the stack."""
@@ -258,7 +222,7 @@
resp, body = self.get(url)
self.expected_success(200, resp.status)
body = json.loads(body)
- return service_client.ResponseBody(resp, body)
+ return rest_client.ResponseBody(resp, body)
def _validate_template(self, post_body):
"""Returns the validation request result."""
@@ -266,7 +230,7 @@
resp, body = self.post('validate', post_body)
self.expected_success(200, resp.status)
body = json.loads(body)
- return service_client.ResponseBody(resp, body)
+ return rest_client.ResponseBody(resp, body)
def validate_template(self, template, parameters=None):
"""Returns the validation result for a template with parameters."""
@@ -293,21 +257,21 @@
resp, body = self.get('resource_types')
self.expected_success(200, resp.status)
body = json.loads(body)
- return service_client.ResponseBody(resp, body)
+ return rest_client.ResponseBody(resp, body)
def show_resource_type(self, resource_type_name):
"""Return the schema of a resource type."""
url = 'resource_types/%s' % resource_type_name
resp, body = self.get(url)
self.expected_success(200, resp.status)
- return service_client.ResponseBody(resp, json.loads(body))
+ return rest_client.ResponseBody(resp, json.loads(body))
def show_resource_type_template(self, resource_type_name):
"""Return the template of a resource type."""
url = 'resource_types/%s/template' % resource_type_name
resp, body = self.get(url)
self.expected_success(200, resp.status)
- return service_client.ResponseBody(resp, json.loads(body))
+ return rest_client.ResponseBody(resp, json.loads(body))
def create_software_config(self, name=None, config=None, group=None,
inputs=None, outputs=None, options=None):
@@ -318,7 +282,7 @@
resp, body = self.post(url, headers=headers, body=body)
self.expected_success(200, resp.status)
body = json.loads(body)
- return service_client.ResponseBody(resp, body)
+ return rest_client.ResponseBody(resp, body)
def show_software_config(self, conf_id):
"""Returns a software configuration resource."""
@@ -326,14 +290,14 @@
resp, body = self.get(url)
self.expected_success(200, resp.status)
body = json.loads(body)
- return service_client.ResponseBody(resp, body)
+ return rest_client.ResponseBody(resp, body)
def delete_software_config(self, conf_id):
"""Deletes a specific software configuration."""
url = 'software_configs/%s' % str(conf_id)
resp, _ = self.delete(url)
self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp)
+ return rest_client.ResponseBody(resp)
def create_software_deploy(self, server_id=None, config_id=None,
action=None, status=None,
@@ -348,7 +312,7 @@
resp, body = self.post(url, headers=headers, body=body)
self.expected_success(200, resp.status)
body = json.loads(body)
- return service_client.ResponseBody(resp, body)
+ return rest_client.ResponseBody(resp, body)
def update_software_deploy(self, deploy_id=None, server_id=None,
config_id=None, action=None, status=None,
@@ -363,7 +327,7 @@
resp, body = self.put(url, headers=headers, body=body)
self.expected_success(200, resp.status)
body = json.loads(body)
- return service_client.ResponseBody(resp, body)
+ return rest_client.ResponseBody(resp, body)
def list_software_deployments(self):
"""Returns a list of all deployments."""
@@ -371,7 +335,7 @@
resp, body = self.get(url)
self.expected_success(200, resp.status)
body = json.loads(body)
- return service_client.ResponseBody(resp, body)
+ return rest_client.ResponseBody(resp, body)
def show_software_deployment(self, deploy_id):
"""Returns a specific software deployment."""
@@ -379,7 +343,7 @@
resp, body = self.get(url)
self.expected_success(200, resp.status)
body = json.loads(body)
- return service_client.ResponseBody(resp, body)
+ return rest_client.ResponseBody(resp, body)
def show_software_deployment_metadata(self, server_id):
"""Return a config metadata for a specific server."""
@@ -387,14 +351,14 @@
resp, body = self.get(url)
self.expected_success(200, resp.status)
body = json.loads(body)
- return service_client.ResponseBody(resp, body)
+ return rest_client.ResponseBody(resp, body)
def delete_software_deploy(self, deploy_id):
"""Deletes a specific software deployment."""
url = 'software_deployments/%s' % str(deploy_id)
resp, _ = self.delete(url)
self.expected_success(204, resp.status)
- return service_client.ResponseBody(resp)
+ return rest_client.ResponseBody(resp)
def _prep_software_config_create(self, name=None, conf=None, group=None,
inputs=None, outputs=None, options=None):
diff --git a/tempest/services/telemetry/__init__.py b/tempest/services/telemetry/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/services/telemetry/__init__.py
+++ /dev/null
diff --git a/tempest/services/telemetry/json/__init__.py b/tempest/services/telemetry/json/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/services/telemetry/json/__init__.py
+++ /dev/null
diff --git a/tempest/services/telemetry/json/alarming_client.py b/tempest/services/telemetry/json/alarming_client.py
deleted file mode 100644
index ce14211..0000000
--- a/tempest/services/telemetry/json/alarming_client.py
+++ /dev/null
@@ -1,98 +0,0 @@
-# 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.common import service_client
-
-
-class AlarmingClient(service_client.ServiceClient):
-
- version = '2'
- uri_prefix = "v2"
-
- def deserialize(self, body):
- return json.loads(body.replace("\n", ""))
-
- def serialize(self, body):
- return json.dumps(body)
-
- def list_alarms(self, query=None):
- uri = '%s/alarms' % self.uri_prefix
- uri_dict = {}
- if query:
- uri_dict = {'q.field': query[0],
- 'q.op': query[1],
- 'q.value': query[2]}
- if uri_dict:
- uri += "?%s" % urllib.urlencode(uri_dict)
- resp, body = self.get(uri)
- self.expected_success(200, resp.status)
- body = self.deserialize(body)
- return service_client.ResponseBodyList(resp, body)
-
- def show_alarm(self, alarm_id):
- uri = '%s/alarms/%s' % (self.uri_prefix, alarm_id)
- resp, body = self.get(uri)
- self.expected_success(200, resp.status)
- body = self.deserialize(body)
- return service_client.ResponseBody(resp, body)
-
- def show_alarm_history(self, alarm_id):
- uri = "%s/alarms/%s/history" % (self.uri_prefix, alarm_id)
- resp, body = self.get(uri)
- self.expected_success(200, resp.status)
- body = self.deserialize(body)
- return service_client.ResponseBodyList(resp, body)
-
- def delete_alarm(self, alarm_id):
- uri = "%s/alarms/%s" % (self.uri_prefix, alarm_id)
- resp, body = self.delete(uri)
- self.expected_success(204, resp.status)
- if body:
- body = self.deserialize(body)
- return service_client.ResponseBody(resp, body)
-
- def create_alarm(self, **kwargs):
- uri = "%s/alarms" % self.uri_prefix
- body = self.serialize(kwargs)
- resp, body = self.post(uri, body)
- self.expected_success(201, resp.status)
- body = self.deserialize(body)
- return service_client.ResponseBody(resp, body)
-
- def update_alarm(self, alarm_id, **kwargs):
- uri = "%s/alarms/%s" % (self.uri_prefix, alarm_id)
- body = self.serialize(kwargs)
- resp, body = self.put(uri, body)
- self.expected_success(200, resp.status)
- body = self.deserialize(body)
- return service_client.ResponseBody(resp, body)
-
- def show_alarm_state(self, alarm_id):
- uri = "%s/alarms/%s/state" % (self.uri_prefix, alarm_id)
- resp, body = self.get(uri)
- self.expected_success(200, resp.status)
- body = self.deserialize(body)
- return service_client.ResponseBodyData(resp, body)
-
- def alarm_set_state(self, alarm_id, state):
- uri = "%s/alarms/%s/state" % (self.uri_prefix, alarm_id)
- body = self.serialize(state)
- resp, body = self.put(uri, body)
- self.expected_success(200, resp.status)
- body = self.deserialize(body)
- return service_client.ResponseBodyData(resp, body)
diff --git a/tempest/services/telemetry/json/telemetry_client.py b/tempest/services/telemetry/json/telemetry_client.py
deleted file mode 100644
index abdeba2..0000000
--- a/tempest/services/telemetry/json/telemetry_client.py
+++ /dev/null
@@ -1,81 +0,0 @@
-# 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.common import service_client
-
-
-class TelemetryClient(service_client.ServiceClient):
-
- version = '2'
- uri_prefix = "v2"
-
- def deserialize(self, body):
- return json.loads(body.replace("\n", ""))
-
- def serialize(self, body):
- return json.dumps(body)
-
- def create_sample(self, meter_name, sample_list):
- uri = "%s/meters/%s" % (self.uri_prefix, meter_name)
- body = self.serialize(sample_list)
- resp, body = self.post(uri, body)
- self.expected_success(200, resp.status)
- body = self.deserialize(body)
- return service_client.ResponseBody(resp, body)
-
- def _helper_list(self, uri, query=None, period=None):
- uri_dict = {}
- if query:
- uri_dict = {'q.field': query[0],
- 'q.op': query[1],
- 'q.value': query[2]}
- if period:
- uri_dict['period'] = period
- if uri_dict:
- uri += "?%s" % urllib.urlencode(uri_dict)
- resp, body = self.get(uri)
- self.expected_success(200, resp.status)
- body = self.deserialize(body)
- return service_client.ResponseBodyList(resp, body)
-
- def list_resources(self, query=None):
- uri = '%s/resources' % self.uri_prefix
- return self._helper_list(uri, query)
-
- def list_meters(self, query=None):
- uri = '%s/meters' % self.uri_prefix
- return self._helper_list(uri, query)
-
- def list_statistics(self, meter, period=None, query=None):
- uri = "%s/meters/%s/statistics" % (self.uri_prefix, meter)
- return self._helper_list(uri, query, period)
-
- def list_samples(self, meter_id, query=None):
- uri = '%s/meters/%s' % (self.uri_prefix, meter_id)
- return self._helper_list(uri, query)
-
- def list_events(self, query=None):
- uri = '%s/events' % self.uri_prefix
- return self._helper_list(uri, query)
-
- def show_resource(self, resource_id):
- uri = '%s/resources/%s' % (self.uri_prefix, resource_id)
- resp, body = self.get(uri)
- self.expected_success(200, resp.status)
- body = self.deserialize(body)
- return service_client.ResponseBody(resp, body)
diff --git a/tempest/services/volume/base/__init__.py b/tempest/services/volume/base/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/services/volume/base/__init__.py
+++ /dev/null
diff --git a/tempest/services/volume/base/admin/__init__.py b/tempest/services/volume/base/admin/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/services/volume/base/admin/__init__.py
+++ /dev/null
diff --git a/tempest/services/volume/base/admin/base_hosts_client.py b/tempest/services/volume/base/admin/base_hosts_client.py
deleted file mode 100644
index 074f87f..0000000
--- a/tempest/services/volume/base/admin/base_hosts_client.py
+++ /dev/null
@@ -1,35 +0,0 @@
-# Copyright 2013 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.common import service_client
-
-
-class BaseHostsClient(service_client.ServiceClient):
- """Client class to send CRUD Volume Hosts API requests"""
-
- def list_hosts(self, **params):
- """Lists 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 service_client.ResponseBody(resp, body)
diff --git a/tempest/services/volume/base/admin/base_quotas_client.py b/tempest/services/volume/base/admin/base_quotas_client.py
deleted file mode 100644
index e063a31..0000000
--- a/tempest/services/volume/base/admin/base_quotas_client.py
+++ /dev/null
@@ -1,69 +0,0 @@
-# Copyright (C) 2014 eNovance SAS <licensing@enovance.com>
-#
-# 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.common import service_client
-
-
-class BaseQuotasClient(service_client.ServiceClient):
- """Client class to send CRUD Volume Quotas API requests"""
-
- TYPE = "json"
-
- 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 service_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 service_client.ResponseBody(resp, body)
-
- def show_quota_usage(self, tenant_id):
- """List the quota set for a tenant."""
-
- body = self.show_quota_set(tenant_id, params={'usage': True})
- return body
-
- def update_quota_set(self, tenant_id, **kwargs):
- """Updates quota set
-
- Available params: see http://developer.openstack.org/
- api-ref-blockstorage-v2.html#updateQuotas-v2
- """
- 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 service_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 service_client.ResponseBody(resp, body)
diff --git a/tempest/services/volume/base/admin/base_services_client.py b/tempest/services/volume/base/admin/base_services_client.py
deleted file mode 100644
index 3626469..0000000
--- a/tempest/services/volume/base/admin/base_services_client.py
+++ /dev/null
@@ -1,32 +0,0 @@
-# Copyright 2014 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 oslo_serialization import jsonutils as json
-from six.moves.urllib import parse as urllib
-
-from tempest.common import service_client
-
-
-class BaseServicesClient(service_client.ServiceClient):
-
- 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 service_client.ResponseBody(resp, body)
diff --git a/tempest/services/volume/base/admin/base_types_client.py b/tempest/services/volume/base/admin/base_types_client.py
deleted file mode 100644
index 867273e..0000000
--- a/tempest/services/volume/base/admin/base_types_client.py
+++ /dev/null
@@ -1,181 +0,0 @@
-# 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 import exceptions as lib_exc
-
-from tempest.common import service_client
-
-
-class BaseTypesClient(service_client.ServiceClient):
- """Client class to send CRUD Volume Types API requests"""
-
- def is_resource_deleted(self, resource):
- # to use this method self.resource must be defined to respective value
- # Resource is a dictionary containing resource id and type
- # Resource : {"id" : resource_id
- # "type": resource_type}
- try:
- if resource['type'] == "volume-type":
- self.show_volume_type(resource['id'])
- elif resource['type'] == "encryption-type":
- body = self.show_encryption_type(resource['id'])
- if not body:
- return True
- else:
- msg = (" resource value is either not defined or incorrect.")
- raise lib_exc.UnprocessableEntity(msg)
- 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/encryption-type'
-
- def list_volume_types(self, **params):
- """List all the volume_types created."""
- 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 service_client.ResponseBody(resp, body)
-
- def show_volume_type(self, volume_id):
- """Returns the details of a single volume_type."""
- url = "types/%s" % str(volume_id)
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def create_volume_type(self, **kwargs):
- """Create volume type.
-
- Available params: see http://developer.openstack.org/
- api-ref-blockstorage-v2.html#createVolumeType
- """
- 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 service_client.ResponseBody(resp, body)
-
- def delete_volume_type(self, volume_id):
- """Deletes the Specified Volume_type."""
- resp, body = self.delete("types/%s" % str(volume_id))
- self.expected_success(202, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def list_volume_types_extra_specs(self, vol_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' % str(vol_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 service_client.ResponseBody(resp, body)
-
- def show_volume_type_extra_specs(self, vol_type_id, extra_specs_name):
- """Returns the details of a single volume_type extra spec."""
- url = "types/%s/extra_specs/%s" % (str(vol_type_id),
- str(extra_specs_name))
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def create_volume_type_extra_specs(self, vol_type_id, extra_specs):
- """Creates a new Volume_type extra spec.
-
- vol_type_id: Id of volume_type.
- extra_specs: A dictionary of values to be used as extra_specs.
- """
- url = "types/%s/extra_specs" % str(vol_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 service_client.ResponseBody(resp, body)
-
- def delete_volume_type_extra_specs(self, vol_id, extra_spec_name):
- """Deletes the Specified Volume_type extra spec."""
- resp, body = self.delete("types/%s/extra_specs/%s" % (
- (str(vol_id)), str(extra_spec_name)))
- self.expected_success(202, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def update_volume_type_extra_specs(self, vol_type_id, extra_spec_name,
- extra_specs):
- """Update a volume_type extra spec.
-
- vol_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.
- """
- url = "types/%s/extra_specs/%s" % (str(vol_type_id),
- str(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 service_client.ResponseBody(resp, body)
-
- def show_encryption_type(self, vol_type_id):
- """Get the volume encryption type for the specified volume type.
-
- vol_type_id: Id of volume_type.
- """
- url = "/types/%s/encryption" % str(vol_type_id)
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def create_encryption_type(self, vol_type_id, **kwargs):
- """Create encryption type.
-
- 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/encryption" % str(vol_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 service_client.ResponseBody(resp, body)
-
- def delete_encryption_type(self, vol_type_id):
- """Delete the encryption type for the specified volume-type."""
- resp, body = self.delete(
- "/types/%s/encryption/provider" % str(vol_type_id))
- self.expected_success(202, resp.status)
- return service_client.ResponseBody(resp, body)
diff --git a/tempest/services/volume/base/base_availability_zone_client.py b/tempest/services/volume/base/base_availability_zone_client.py
deleted file mode 100644
index b63fdc2..0000000
--- a/tempest/services/volume/base/base_availability_zone_client.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# Copyright 2014 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 oslo_serialization import jsonutils as json
-
-from tempest.common import service_client
-
-
-class BaseAvailabilityZoneClient(service_client.ServiceClient):
-
- def list_availability_zones(self):
- resp, body = self.get('os-availability-zone')
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return service_client.ResponseBody(resp, body)
diff --git a/tempest/services/volume/base/base_backups_client.py b/tempest/services/volume/base/base_backups_client.py
deleted file mode 100644
index fc9a40a..0000000
--- a/tempest/services/volume/base/base_backups_client.py
+++ /dev/null
@@ -1,114 +0,0 @@
-# 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.
-
-import time
-
-from oslo_serialization import jsonutils as json
-from tempest_lib import exceptions as lib_exc
-
-from tempest.common import service_client
-from tempest import exceptions
-
-
-class BaseBackupsClient(service_client.ServiceClient):
- """Client class to send CRUD Volume backup API requests"""
-
- def create_backup(self, **kwargs):
- """Creates a backup of volume."""
- post_body = json.dumps({'backup': kwargs})
- resp, body = self.post('backups', post_body)
- body = json.loads(body)
- self.expected_success(202, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def restore_backup(self, backup_id, **kwargs):
- """Restore volume from 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 service_client.ResponseBody(resp, body)
-
- def delete_backup(self, backup_id):
- """Delete a backup of volume."""
- resp, body = self.delete('backups/%s' % (str(backup_id)))
- self.expected_success(202, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def show_backup(self, backup_id):
- """Returns the details of a single backup."""
- url = "backups/%s" % str(backup_id)
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def list_backups(self, detail=False):
- """Information for all the tenant's backups."""
- url = "backups"
- if detail:
- url += "/detail"
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return service_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 service_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 service_client.ResponseBody(resp, body)
-
- def wait_for_backup_status(self, backup_id, status):
- """Waits for a Backup to reach a given status."""
- body = self.show_backup(backup_id)['backup']
- backup_status = body['status']
- start = int(time.time())
-
- while backup_status != status:
- time.sleep(self.build_interval)
- body = self.show_backup(backup_id)['backup']
- backup_status = body['status']
- if backup_status == 'error':
- raise exceptions.VolumeBackupException(backup_id=backup_id)
-
- if int(time.time()) - start >= self.build_timeout:
- message = ('Volume backup %s failed to reach %s status '
- '(current %s) within the required time (%s s).' %
- (backup_id, status, backup_status,
- self.build_timeout))
- raise exceptions.TimeoutException(message)
-
- def wait_for_backup_deletion(self, backup_id):
- """Waits for backup deletion"""
- start_time = int(time.time())
- while True:
- try:
- self.show_backup(backup_id)
- except lib_exc.NotFound:
- return
- if int(time.time()) - start_time >= self.build_timeout:
- raise exceptions.TimeoutException
- time.sleep(self.build_interval)
diff --git a/tempest/services/volume/base/base_extensions_client.py b/tempest/services/volume/base/base_extensions_client.py
deleted file mode 100644
index afc3f6b..0000000
--- a/tempest/services/volume/base/base_extensions_client.py
+++ /dev/null
@@ -1,28 +0,0 @@
-# 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.common import service_client
-
-
-class BaseExtensionsClient(service_client.ServiceClient):
-
- def list_extensions(self):
- url = 'extensions'
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return service_client.ResponseBody(resp, body)
diff --git a/tempest/services/volume/base/base_qos_client.py b/tempest/services/volume/base/base_qos_client.py
deleted file mode 100644
index 697e902..0000000
--- a/tempest/services/volume/base/base_qos_client.py
+++ /dev/null
@@ -1,158 +0,0 @@
-# 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 time
-
-from oslo_serialization import jsonutils as json
-from tempest_lib import exceptions as lib_exc
-
-from tempest.common import service_client
-from tempest import exceptions
-
-
-class BaseQosSpecsClient(service_client.ServiceClient):
- """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 wait_for_qos_operations(self, qos_id, operation, args=None):
- """Waits for a qos operations to be completed.
-
- NOTE : operation value is required for wait_for_qos_operations()
- operation = 'qos-key' / 'disassociate' / 'disassociate-all'
- args = keys[] when operation = 'qos-key'
- args = volume-type-id disassociated when operation = 'disassociate'
- args = None when operation = 'disassociate-all'
- """
- start_time = int(time.time())
- while True:
- if operation == 'qos-key-unset':
- body = self.show_qos(qos_id)['qos_specs']
- if not any(key in body['specs'] for key in args):
- return
- elif operation == 'disassociate':
- body = self.show_association_qos(qos_id)['qos_associations']
- if not any(args in body[i]['id'] for i in range(0, len(body))):
- return
- elif operation == 'disassociate-all':
- body = self.show_association_qos(qos_id)['qos_associations']
- if not body:
- return
- else:
- msg = (" operation value is either not defined or incorrect.")
- raise lib_exc.UnprocessableEntity(msg)
-
- if int(time.time()) - start_time >= self.build_timeout:
- raise exceptions.TimeoutException
- time.sleep(self.build_interval)
-
- def create_qos(self, **kwargs):
- """Create a QoS Specification.
-
- Available params: see http://developer.openstack.org/
- api-ref-blockstorage-v2.html#createQoSSpec
- """
- 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 service_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" % (str(qos_id), force))
- self.expected_success(202, resp.status)
- return service_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 service_client.ResponseBody(resp, body)
-
- def show_qos(self, qos_id):
- """Get the specified QoS specification."""
- url = "qos-specs/%s" % str(qos_id)
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def set_qos_key(self, qos_id, **kwargs):
- """Set the specified keys/values of QoS specification.
-
- Available params: see http://developer.openstack.org/
- api-ref-blockstorage-v2.html#setQoSKey
- """
- 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 service_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.
-
- TODO(jordanP): Add a link once LP #1524877 is fixed.
- """
- 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 service_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" % str(qos_id)
- url += "?vol_type_id=%s" % vol_type_id
- resp, body = self.get(url)
- self.expected_success(202, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def show_association_qos(self, qos_id):
- """Get the association of the specified QoS specification."""
- url = "qos-specs/%s/associations" % str(qos_id)
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return service_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" % str(qos_id)
- url += "?vol_type_id=%s" % vol_type_id
- resp, body = self.get(url)
- self.expected_success(202, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def disassociate_all_qos(self, qos_id):
- """Disassociate the specified QoS with all associations."""
- url = "qos-specs/%s/disassociate_all" % str(qos_id)
- resp, body = self.get(url)
- self.expected_success(202, resp.status)
- return service_client.ResponseBody(resp, body)
diff --git a/tempest/services/volume/base/base_snapshots_client.py b/tempest/services/volume/base/base_snapshots_client.py
deleted file mode 100644
index fac90e4..0000000
--- a/tempest/services/volume/base/base_snapshots_client.py
+++ /dev/null
@@ -1,198 +0,0 @@
-# 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 time
-
-from oslo_log import log as logging
-from oslo_serialization import jsonutils as json
-from six.moves.urllib import parse as urllib
-from tempest_lib import exceptions as lib_exc
-
-from tempest.common import service_client
-from tempest import exceptions
-
-
-LOG = logging.getLogger(__name__)
-
-
-class BaseSnapshotsClient(service_client.ServiceClient):
- """Base Client class to send CRUD Volume API requests."""
-
- create_resp = 200
-
- def list_snapshots(self, detail=False, params=None):
- """List all the snapshot."""
- 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 service_client.ResponseBody(resp, body)
-
- def show_snapshot(self, snapshot_id):
- """Returns the details of a single snapshot."""
- url = "snapshots/%s" % str(snapshot_id)
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def create_snapshot(self, volume_id, **kwargs):
- """Creates a new snapshot.
-
- volume_id(Required): id of the volume.
- force: Create a snapshot even if the volume attached (Default=False)
- display_name: Optional snapshot Name.
- display_description: User friendly snapshot description.
- """
- post_body = {'volume_id': volume_id}
- post_body.update(kwargs)
- post_body = json.dumps({'snapshot': post_body})
- resp, body = self.post('snapshots', post_body)
- body = json.loads(body)
- self.expected_success(self.create_resp, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def update_snapshot(self, snapshot_id, **kwargs):
- """Updates 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 service_client.ResponseBody(resp, body)
-
- # NOTE(afazekas): just for the wait function
- def _get_snapshot_status(self, snapshot_id):
- body = self.show_snapshot(snapshot_id)['snapshot']
- status = body['status']
- # NOTE(afazekas): snapshot can reach an "error"
- # state in a "normal" lifecycle
- if (status == 'error'):
- raise exceptions.SnapshotBuildErrorException(
- snapshot_id=snapshot_id)
-
- return status
-
- # NOTE(afazkas): Wait reinvented again. It is not in the correct layer
- def wait_for_snapshot_status(self, snapshot_id, status):
- """Waits for a Snapshot to reach a given status."""
- start_time = time.time()
- old_value = value = self._get_snapshot_status(snapshot_id)
- while True:
- dtime = time.time() - start_time
- time.sleep(self.build_interval)
- if value != old_value:
- LOG.info('Value transition from "%s" to "%s"'
- 'in %d second(s).', old_value,
- value, dtime)
- if (value == status):
- return value
-
- if dtime > self.build_timeout:
- message = ('Time Limit Exceeded! (%ds)'
- 'while waiting for %s, '
- 'but we got %s.' %
- (self.build_timeout, status, value))
- raise exceptions.TimeoutException(message)
- time.sleep(self.build_interval)
- old_value = value
- value = self._get_snapshot_status(snapshot_id)
-
- def delete_snapshot(self, snapshot_id):
- """Delete Snapshot."""
- resp, body = self.delete("snapshots/%s" % str(snapshot_id))
- self.expected_success(202, resp.status)
- return service_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 service_client.ResponseBody(resp, body)
-
- def update_snapshot_status(self, snapshot_id, status, progress):
- """Update the specified snapshot's status."""
- post_body = {
- 'status': status,
- 'progress': progress
- }
- post_body = json.dumps({'os-update_snapshot_status': post_body})
- url = 'snapshots/%s/action' % str(snapshot_id)
- resp, body = self.post(url, post_body)
- self.expected_success(202, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def create_snapshot_metadata(self, snapshot_id, metadata):
- """Create metadata for the snapshot."""
- put_body = json.dumps({'metadata': metadata})
- url = "snapshots/%s/metadata" % str(snapshot_id)
- resp, body = self.post(url, put_body)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def show_snapshot_metadata(self, snapshot_id):
- """Get metadata of the snapshot."""
- url = "snapshots/%s/metadata" % str(snapshot_id)
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def update_snapshot_metadata(self, snapshot_id, metadata):
- """Update metadata for the snapshot."""
- put_body = json.dumps({'metadata': metadata})
- url = "snapshots/%s/metadata" % str(snapshot_id)
- resp, body = self.put(url, put_body)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def update_snapshot_metadata_item(self, snapshot_id, id, meta_item):
- """Update metadata item for the snapshot."""
- put_body = json.dumps({'meta': meta_item})
- url = "snapshots/%s/metadata/%s" % (str(snapshot_id), str(id))
- resp, body = self.put(url, put_body)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def delete_snapshot_metadata_item(self, snapshot_id, id):
- """Delete metadata item for the snapshot."""
- url = "snapshots/%s/metadata/%s" % (str(snapshot_id), str(id))
- resp, body = self.delete(url)
- self.expected_success(200, resp.status)
- return service_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 service_client.ResponseBody(resp, body)
diff --git a/tempest/services/volume/base/base_volumes_client.py b/tempest/services/volume/base/base_volumes_client.py
deleted file mode 100644
index c7302e8..0000000
--- a/tempest/services/volume/base/base_volumes_client.py
+++ /dev/null
@@ -1,336 +0,0 @@
-# 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
-import six
-from six.moves.urllib import parse as urllib
-from tempest_lib import exceptions as lib_exc
-
-from tempest.common import service_client
-from tempest.common import waiters
-
-
-class BaseVolumesClient(service_client.ServiceClient):
- """Base client class to send CRUD Volume API requests"""
-
- create_resp = 200
-
- def __init__(self, auth_provider, service, region,
- default_volume_size=1, **kwargs):
- super(BaseVolumesClient, self).__init__(
- auth_provider, service, region, **kwargs)
- self.default_volume_size = default_volume_size
-
- def get_attachment_from_volume(self, volume):
- """Return the element 'attachment' from input volumes."""
- return volume['attachments'][0]
-
- 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.
- """
- 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 service_client.ResponseBody(resp, body)
-
- def show_volume(self, volume_id):
- """Returns the details of a single volume."""
- url = "volumes/%s" % str(volume_id)
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def create_volume(self, size=None, **kwargs):
- """Creates a new Volume.
-
- size: Size of volume in GB.
- Following optional keyword arguments are accepted:
- display_name: Optional Volume Name(only for V1).
- name: Optional Volume Name(only for V2).
- metadata: A dictionary of values to be used as metadata.
- volume_type: Optional Name of volume_type for the volume
- snapshot_id: When specified the volume is created from this snapshot
- imageRef: When specified the volume is created from this image
- """
- if size is None:
- size = self.default_volume_size
- post_body = {'size': size}
- post_body.update(kwargs)
- post_body = json.dumps({'volume': post_body})
- resp, body = self.post('volumes', post_body)
- body = json.loads(body)
- self.expected_success(self.create_resp, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def update_volume(self, volume_id, **kwargs):
- """Updates the Specified 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 service_client.ResponseBody(resp, body)
-
- def delete_volume(self, volume_id):
- """Deletes the Specified Volume."""
- resp, body = self.delete("volumes/%s" % str(volume_id))
- self.expected_success(202, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def upload_volume(self, volume_id, image_name, disk_format):
- """Uploads a volume in Glance."""
- post_body = {
- 'image_name': image_name,
- 'disk_format': disk_format
- }
- post_body = json.dumps({'os-volume_upload_image': post_body})
- url = 'volumes/%s/action' % (volume_id)
- resp, body = self.post(url, post_body)
- body = json.loads(body)
- self.expected_success(202, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def attach_volume(self, volume_id, instance_uuid, mountpoint):
- """Attaches a volume to a given instance on a given mountpoint."""
- post_body = {
- 'instance_uuid': instance_uuid,
- 'mountpoint': mountpoint,
- }
- post_body = json.dumps({'os-attach': post_body})
- url = 'volumes/%s/action' % (volume_id)
- resp, body = self.post(url, post_body)
- self.expected_success(202, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def set_bootable_volume(self, volume_id, bootable):
- """set a bootable flag for a volume - true or false."""
- post_body = {"bootable": bootable}
- post_body = json.dumps({'os-set_bootable': post_body})
- url = 'volumes/%s/action' % (volume_id)
- resp, body = self.post(url, post_body)
- self.expected_success(200, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def detach_volume(self, volume_id):
- """Detaches a volume from an instance."""
- post_body = {}
- post_body = json.dumps({'os-detach': post_body})
- url = 'volumes/%s/action' % (volume_id)
- resp, body = self.post(url, post_body)
- self.expected_success(202, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def reserve_volume(self, volume_id):
- """Reserves a volume."""
- post_body = {}
- post_body = json.dumps({'os-reserve': post_body})
- url = 'volumes/%s/action' % (volume_id)
- resp, body = self.post(url, post_body)
- self.expected_success(202, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def unreserve_volume(self, volume_id):
- """Restore a reserved volume ."""
- post_body = {}
- post_body = json.dumps({'os-unreserve': post_body})
- url = 'volumes/%s/action' % (volume_id)
- resp, body = self.post(url, post_body)
- self.expected_success(202, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def wait_for_volume_status(self, volume_id, status):
- """Waits for a Volume to reach a given status."""
- waiters.wait_for_volume_status(self, volume_id, status)
-
- def is_resource_deleted(self, id):
- try:
- self.show_volume(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'
-
- def extend_volume(self, volume_id, extend_size):
- """Extend a volume."""
- post_body = {
- 'new_size': extend_size
- }
- post_body = json.dumps({'os-extend': post_body})
- url = 'volumes/%s/action' % (volume_id)
- resp, body = self.post(url, post_body)
- self.expected_success(202, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def reset_volume_status(self, volume_id, status):
- """Reset the Specified Volume's Status."""
- post_body = json.dumps({'os-reset_status': {"status": status}})
- resp, body = self.post('volumes/%s/action' % volume_id, post_body)
- self.expected_success(202, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def volume_begin_detaching(self, volume_id):
- """Volume Begin Detaching."""
- # ref cinder/api/contrib/volume_actions.py#L158
- post_body = json.dumps({'os-begin_detaching': {}})
- resp, body = self.post('volumes/%s/action' % volume_id, post_body)
- self.expected_success(202, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def volume_roll_detaching(self, volume_id):
- """Volume Roll Detaching."""
- # cinder/api/contrib/volume_actions.py#L170
- post_body = json.dumps({'os-roll_detaching': {}})
- resp, body = self.post('volumes/%s/action' % volume_id, post_body)
- self.expected_success(202, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def create_volume_transfer(self, vol_id, display_name=None):
- """Create a volume transfer."""
- post_body = {
- 'volume_id': vol_id
- }
- if display_name:
- post_body['name'] = display_name
- post_body = json.dumps({'transfer': post_body})
- resp, body = self.post('os-volume-transfer', post_body)
- body = json.loads(body)
- self.expected_success(202, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def show_volume_transfer(self, transfer_id):
- """Returns the details of a volume transfer."""
- url = "os-volume-transfer/%s" % str(transfer_id)
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def list_volume_transfers(self, params=None):
- """List all the volume transfers created."""
- url = 'os-volume-transfer'
- if params:
- url += '?%s' % urllib.urlencode(params)
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def delete_volume_transfer(self, transfer_id):
- """Delete a volume transfer."""
- resp, body = self.delete("os-volume-transfer/%s" % str(transfer_id))
- self.expected_success(202, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def accept_volume_transfer(self, transfer_id, transfer_auth_key):
- """Accept a volume transfer."""
- post_body = {
- 'auth_key': transfer_auth_key,
- }
- url = 'os-volume-transfer/%s/accept' % transfer_id
- post_body = json.dumps({'accept': post_body})
- resp, body = self.post(url, post_body)
- body = json.loads(body)
- self.expected_success(202, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def update_volume_readonly(self, volume_id, readonly):
- """Update the Specified Volume readonly."""
- post_body = {
- 'readonly': readonly
- }
- post_body = json.dumps({'os-update_readonly_flag': post_body})
- url = 'volumes/%s/action' % (volume_id)
- resp, body = self.post(url, post_body)
- self.expected_success(202, resp.status)
- return service_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 service_client.ResponseBody(resp, body)
-
- def create_volume_metadata(self, volume_id, metadata):
- """Create metadata for the volume."""
- put_body = json.dumps({'metadata': metadata})
- url = "volumes/%s/metadata" % str(volume_id)
- resp, body = self.post(url, put_body)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def show_volume_metadata(self, volume_id):
- """Get metadata of the volume."""
- url = "volumes/%s/metadata" % str(volume_id)
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def update_volume_metadata(self, volume_id, metadata):
- """Update metadata for the volume."""
- put_body = json.dumps({'metadata': metadata})
- url = "volumes/%s/metadata" % str(volume_id)
- resp, body = self.put(url, put_body)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return service_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" % (str(volume_id), str(id))
- resp, body = self.put(url, put_body)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def delete_volume_metadata_item(self, volume_id, id):
- """Delete metadata item for the volume."""
- url = "volumes/%s/metadata/%s" % (str(volume_id), str(id))
- resp, body = self.delete(url)
- self.expected_success(200, resp.status)
- return service_client.ResponseBody(resp, body)
-
- def retype_volume(self, volume_id, volume_type, **kwargs):
- """Updates volume with new volume type."""
- post_body = {'new_type': volume_type}
- post_body.update(kwargs)
- post_body = json.dumps({'os-retype': post_body})
- resp, body = self.post('volumes/%s/action' % volume_id, post_body)
- self.expected_success(202, resp.status)
diff --git a/tempest/services/volume/v1/json/__init__.py b/tempest/services/volume/v1/json/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/services/volume/v1/json/__init__.py
+++ /dev/null
diff --git a/tempest/services/volume/v1/json/admin/__init__.py b/tempest/services/volume/v1/json/admin/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/services/volume/v1/json/admin/__init__.py
+++ /dev/null
diff --git a/tempest/services/volume/v1/json/admin/hosts_client.py b/tempest/services/volume/v1/json/admin/hosts_client.py
deleted file mode 100644
index 3b52968..0000000
--- a/tempest/services/volume/v1/json/admin/hosts_client.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# Copyright 2013 OpenStack Foundation
-# All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-from tempest.services.volume.base.admin import base_hosts_client
-
-
-class HostsClient(base_hosts_client.BaseHostsClient):
- """Client class to send CRUD Volume Host API V1 requests"""
diff --git a/tempest/services/volume/v1/json/admin/quotas_client.py b/tempest/services/volume/v1/json/admin/quotas_client.py
deleted file mode 100644
index 27fc301..0000000
--- a/tempest/services/volume/v1/json/admin/quotas_client.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright (C) 2014 eNovance SAS <licensing@enovance.com>
-#
-# 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.services.volume.base.admin import base_quotas_client
-
-
-class QuotasClient(base_quotas_client.BaseQuotasClient):
- """Client class to send CRUD Volume Type API V1 requests"""
diff --git a/tempest/services/volume/v1/json/admin/services_client.py b/tempest/services/volume/v1/json/admin/services_client.py
deleted file mode 100644
index 2bffd55..0000000
--- a/tempest/services/volume/v1/json/admin/services_client.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# Copyright 2014 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.services.volume.base.admin import base_services_client
-
-
-class ServicesClient(base_services_client.BaseServicesClient):
- """Volume V1 volume services client"""
diff --git a/tempest/services/volume/v1/json/admin/types_client.py b/tempest/services/volume/v1/json/admin/types_client.py
deleted file mode 100644
index 0e84296..0000000
--- a/tempest/services/volume/v1/json/admin/types_client.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# 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 tempest.services.volume.base.admin import base_types_client
-
-
-class TypesClient(base_types_client.BaseTypesClient):
- """Volume V1 Volume Types client"""
diff --git a/tempest/services/volume/v1/json/availability_zone_client.py b/tempest/services/volume/v1/json/availability_zone_client.py
deleted file mode 100644
index 3a27027..0000000
--- a/tempest/services/volume/v1/json/availability_zone_client.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# Copyright 2014 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.services.volume.base import base_availability_zone_client
-
-
-class AvailabilityZoneClient(
- base_availability_zone_client.BaseAvailabilityZoneClient):
- """Volume V1 availability zone client."""
diff --git a/tempest/services/volume/v1/json/backups_client.py b/tempest/services/volume/v1/json/backups_client.py
deleted file mode 100644
index ac6db6a..0000000
--- a/tempest/services/volume/v1/json/backups_client.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# 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 tempest.services.volume.base import base_backups_client
-
-
-class BackupsClient(base_backups_client.BaseBackupsClient):
- """Volume V1 Backups client"""
diff --git a/tempest/services/volume/v1/json/extensions_client.py b/tempest/services/volume/v1/json/extensions_client.py
deleted file mode 100644
index f99d0f5..0000000
--- a/tempest/services/volume/v1/json/extensions_client.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# 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 tempest.services.volume.base import base_extensions_client
-
-
-class ExtensionsClient(base_extensions_client.BaseExtensionsClient):
- """Volume V1 extensions client."""
diff --git a/tempest/services/volume/v1/json/qos_client.py b/tempest/services/volume/v1/json/qos_client.py
deleted file mode 100644
index b2b2195..0000000
--- a/tempest/services/volume/v1/json/qos_client.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# 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.services.volume.base import base_qos_client
-
-
-class QosSpecsClient(base_qos_client.BaseQosSpecsClient):
- """Volume V1 QoS client."""
diff --git a/tempest/services/volume/v1/json/snapshots_client.py b/tempest/services/volume/v1/json/snapshots_client.py
deleted file mode 100644
index b039c2b..0000000
--- a/tempest/services/volume/v1/json/snapshots_client.py
+++ /dev/null
@@ -1,17 +0,0 @@
-# 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.services.volume.base import base_snapshots_client
-
-
-class SnapshotsClient(base_snapshots_client.BaseSnapshotsClient):
- """Client class to send CRUD Volume V1 API requests."""
diff --git a/tempest/services/volume/v1/json/volumes_client.py b/tempest/services/volume/v1/json/volumes_client.py
deleted file mode 100644
index 7782043..0000000
--- a/tempest/services/volume/v1/json/volumes_client.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# 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 tempest.services.volume.base import base_volumes_client
-
-
-class VolumesClient(base_volumes_client.BaseVolumesClient):
- """Client class to send CRUD Volume V1 API requests"""
diff --git a/tempest/services/volume/v2/json/__init__.py b/tempest/services/volume/v2/json/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/services/volume/v2/json/__init__.py
+++ /dev/null
diff --git a/tempest/services/volume/v2/json/admin/__init__.py b/tempest/services/volume/v2/json/admin/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/services/volume/v2/json/admin/__init__.py
+++ /dev/null
diff --git a/tempest/services/volume/v2/json/admin/hosts_client.py b/tempest/services/volume/v2/json/admin/hosts_client.py
deleted file mode 100644
index e092c6a..0000000
--- a/tempest/services/volume/v2/json/admin/hosts_client.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# 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 tempest.services.volume.base.admin import base_hosts_client
-
-
-class HostsClient(base_hosts_client.BaseHostsClient):
- """Client class to send CRUD Volume V2 API requests"""
- api_version = "v2"
diff --git a/tempest/services/volume/v2/json/admin/quotas_client.py b/tempest/services/volume/v2/json/admin/quotas_client.py
deleted file mode 100644
index 11e0e22..0000000
--- a/tempest/services/volume/v2/json/admin/quotas_client.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# 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 tempest.services.volume.base.admin import base_quotas_client
-
-
-class QuotasClient(base_quotas_client.BaseQuotasClient):
- """Client class to send CRUD Volume V2 API requests"""
- api_version = "v2"
diff --git a/tempest/services/volume/v2/json/admin/services_client.py b/tempest/services/volume/v2/json/admin/services_client.py
deleted file mode 100644
index db19ba9..0000000
--- a/tempest/services/volume/v2/json/admin/services_client.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# 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 tempest.services.volume.base.admin import base_services_client
-
-
-class ServicesClient(base_services_client.BaseServicesClient):
- """Client class to send CRUD Volume V2 API requests"""
- api_version = "v2"
diff --git a/tempest/services/volume/v2/json/admin/types_client.py b/tempest/services/volume/v2/json/admin/types_client.py
deleted file mode 100644
index ecf5131..0000000
--- a/tempest/services/volume/v2/json/admin/types_client.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# 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 tempest.services.volume.base.admin import base_types_client
-
-
-class TypesClient(base_types_client.BaseTypesClient):
- """Client class to send CRUD Volume V2 API requests"""
- api_version = "v2"
diff --git a/tempest/services/volume/v2/json/availability_zone_client.py b/tempest/services/volume/v2/json/availability_zone_client.py
deleted file mode 100644
index 905ebdc..0000000
--- a/tempest/services/volume/v2/json/availability_zone_client.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# 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 tempest.services.volume.base import base_availability_zone_client
-
-
-class AvailabilityZoneClient(
- base_availability_zone_client.BaseAvailabilityZoneClient):
- api_version = "v2"
diff --git a/tempest/services/volume/v2/json/backups_client.py b/tempest/services/volume/v2/json/backups_client.py
deleted file mode 100644
index 78bab82..0000000
--- a/tempest/services/volume/v2/json/backups_client.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# 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 tempest.services.volume.base import base_backups_client
-
-
-class BackupsClient(base_backups_client.BaseBackupsClient):
- """Client class to send CRUD Volume V2 API requests"""
- api_version = "v2"
diff --git a/tempest/services/volume/v2/json/extensions_client.py b/tempest/services/volume/v2/json/extensions_client.py
deleted file mode 100644
index 245906f..0000000
--- a/tempest/services/volume/v2/json/extensions_client.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# 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 tempest.services.volume.base import base_extensions_client
-
-
-class ExtensionsClient(base_extensions_client.BaseExtensionsClient):
- api_version = "v2"
diff --git a/tempest/services/volume/v2/json/qos_client.py b/tempest/services/volume/v2/json/qos_client.py
deleted file mode 100644
index 3c0f74f..0000000
--- a/tempest/services/volume/v2/json/qos_client.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# 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.services.volume.base import base_qos_client
-
-
-class QosSpecsClient(base_qos_client.BaseQosSpecsClient):
- api_version = "v2"
diff --git a/tempest/services/volume/v2/json/snapshots_client.py b/tempest/services/volume/v2/json/snapshots_client.py
deleted file mode 100644
index a2d415f..0000000
--- a/tempest/services/volume/v2/json/snapshots_client.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# 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.services.volume.base import base_snapshots_client
-
-
-class SnapshotsClient(base_snapshots_client.BaseSnapshotsClient):
- """Client class to send CRUD Volume V2 API requests."""
- api_version = "v2"
- create_resp = 202
diff --git a/tempest/services/volume/v2/json/volumes_client.py b/tempest/services/volume/v2/json/volumes_client.py
deleted file mode 100644
index b7d9dfb..0000000
--- a/tempest/services/volume/v2/json/volumes_client.py
+++ /dev/null
@@ -1,22 +0,0 @@
-# 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 tempest.services.volume.base import base_volumes_client
-
-
-class VolumesClient(base_volumes_client.BaseVolumesClient):
- """Client class to send CRUD Volume V2 API requests"""
- api_version = "v2"
- create_resp = 202
diff --git a/tempest/stress/README.rst b/tempest/stress/README.rst
deleted file mode 100644
index 33842fd..0000000
--- a/tempest/stress/README.rst
+++ /dev/null
@@ -1,60 +0,0 @@
-.. _stress_field_guide:
-
-Tempest Field Guide to Stress Tests
-===================================
-
-OpenStack is a distributed, asynchronous system that is prone to race condition
-bugs. These bugs will not be easily found during
-functional testing but will be encountered by users in large deployments in a
-way that is hard to debug. The stress test tries to cause these bugs to happen
-in a more controlled environment.
-
-
-Environment
------------
-This particular framework assumes your working Nova cluster understands Nova
-API 2.0. The stress tests can read the logs from the cluster. To enable this
-you have to provide the hostname to call 'nova-manage' and
-the private key and user name for ssh to the cluster in the
-[stress] section of tempest.conf. You also need to provide the
-location of the log files:
-
- target_logfiles = "regexp to all log files to be checked for errors"
- target_private_key_path = "private ssh key for controller and log file nodes"
- target_ssh_user = "username for controller and log file nodes"
- target_controller = "hostname or ip of controller node (for nova-manage)
- log_check_interval = "time between checking logs for errors (default 60s)"
-
-To activate logging on your console please make sure that you activate `use_stderr`
-in tempest.conf or use the default `logging.conf.sample` file.
-
-Running default stress test set
--------------------------------
-
-The stress test framework can automatically discover test inside the tempest
-test suite. All test flag with the `@stresstest` decorator will be executed.
-In order to use this discovery you have to install tempest CLI, be in the
-tempest root directory and execute the following:
-
- tempest run-stress -a -d 30
-
-Running the sample test
------------------------
-
-To test installation, do the following:
-
- tempest run-stress -t tempest/stress/etc/server-create-destroy-test.json -d 30
-
-This sample test tries to create a few VMs and kill a few VMs.
-
-
-Additional Tools
-----------------
-
-Sometimes the tests don't finish, or there are failures. In these
-cases, you may want to clean out the nova cluster. We have provided
-some scripts to do this in the ``tools`` subdirectory.
-You can use the following script to destroy any keypairs,
-floating ips, and servers:
-
-tempest/stress/tools/cleanup.py
diff --git a/tempest/stress/actions/__init__.py b/tempest/stress/actions/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/stress/actions/__init__.py
+++ /dev/null
diff --git a/tempest/stress/actions/server_create_destroy.py b/tempest/stress/actions/server_create_destroy.py
deleted file mode 100644
index 44b6f62..0000000
--- a/tempest/stress/actions/server_create_destroy.py
+++ /dev/null
@@ -1,42 +0,0 @@
-# Copyright 2013 Quanta Research Cambridge, Inc.
-#
-# 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.utils import data_utils
-from tempest.common import waiters
-from tempest import config
-import tempest.stress.stressaction as stressaction
-
-CONF = config.CONF
-
-
-class ServerCreateDestroyTest(stressaction.StressAction):
-
- def setUp(self, **kwargs):
- self.image = CONF.compute.image_ref
- self.flavor = CONF.compute.flavor_ref
-
- def run(self):
- name = data_utils.rand_name("instance")
- self.logger.info("creating %s" % name)
- server = self.manager.servers_client.create_server(
- name=name, imageRef=self.image, flavorRef=self.flavor)['server']
- server_id = server['id']
- waiters.wait_for_server_status(self.manager.servers_client, server_id,
- 'ACTIVE')
- self.logger.info("created %s" % server_id)
- self.logger.info("deleting %s" % name)
- self.manager.servers_client.delete_server(server_id)
- waiters.wait_for_server_termination(self.manager.servers_client,
- server_id)
- self.logger.info("deleted %s" % server_id)
diff --git a/tempest/stress/actions/ssh_floating.py b/tempest/stress/actions/ssh_floating.py
deleted file mode 100644
index 4f8c6bd..0000000
--- a/tempest/stress/actions/ssh_floating.py
+++ /dev/null
@@ -1,199 +0,0 @@
-# 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 socket
-import subprocess
-
-from tempest.common.utils import data_utils
-from tempest.common import waiters
-from tempest import config
-import tempest.stress.stressaction as stressaction
-import tempest.test
-
-CONF = config.CONF
-
-
-class FloatingStress(stressaction.StressAction):
-
- # from the scenario manager
- def ping_ip_address(self, ip_address):
- cmd = ['ping', '-c1', '-w1', ip_address]
-
- proc = subprocess.Popen(cmd,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE)
- proc.communicate()
- success = proc.returncode == 0
- return success
-
- def tcp_connect_scan(self, addr, port):
- # like tcp
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- try:
- s.connect((addr, port))
- except socket.error as exc:
- self.logger.info("%s(%s): %s", self.server_id, self.floating['ip'],
- str(exc))
- return False
- self.logger.info("%s(%s): Connected :)", self.server_id,
- self.floating['ip'])
- s.close()
- return True
-
- def check_port_ssh(self):
- def func():
- return self.tcp_connect_scan(self.floating['ip'], 22)
- if not tempest.test.call_until_true(func, self.check_timeout,
- self.check_interval):
- raise RuntimeError("Cannot connect to the ssh port.")
-
- def check_icmp_echo(self):
- self.logger.info("%s(%s): Pinging..",
- self.server_id, self.floating['ip'])
-
- def func():
- return self.ping_ip_address(self.floating['ip'])
- if not tempest.test.call_until_true(func, self.check_timeout,
- self.check_interval):
- raise RuntimeError("%s(%s): Cannot ping the machine.",
- self.server_id, self.floating['ip'])
- self.logger.info("%s(%s): pong :)",
- self.server_id, self.floating['ip'])
-
- def _create_vm(self):
- self.name = name = data_utils.rand_name("instance")
- servers_client = self.manager.servers_client
- self.logger.info("creating %s" % name)
- vm_args = self.vm_extra_args.copy()
- vm_args['security_groups'] = [self.sec_grp]
- server = servers_client.create_server(name=name, imageRef=self.image,
- flavorRef=self.flavor,
- **vm_args)['server']
- self.server_id = server['id']
- if self.wait_after_vm_create:
- waiters.wait_for_server_status(self.manager.servers_client,
- self.server_id, 'ACTIVE')
-
- def _destroy_vm(self):
- self.logger.info("deleting %s" % self.server_id)
- self.manager.servers_client.delete_server(self.server_id)
- waiters.wait_for_server_termination(self.manager.servers_client,
- self.server_id)
- self.logger.info("deleted %s" % self.server_id)
-
- def _create_sec_group(self):
- sec_grp_cli = self.manager.compute_security_groups_client
- s_name = data_utils.rand_name('sec_grp')
- s_description = data_utils.rand_name('desc')
- self.sec_grp = sec_grp_cli.create_security_group(
- name=s_name, description=s_description)['security_group']
- create_rule = sec_grp_cli.create_security_group_rule
- create_rule(parent_group_id=self.sec_grp['id'], ip_protocol='tcp',
- from_port=22, to_port=22)
- create_rule(parent_group_id=self.sec_grp['id'], ip_protocol='icmp',
- from_port=-1, to_port=-1)
-
- def _destroy_sec_grp(self):
- sec_grp_cli = self.manager.compute_security_groups_client
- sec_grp_cli.delete_security_group(self.sec_grp['id'])
-
- def _create_floating_ip(self):
- floating_cli = self.manager.compute_floating_ips_client
- self.floating = (floating_cli.create_floating_ip(self.floating_pool)
- ['floating_ip'])
-
- def _destroy_floating_ip(self):
- cli = self.manager.compute_floating_ips_client
- cli.delete_floating_ip(self.floating['id'])
- cli.wait_for_resource_deletion(self.floating['id'])
- self.logger.info("Deleted Floating IP %s", str(self.floating['ip']))
-
- def setUp(self, **kwargs):
- self.image = CONF.compute.image_ref
- self.flavor = CONF.compute.flavor_ref
- self.vm_extra_args = kwargs.get('vm_extra_args', {})
- self.wait_after_vm_create = kwargs.get('wait_after_vm_create',
- True)
- self.new_vm = kwargs.get('new_vm', False)
- self.new_sec_grp = kwargs.get('new_sec_group', False)
- self.new_floating = kwargs.get('new_floating', False)
- self.reboot = kwargs.get('reboot', False)
- self.floating_pool = kwargs.get('floating_pool', None)
- self.verify = kwargs.get('verify', ('check_port_ssh',
- 'check_icmp_echo'))
- self.check_timeout = kwargs.get('check_timeout', 120)
- self.check_interval = kwargs.get('check_interval', 1)
- self.wait_for_disassociate = kwargs.get('wait_for_disassociate',
- True)
-
- # allocate floating
- if not self.new_floating:
- self._create_floating_ip()
- # add security group
- if not self.new_sec_grp:
- self._create_sec_group()
- # create vm
- if not self.new_vm:
- self._create_vm()
-
- def wait_disassociate(self):
- cli = self.manager.compute_floating_ips_client
-
- def func():
- floating = (cli.show_floating_ip(self.floating['id'])
- ['floating_ip'])
- return floating['instance_id'] is None
-
- if not tempest.test.call_until_true(func, self.check_timeout,
- self.check_interval):
- raise RuntimeError("IP disassociate timeout!")
-
- def run_core(self):
- cli = self.manager.compute_floating_ips_client
- cli.associate_floating_ip_to_server(self.floating['ip'],
- self.server_id)
- for method in self.verify:
- m = getattr(self, method)
- m()
- cli.disassociate_floating_ip_from_server(self.floating['ip'],
- self.server_id)
- if self.wait_for_disassociate:
- self.wait_disassociate()
-
- def run(self):
- if self.new_sec_grp:
- self._create_sec_group()
- if self.new_floating:
- self._create_floating_ip()
- if self.new_vm:
- self._create_vm()
- if self.reboot:
- self.manager.servers_client.reboot(self.server_id, 'HARD')
- waiters.wait_for_server_status(self.manager.servers_client,
- self.server_id, 'ACTIVE')
-
- self.run_core()
-
- if self.new_vm:
- self._destroy_vm()
- if self.new_floating:
- self._destroy_floating_ip()
- if self.new_sec_grp:
- self._destroy_sec_grp()
-
- def tearDown(self):
- if not self.new_vm:
- self._destroy_vm()
- if not self.new_floating:
- self._destroy_floating_ip()
- if not self.new_sec_grp:
- self._destroy_sec_grp()
diff --git a/tempest/stress/actions/unit_test.py b/tempest/stress/actions/unit_test.py
deleted file mode 100644
index 3b27885..0000000
--- a/tempest/stress/actions/unit_test.py
+++ /dev/null
@@ -1,94 +0,0 @@
-# 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_log import log as logging
-from oslo_utils import importutils
-
-from tempest import config
-import tempest.stress.stressaction as stressaction
-
-CONF = config.CONF
-
-
-class SetUpClassRunTime(object):
-
- process = 'process'
- action = 'action'
- application = 'application'
-
- allowed = set((process, action, application))
-
- @classmethod
- def validate(cls, name):
- if name not in cls.allowed:
- raise KeyError("\'%s\' not a valid option" % name)
-
-
-class UnitTest(stressaction.StressAction):
- """This is a special action for running existing unittests as stress test.
-
- You need to pass ``test_method`` and ``class_setup_per``
- using ``kwargs`` in the JSON descriptor;
- ``test_method`` should be the fully qualified name of a unittest,
- ``class_setup_per`` should be one from:
- ``application``: once in the stress job lifetime
- ``process``: once in the worker process lifetime
- ``action``: on each action
- Not all combination working in every case.
- """
-
- def setUp(self, **kwargs):
- method = kwargs['test_method'].split('.')
- self.test_method = method.pop()
- self.klass = importutils.import_class('.'.join(method))
- self.logger = logging.getLogger('.'.join(method))
- # valid options are 'process', 'application' , 'action'
- self.class_setup_per = kwargs.get('class_setup_per',
- SetUpClassRunTime.process)
- SetUpClassRunTime.validate(self.class_setup_per)
-
- if self.class_setup_per == SetUpClassRunTime.application:
- self.klass.setUpClass()
- self.setupclass_called = False
-
- @property
- def action(self):
- if self.test_method:
- return self.test_method
- return super(UnitTest, self).action
-
- def run_core(self):
- res = self.klass(self.test_method).run()
- if res.errors:
- raise RuntimeError(res.errors)
-
- def run(self):
- if self.class_setup_per != SetUpClassRunTime.application:
- if (self.class_setup_per == SetUpClassRunTime.action
- or self.setupclass_called is False):
- self.klass.setUpClass()
- self.setupclass_called = True
-
- try:
- self.run_core()
- except Exception as e:
- raise e
- finally:
- if (CONF.stress.leave_dirty_stack is False
- and self.class_setup_per == SetUpClassRunTime.action):
- self.klass.tearDownClass()
- else:
- self.run_core()
-
- def tearDown(self):
- if self.class_setup_per != SetUpClassRunTime.action:
- self.klass.tearDownClass()
diff --git a/tempest/stress/actions/volume_attach_delete.py b/tempest/stress/actions/volume_attach_delete.py
deleted file mode 100644
index 847f342..0000000
--- a/tempest/stress/actions/volume_attach_delete.py
+++ /dev/null
@@ -1,70 +0,0 @@
-# (c) 2013 Deutsche Telekom AG
-# 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.utils import data_utils
-from tempest.common import waiters
-from tempest import config
-import tempest.stress.stressaction as stressaction
-
-CONF = config.CONF
-
-
-class VolumeAttachDeleteTest(stressaction.StressAction):
-
- def setUp(self, **kwargs):
- self.image = CONF.compute.image_ref
- self.flavor = CONF.compute.flavor_ref
-
- def run(self):
- # Step 1: create volume
- name = data_utils.rand_name("volume")
- self.logger.info("creating volume: %s" % name)
- volume = self.manager.volumes_client.create_volume(
- display_name=name)['volume']
- self.manager.volumes_client.wait_for_volume_status(volume['id'],
- 'available')
- self.logger.info("created volume: %s" % volume['id'])
-
- # Step 2: create vm instance
- vm_name = data_utils.rand_name("instance")
- self.logger.info("creating vm: %s" % vm_name)
- server = self.manager.servers_client.create_server(
- name=vm_name, imageRef=self.image, flavorRef=self.flavor)['server']
- server_id = server['id']
- waiters.wait_for_server_status(self.manager.servers_client, server_id,
- 'ACTIVE')
- self.logger.info("created vm %s" % server_id)
-
- # Step 3: attach volume to vm
- self.logger.info("attach volume (%s) to vm %s" %
- (volume['id'], server_id))
- self.manager.servers_client.attach_volume(server_id,
- volumeId=volume['id'],
- device='/dev/vdc')
- self.manager.volumes_client.wait_for_volume_status(volume['id'],
- 'in-use')
- self.logger.info("volume (%s) attached to vm %s" %
- (volume['id'], server_id))
-
- # Step 4: delete vm
- self.logger.info("deleting vm: %s" % vm_name)
- self.manager.servers_client.delete_server(server_id)
- waiters.wait_for_server_termination(self.manager.servers_client,
- server_id)
- self.logger.info("deleted vm: %s" % server_id)
-
- # Step 5: delete volume
- self.logger.info("deleting volume: %s" % volume['id'])
- self.manager.volumes_client.delete_volume(volume['id'])
- self.manager.volumes_client.wait_for_resource_deletion(volume['id'])
- self.logger.info("deleted volume: %s" % volume['id'])
diff --git a/tempest/stress/actions/volume_attach_verify.py b/tempest/stress/actions/volume_attach_verify.py
deleted file mode 100644
index 8bbbfc4..0000000
--- a/tempest/stress/actions/volume_attach_verify.py
+++ /dev/null
@@ -1,232 +0,0 @@
-# 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 re
-
-from tempest.common.utils import data_utils
-from tempest.common.utils.linux import remote_client
-from tempest.common import waiters
-from tempest import config
-import tempest.stress.stressaction as stressaction
-import tempest.test
-
-CONF = config.CONF
-
-
-class VolumeVerifyStress(stressaction.StressAction):
-
- def _create_keypair(self):
- keyname = data_utils.rand_name("key")
- self.key = (self.manager.keypairs_client.create_keypair(name=keyname)
- ['keypair'])
-
- def _delete_keypair(self):
- self.manager.keypairs_client.delete_keypair(self.key['name'])
-
- def _create_vm(self):
- self.name = name = data_utils.rand_name("instance")
- servers_client = self.manager.servers_client
- self.logger.info("creating %s" % name)
- vm_args = self.vm_extra_args.copy()
- vm_args['security_groups'] = [self.sec_grp]
- vm_args['key_name'] = self.key['name']
- server = servers_client.create_server(name=name, imageRef=self.image,
- flavorRef=self.flavor,
- **vm_args)['server']
- self.server_id = server['id']
- waiters.wait_for_server_status(self.manager.servers_client,
- self.server_id, 'ACTIVE')
-
- def _destroy_vm(self):
- self.logger.info("deleting server: %s" % self.server_id)
- self.manager.servers_client.delete_server(self.server_id)
- waiters.wait_for_server_termination(self.manager.servers_client,
- self.server_id)
- self.logger.info("deleted server: %s" % self.server_id)
-
- def _create_sec_group(self):
- sec_grp_cli = self.manager.compute_security_groups_client
- s_name = data_utils.rand_name('sec_grp')
- s_description = data_utils.rand_name('desc')
- self.sec_grp = sec_grp_cli.create_security_group(
- name=s_name, description=s_description)['security_group']
- create_rule = sec_grp_cli.create_security_group_rule
- create_rule(parent_group_id=self.sec_grp['id'], ip_protocol='tcp',
- from_port=22, to_port=22)
- create_rule(parent_group_id=self.sec_grp['id'], ip_protocol='icmp',
- from_port=-1, to_port=-1)
-
- def _destroy_sec_grp(self):
- sec_grp_cli = self.manager.compute_security_groups_client
- sec_grp_cli.delete_security_group(self.sec_grp['id'])
-
- def _create_floating_ip(self):
- floating_cli = self.manager.compute_floating_ips_client
- self.floating = (floating_cli.create_floating_ip(self.floating_pool)
- ['floating_ip'])
-
- def _destroy_floating_ip(self):
- cli = self.manager.compute_floating_ips_client
- cli.delete_floating_ip(self.floating['id'])
- cli.wait_for_resource_deletion(self.floating['id'])
- self.logger.info("Deleted Floating IP %s", str(self.floating['ip']))
-
- def _create_volume(self):
- name = data_utils.rand_name("volume")
- self.logger.info("creating volume: %s" % name)
- volumes_client = self.manager.volumes_client
- self.volume = volumes_client.create_volume(
- display_name=name)['volume']
- volumes_client.wait_for_volume_status(self.volume['id'],
- 'available')
- self.logger.info("created volume: %s" % self.volume['id'])
-
- def _delete_volume(self):
- self.logger.info("deleting volume: %s" % self.volume['id'])
- volumes_client = self.manager.volumes_client
- volumes_client.delete_volume(self.volume['id'])
- volumes_client.wait_for_resource_deletion(self.volume['id'])
- self.logger.info("deleted volume: %s" % self.volume['id'])
-
- def _wait_disassociate(self):
- cli = self.manager.compute_floating_ips_client
-
- def func():
- floating = (cli.show_floating_ip(self.floating['id'])
- ['floating_ip'])
- return floating['instance_id'] is None
-
- if not tempest.test.call_until_true(func, CONF.compute.build_timeout,
- CONF.compute.build_interval):
- raise RuntimeError("IP disassociate timeout!")
-
- def new_server_ops(self):
- self._create_vm()
- cli = self.manager.compute_floating_ips_client
- cli.associate_floating_ip_to_server(self.floating['ip'],
- self.server_id)
- if self.ssh_test_before_attach and self.enable_ssh_verify:
- self.logger.info("Scanning for block devices via ssh on %s"
- % self.server_id)
- self.part_wait(self.detach_match_count)
-
- def setUp(self, **kwargs):
- """Note able configuration combinations:
-
- Closest options to the test_stamp_pattern:
- new_server = True
- new_volume = True
- enable_ssh_verify = True
- ssh_test_before_attach = False
- Just attaching:
- new_server = False
- new_volume = False
- enable_ssh_verify = True
- ssh_test_before_attach = True
- Mostly API load by repeated attachment:
- new_server = False
- new_volume = False
- enable_ssh_verify = False
- ssh_test_before_attach = False
- Minimal Nova load, but cinder load not decreased:
- new_server = False
- new_volume = True
- enable_ssh_verify = True
- ssh_test_before_attach = True
- """
- self.image = CONF.compute.image_ref
- self.flavor = CONF.compute.flavor_ref
- self.vm_extra_args = kwargs.get('vm_extra_args', {})
- self.floating_pool = kwargs.get('floating_pool', None)
- self.new_volume = kwargs.get('new_volume', True)
- self.new_server = kwargs.get('new_server', False)
- self.enable_ssh_verify = kwargs.get('enable_ssh_verify', True)
- self.ssh_test_before_attach = kwargs.get('ssh_test_before_attach',
- False)
- self.part_line_re = re.compile(kwargs.get('part_line_re', '.*vd.*'))
- self.detach_match_count = kwargs.get('detach_match_count', 1)
- self.attach_match_count = kwargs.get('attach_match_count', 2)
- self.part_name = kwargs.get('part_name', '/dev/vdc')
-
- self._create_floating_ip()
- self._create_sec_group()
- self._create_keypair()
- private_key = self.key['private_key']
- username = CONF.validation.image_ssh_user
- self.remote_client = remote_client.RemoteClient(self.floating['ip'],
- username,
- pkey=private_key)
- if not self.new_volume:
- self._create_volume()
- if not self.new_server:
- self.new_server_ops()
-
- # now we just test that the number of partitions has increased or decreased
- def part_wait(self, num_match):
- def _part_state():
- self.partitions = self.remote_client.get_partitions().split('\n')
- matching = 0
- for part_line in self.partitions[1:]:
- if self.part_line_re.match(part_line):
- matching += 1
- return matching == num_match
- if tempest.test.call_until_true(_part_state,
- CONF.compute.build_timeout,
- CONF.compute.build_interval):
- return
- else:
- raise RuntimeError("Unexpected partitions: %s",
- str(self.partitions))
-
- def run(self):
- if self.new_server:
- self.new_server_ops()
- if self.new_volume:
- self._create_volume()
- servers_client = self.manager.servers_client
- self.logger.info("attach volume (%s) to vm %s" %
- (self.volume['id'], self.server_id))
- servers_client.attach_volume(self.server_id,
- volumeId=self.volume['id'],
- device=self.part_name)
- self.manager.volumes_client.wait_for_volume_status(self.volume['id'],
- 'in-use')
- if self.enable_ssh_verify:
- self.logger.info("Scanning for new block device on %s"
- % self.server_id)
- self.part_wait(self.attach_match_count)
-
- servers_client.detach_volume(self.server_id,
- self.volume['id'])
- self.manager.volumes_client.wait_for_volume_status(self.volume['id'],
- 'available')
- if self.enable_ssh_verify:
- self.logger.info("Scanning for block device disappearance on %s"
- % self.server_id)
- self.part_wait(self.detach_match_count)
- if self.new_volume:
- self._delete_volume()
- if self.new_server:
- self._destroy_vm()
-
- def tearDown(self):
- cli = self.manager.compute_floating_ips_client
- cli.disassociate_floating_ip_from_server(self.floating['ip'],
- self.server_id)
- self._wait_disassociate()
- if not self.new_server:
- self._destroy_vm()
- self._delete_keypair()
- self._destroy_floating_ip()
- self._destroy_sec_grp()
- if not self.new_volume:
- self._delete_volume()
diff --git a/tempest/stress/actions/volume_create_delete.py b/tempest/stress/actions/volume_create_delete.py
deleted file mode 100644
index 3986748..0000000
--- a/tempest/stress/actions/volume_create_delete.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# 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.utils import data_utils
-import tempest.stress.stressaction as stressaction
-
-
-class VolumeCreateDeleteTest(stressaction.StressAction):
-
- def run(self):
- name = data_utils.rand_name("volume")
- self.logger.info("creating %s" % name)
- volumes_client = self.manager.volumes_client
- volume = volumes_client.create_volume(display_name=name)['volume']
- vol_id = volume['id']
- volumes_client.wait_for_volume_status(vol_id, 'available')
- self.logger.info("created %s" % volume['id'])
- self.logger.info("deleting %s" % name)
- volumes_client.delete_volume(vol_id)
- volumes_client.wait_for_resource_deletion(vol_id)
- self.logger.info("deleted %s" % vol_id)
diff --git a/tempest/stress/cleanup.py b/tempest/stress/cleanup.py
deleted file mode 100644
index 5c25e32..0000000
--- a/tempest/stress/cleanup.py
+++ /dev/null
@@ -1,118 +0,0 @@
-#!/usr/bin/env python
-
-# Copyright 2013 Quanta Research Cambridge, Inc.
-#
-# 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_log import log as logging
-
-from tempest.common import credentials_factory as credentials
-from tempest.common import waiters
-
-LOG = logging.getLogger(__name__)
-
-
-def cleanup():
- admin_manager = credentials.AdminManager()
-
- body = admin_manager.servers_client.list_servers(all_tenants=True)
- LOG.info("Cleanup::remove %s servers" % len(body['servers']))
- for s in body['servers']:
- try:
- admin_manager.servers_client.delete_server(s['id'])
- except Exception:
- pass
-
- for s in body['servers']:
- try:
- waiters.wait_for_server_termination(admin_manager.servers_client,
- s['id'])
- except Exception:
- pass
-
- keypairs = admin_manager.keypairs_client.list_keypairs()['keypairs']
- LOG.info("Cleanup::remove %s keypairs" % len(keypairs))
- for k in keypairs:
- try:
- admin_manager.keypairs_client.delete_keypair(k['name'])
- except Exception:
- pass
-
- secgrp_client = admin_manager.compute_security_groups_client
- secgrp = (secgrp_client.list_security_groups(all_tenants=True)
- ['security_groups'])
- secgrp_del = [grp for grp in secgrp if grp['name'] != 'default']
- LOG.info("Cleanup::remove %s Security Group" % len(secgrp_del))
- for g in secgrp_del:
- try:
- secgrp_client.delete_security_group(g['id'])
- except Exception:
- pass
-
- admin_floating_ips_client = admin_manager.compute_floating_ips_client
- floating_ips = (admin_floating_ips_client.list_floating_ips()
- ['floating_ips'])
- LOG.info("Cleanup::remove %s floating ips" % len(floating_ips))
- for f in floating_ips:
- try:
- admin_floating_ips_client.delete_floating_ip(f['id'])
- except Exception:
- pass
-
- users = admin_manager.identity_client.list_users()['users']
- LOG.info("Cleanup::remove %s users" % len(users))
- for user in users:
- if user['name'].startswith("stress_user"):
- admin_manager.identity_client.delete_user(user['id'])
- tenants = admin_manager.tenants_client.list_tenants()['tenants']
- LOG.info("Cleanup::remove %s tenants" % len(tenants))
- for tenant in tenants:
- if tenant['name'].startswith("stress_tenant"):
- admin_manager.tenants_client.delete_tenant(tenant['id'])
-
- # We have to delete snapshots first or
- # volume deletion may block
-
- _, snaps = admin_manager.snapshots_client.list_snapshots(
- params={"all_tenants": True})['snapshots']
- LOG.info("Cleanup::remove %s snapshots" % len(snaps))
- for v in snaps:
- try:
- admin_manager.snapshots_client.\
- wait_for_snapshot_status(v['id'], 'available')
- admin_manager.snapshots_client.delete_snapshot(v['id'])
- except Exception:
- pass
-
- for v in snaps:
- try:
- admin_manager.snapshots_client.wait_for_resource_deletion(v['id'])
- except Exception:
- pass
-
- vols = admin_manager.volumes_client.list_volumes(
- params={"all_tenants": True})
- LOG.info("Cleanup::remove %s volumes" % len(vols))
- for v in vols:
- try:
- admin_manager.volumes_client.\
- wait_for_volume_status(v['id'], 'available')
- admin_manager.volumes_client.delete_volume(v['id'])
- except Exception:
- pass
-
- for v in vols:
- try:
- admin_manager.volumes_client.wait_for_resource_deletion(v['id'])
- except Exception:
- pass
diff --git a/tempest/stress/driver.py b/tempest/stress/driver.py
deleted file mode 100644
index a3e1ea3..0000000
--- a/tempest/stress/driver.py
+++ /dev/null
@@ -1,256 +0,0 @@
-# Copyright 2013 Quanta Research Cambridge, Inc.
-#
-# 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 multiprocessing
-import os
-import signal
-import time
-
-from oslo_log import log as logging
-from oslo_utils import importutils
-import six
-from six import moves
-from tempest_lib.common import ssh
-
-
-from tempest import clients
-from tempest.common import cred_client
-from tempest.common import credentials_factory as credentials
-from tempest.common.utils import data_utils
-from tempest import config
-from tempest import exceptions
-from tempest.stress import cleanup
-
-CONF = config.CONF
-
-LOG = logging.getLogger(__name__)
-processes = []
-
-
-def do_ssh(command, host, ssh_user, ssh_key=None):
- ssh_client = ssh.Client(host, ssh_user, key_filename=ssh_key)
- try:
- return ssh_client.exec_command(command)
- except exceptions.SSHExecCommandFailed:
- LOG.error('do_ssh raise exception. command:%s, host:%s.'
- % (command, host))
- return None
-
-
-def _get_compute_nodes(controller, ssh_user, ssh_key=None):
- """Returns a list of active compute nodes.
-
- List is generated by running nova-manage on the controller.
- """
- nodes = []
- cmd = "nova-manage service list | grep ^nova-compute"
- output = do_ssh(cmd, controller, ssh_user, ssh_key)
- if not output:
- return nodes
- # For example: nova-compute xg11eth0 nova enabled :-) 2011-10-31 18:57:46
- # This is fragile but there is, at present, no other way to get this info.
- for line in output.split('\n'):
- words = line.split()
- if len(words) > 0 and words[4] == ":-)":
- nodes.append(words[1])
- return nodes
-
-
-def _has_error_in_logs(logfiles, nodes, ssh_user, ssh_key=None,
- stop_on_error=False):
- """Detect errors in nova log files on the controller and compute nodes."""
- grep = 'egrep "ERROR|TRACE" %s' % logfiles
- ret = False
- for node in nodes:
- errors = do_ssh(grep, node, ssh_user, ssh_key)
- if len(errors) > 0:
- LOG.error('%s: %s' % (node, errors))
- ret = True
- if stop_on_error:
- break
- return ret
-
-
-def sigchld_handler(signalnum, frame):
- """Signal handler (only active if stop_on_error is True)."""
- for process in processes:
- if (not process['process'].is_alive() and
- process['process'].exitcode != 0):
- signal.signal(signalnum, signal.SIG_DFL)
- terminate_all_processes()
- break
-
-
-def terminate_all_processes(check_interval=20):
- """Goes through the process list and terminates all child processes."""
- LOG.info("Stopping all processes.")
- for process in processes:
- if process['process'].is_alive():
- try:
- process['process'].terminate()
- except Exception:
- pass
- time.sleep(check_interval)
- for process in processes:
- if process['process'].is_alive():
- try:
- pid = process['process'].pid
- LOG.warning("Process %d hangs. Send SIGKILL." % pid)
- os.kill(pid, signal.SIGKILL)
- except Exception:
- pass
- process['process'].join()
-
-
-def stress_openstack(tests, duration, max_runs=None, stop_on_error=False):
- """Workload driver. Executes an action function against a nova-cluster."""
- admin_manager = credentials.AdminManager()
-
- ssh_user = CONF.stress.target_ssh_user
- ssh_key = CONF.stress.target_private_key_path
- logfiles = CONF.stress.target_logfiles
- log_check_interval = int(CONF.stress.log_check_interval)
- default_thread_num = int(CONF.stress.default_thread_number_per_action)
- if logfiles:
- controller = CONF.stress.target_controller
- computes = _get_compute_nodes(controller, ssh_user, ssh_key)
- for node in computes:
- do_ssh("rm -f %s" % logfiles, node, ssh_user, ssh_key)
- skip = False
- for test in tests:
- for service in test.get('required_services', []):
- if not CONF.service_available.get(service):
- skip = True
- break
- if skip:
- break
- if test.get('use_admin', False):
- manager = admin_manager
- else:
- manager = credentials.ConfiguredUserManager()
- for p_number in moves.xrange(test.get('threads', default_thread_num)):
- if test.get('use_isolated_tenants', False):
- username = data_utils.rand_name("stress_user")
- tenant_name = data_utils.rand_name("stress_tenant")
- password = "pass"
- if CONF.identity.auth_version == 'v2':
- identity_client = admin_manager.identity_client
- projects_client = admin_manager.tenants_client
- roles_client = admin_manager.roles_client
- else:
- identity_client = admin_manager.identity_v3_client
- projects_client = None
- roles_client = None
- credentials_client = cred_client.get_creds_client(
- identity_client, projects_client, roles_client)
- project = credentials_client.create_project(
- name=tenant_name, description=tenant_name)
- user = credentials_client.create_user(username, password,
- project, "email")
- # Add roles specified in config file
- for conf_role in CONF.auth.tempest_roles:
- credentials_client.assign_user_role(user, project,
- conf_role)
- creds = credentials_client.get_credentials(user, project,
- password)
- manager = clients.Manager(credentials=creds)
-
- test_obj = importutils.import_class(test['action'])
- test_run = test_obj(manager, max_runs, stop_on_error)
-
- kwargs = test.get('kwargs', {})
- test_run.setUp(**dict(six.iteritems(kwargs)))
-
- LOG.debug("calling Target Object %s" %
- test_run.__class__.__name__)
-
- mp_manager = multiprocessing.Manager()
- shared_statistic = mp_manager.dict()
- shared_statistic['runs'] = 0
- shared_statistic['fails'] = 0
-
- p = multiprocessing.Process(target=test_run.execute,
- args=(shared_statistic,))
-
- process = {'process': p,
- 'p_number': p_number,
- 'action': test_run.action,
- 'statistic': shared_statistic}
-
- processes.append(process)
- p.start()
- if stop_on_error:
- # NOTE(mkoderer): only the parent should register the handler
- signal.signal(signal.SIGCHLD, sigchld_handler)
- end_time = time.time() + duration
- had_errors = False
- try:
- while True:
- if max_runs is None:
- remaining = end_time - time.time()
- if remaining <= 0:
- break
- else:
- remaining = log_check_interval
- all_proc_term = True
- for process in processes:
- if process['process'].is_alive():
- all_proc_term = False
- break
- if all_proc_term:
- break
-
- time.sleep(min(remaining, log_check_interval))
- if stop_on_error:
- if any([True for proc in processes
- if proc['statistic']['fails'] > 0]):
- break
-
- if not logfiles:
- continue
- if _has_error_in_logs(logfiles, computes, ssh_user, ssh_key,
- stop_on_error):
- had_errors = True
- break
- except KeyboardInterrupt:
- LOG.warning("Interrupted, going to print statistics and exit ...")
-
- if stop_on_error:
- signal.signal(signal.SIGCHLD, signal.SIG_DFL)
- terminate_all_processes()
-
- sum_fails = 0
- sum_runs = 0
-
- LOG.info("Statistics (per process):")
- for process in processes:
- if process['statistic']['fails'] > 0:
- had_errors = True
- sum_runs += process['statistic']['runs']
- sum_fails += process['statistic']['fails']
- print ("Process %d (%s): Run %d actions (%d failed)" % (
- process['p_number'],
- process['action'],
- process['statistic']['runs'],
- process['statistic']['fails']))
- print ("Summary:")
- print ("Run %d actions (%d failed)" % (sum_runs, sum_fails))
-
- if not had_errors and CONF.stress.full_clean_stack:
- LOG.info("cleaning up")
- cleanup.cleanup()
- if had_errors:
- return 1
- else:
- return 0
diff --git a/tempest/stress/etc/sample-unit-test.json b/tempest/stress/etc/sample-unit-test.json
deleted file mode 100644
index b388bfe..0000000
--- a/tempest/stress/etc/sample-unit-test.json
+++ /dev/null
@@ -1,8 +0,0 @@
-[{"action": "tempest.stress.actions.unit_test.UnitTest",
- "threads": 8,
- "use_admin": false,
- "use_isolated_tenants": false,
- "kwargs": {"test_method": "tempest.cli.simple_read_only.test_glance.SimpleReadOnlyGlanceClientTest.test_glance_fake_action",
- "class_setup_per": "process"}
- }
-]
diff --git a/tempest/stress/etc/server-create-destroy-test.json b/tempest/stress/etc/server-create-destroy-test.json
deleted file mode 100644
index 17d5e1a..0000000
--- a/tempest/stress/etc/server-create-destroy-test.json
+++ /dev/null
@@ -1,7 +0,0 @@
-[{"action": "tempest.stress.actions.server_create_destroy.ServerCreateDestroyTest",
- "threads": 8,
- "use_admin": false,
- "use_isolated_tenants": false,
- "kwargs": {}
- }
-]
diff --git a/tempest/stress/etc/ssh_floating.json b/tempest/stress/etc/ssh_floating.json
deleted file mode 100644
index e03fd4f..0000000
--- a/tempest/stress/etc/ssh_floating.json
+++ /dev/null
@@ -1,16 +0,0 @@
-[{"action": "tempest.stress.actions.ssh_floating.FloatingStress",
- "threads": 8,
- "use_admin": false,
- "use_isolated_tenants": false,
- "kwargs": {"vm_extra_args": {},
- "new_vm": true,
- "new_sec_group": true,
- "new_floating": true,
- "verify": ["check_icmp_echo", "check_port_ssh"],
- "check_timeout": 120,
- "check_interval": 1,
- "wait_after_vm_create": true,
- "wait_for_disassociate": true,
- "reboot": false}
-}
-]
diff --git a/tempest/stress/etc/stress-tox-job.json b/tempest/stress/etc/stress-tox-job.json
deleted file mode 100644
index 9cee316..0000000
--- a/tempest/stress/etc/stress-tox-job.json
+++ /dev/null
@@ -1,28 +0,0 @@
-[{"action": "tempest.stress.actions.server_create_destroy.ServerCreateDestroyTest",
- "threads": 8,
- "use_admin": false,
- "use_isolated_tenants": false,
- "kwargs": {}
- },
- {"action": "tempest.stress.actions.volume_create_delete.VolumeCreateDeleteTest",
- "threads": 4,
- "use_admin": false,
- "use_isolated_tenants": false,
- "kwargs": {}
- },
- {"action": "tempest.stress.actions.volume_attach_delete.VolumeAttachDeleteTest",
- "threads": 2,
- "use_admin": false,
- "use_isolated_tenants": false,
- "kwargs": {}
- },
- {"action": "tempest.stress.actions.unit_test.UnitTest",
- "threads": 4,
- "use_admin": false,
- "use_isolated_tenants": false,
- "required_services": ["neutron"],
- "kwargs": {"test_method": "tempest.scenario.test_network_advanced_server_ops.TestNetworkAdvancedServerOps.test_server_connectivity_stop_start",
- "class_setup_per": "process"}
- }
-]
-
diff --git a/tempest/stress/etc/volume-attach-delete-test.json b/tempest/stress/etc/volume-attach-delete-test.json
deleted file mode 100644
index 4553ff8..0000000
--- a/tempest/stress/etc/volume-attach-delete-test.json
+++ /dev/null
@@ -1,7 +0,0 @@
-[{"action": "tempest.stress.actions.volume_attach_delete.VolumeAttachDeleteTest",
- "threads": 4,
- "use_admin": false,
- "use_isolated_tenants": false,
- "kwargs": {}
- }
-]
diff --git a/tempest/stress/etc/volume-attach-verify.json b/tempest/stress/etc/volume-attach-verify.json
deleted file mode 100644
index 731f5ed..0000000
--- a/tempest/stress/etc/volume-attach-verify.json
+++ /dev/null
@@ -1,11 +0,0 @@
-[{"action": "tempest.stress.actions.volume_attach_verify.VolumeVerifyStress",
- "threads": 1,
- "use_admin": false,
- "use_isolated_tenants": false,
- "kwargs": {"vm_extra_args": {},
- "new_volume": true,
- "new_server": false,
- "ssh_test_before_attach": false,
- "enable_ssh_verify": true}
-}
-]
diff --git a/tempest/stress/etc/volume-create-delete-test.json b/tempest/stress/etc/volume-create-delete-test.json
deleted file mode 100644
index e8a58f7..0000000
--- a/tempest/stress/etc/volume-create-delete-test.json
+++ /dev/null
@@ -1,7 +0,0 @@
-[{"action": "tempest.stress.actions.volume_create_delete.VolumeCreateDeleteTest",
- "threads": 4,
- "use_admin": false,
- "use_isolated_tenants": false,
- "kwargs": {}
- }
-]
diff --git a/tempest/stress/stressaction.py b/tempest/stress/stressaction.py
deleted file mode 100644
index c8bd652..0000000
--- a/tempest/stress/stressaction.py
+++ /dev/null
@@ -1,96 +0,0 @@
-# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
-#
-# 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 abc
-import signal
-import sys
-
-import six
-
-from oslo_log import log as logging
-
-
-@six.add_metaclass(abc.ABCMeta)
-class StressAction(object):
-
- def __init__(self, manager, max_runs=None, stop_on_error=False):
- full_cname = self.__module__ + "." + self.__class__.__name__
- self.logger = logging.getLogger(full_cname)
- self.manager = manager
- self.max_runs = max_runs
- self.stop_on_error = stop_on_error
-
- def _shutdown_handler(self, signal, frame):
- try:
- self.tearDown()
- except Exception:
- self.logger.exception("Error while tearDown")
- sys.exit(0)
-
- @property
- def action(self):
- """This methods returns the action.
-
- Overload this if you create a stress test wrapper.
- """
- return self.__class__.__name__
-
- def setUp(self, **kwargs):
- """Initialize test structures/resources
-
- This method is called before "run" method to help the test
- initialize any structures. kwargs contains arguments passed
- in from the configuration json file.
-
- setUp doesn't count against the time duration.
- """
- self.logger.debug("setUp")
-
- def tearDown(self):
- """Cleanup test structures/resources
-
- This method is called to do any cleanup after the test is complete.
- """
- self.logger.debug("tearDown")
-
- def execute(self, shared_statistic):
- """This is the main execution entry point called by the driver.
-
- We register a signal handler to allow us to tearDown gracefully,
- and then exit. We also keep track of how many runs we do.
- """
- signal.signal(signal.SIGHUP, self._shutdown_handler)
- signal.signal(signal.SIGTERM, self._shutdown_handler)
-
- while self.max_runs is None or (shared_statistic['runs'] <
- self.max_runs):
- self.logger.debug("Trigger new run (run %d)" %
- shared_statistic['runs'])
- try:
- self.run()
- except Exception:
- shared_statistic['fails'] += 1
- self.logger.exception("Failure in run")
- finally:
- shared_statistic['runs'] += 1
- if self.stop_on_error and (shared_statistic['fails'] > 1):
- self.logger.warn("Stop process due to"
- "\"stop-on-error\" argument")
- self.tearDown()
- sys.exit(1)
-
- @abc.abstractmethod
- def run(self):
- """This method is where the stress test code runs."""
- return
diff --git a/tempest/stress/tools/cleanup.py b/tempest/stress/tools/cleanup.py
deleted file mode 100755
index 3885ba0..0000000
--- a/tempest/stress/tools/cleanup.py
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/usr/bin/env python
-
-# Copyright 2013 Quanta Research Cambridge, Inc.
-#
-# 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.stress import cleanup
-
-cleanup.cleanup()
diff --git a/tempest/test.py b/tempest/test.py
index aed438c..f2babbb 100644
--- a/tempest/test.py
+++ b/tempest/test.py
@@ -14,171 +14,53 @@
# under the License.
import atexit
-import functools
import os
-import re
import sys
-import time
-import urllib
-import uuid
+import debtcollector.moves
import fixtures
from oslo_log import log as logging
-from oslo_serialization import jsonutils as json
-from oslo_utils import importutils
import six
-from tempest_lib import decorators
-import testscenarios
import testtools
from tempest import clients
-from tempest.common import cred_client
from tempest.common import credentials_factory as credentials
-from tempest.common import fixed_network
-import tempest.common.generator.valid_generator as valid
-import tempest.common.validation_resources as vresources
+from tempest.common import utils
from tempest import config
-from tempest import exceptions
+from tempest.lib.common import fixed_network
+from tempest.lib.common import validation_resources as vr
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
LOG = logging.getLogger(__name__)
CONF = config.CONF
-idempotent_id = decorators.idempotent_id
+# TODO(oomichi): This test.idempotent_id should be removed after all projects
+# switch to use decorators.idempotent_id.
+idempotent_id = debtcollector.moves.moved_function(
+ decorators.idempotent_id, 'idempotent_id', __name__,
+ version='Mitaka', removal_version='?')
-def attr(**kwargs):
- """A decorator which applies the testtools attr decorator
-
- This decorator applies the testtools.testcase.attr if it is in the list of
- attributes to testtools we want to apply.
- """
-
- def decorator(f):
- if 'type' in kwargs and isinstance(kwargs['type'], str):
- f = testtools.testcase.attr(kwargs['type'])(f)
- elif 'type' in kwargs and isinstance(kwargs['type'], list):
- for attr in kwargs['type']:
- f = testtools.testcase.attr(attr)(f)
- return f
-
- return decorator
+attr = debtcollector.moves.moved_function(
+ decorators.attr, 'attr', __name__,
+ version='Pike', removal_version='?')
-def get_service_list():
- service_list = {
- 'compute': CONF.service_available.nova,
- 'image': CONF.service_available.glance,
- 'baremetal': CONF.service_available.ironic,
- 'volume': CONF.service_available.cinder,
- 'orchestration': CONF.service_available.heat,
- # NOTE(mtreinish) nova-network will provide networking functionality
- # if neutron isn't available, so always set to True.
- 'network': True,
- 'identity': True,
- 'object_storage': CONF.service_available.swift,
- 'dashboard': CONF.service_available.horizon,
- 'telemetry': CONF.service_available.ceilometer,
- 'data_processing': CONF.service_available.sahara,
- 'database': CONF.service_available.trove
- }
- return service_list
+services = debtcollector.moves.moved_function(
+ utils.services, 'services', __name__,
+ version='Pike', removal_version='?')
-def services(*args):
- """A decorator used to set an attr for each service used in a test case
-
- This decorator applies a testtools attr for each service that gets
- exercised by a test case.
- """
- def decorator(f):
- services = ['compute', 'image', 'baremetal', 'volume', 'orchestration',
- 'network', 'identity', 'object_storage', 'dashboard',
- 'telemetry', 'data_processing', 'database']
- for service in args:
- if service not in services:
- raise exceptions.InvalidServiceTag('%s is not a valid '
- 'service' % service)
- attr(type=list(args))(f)
-
- @functools.wraps(f)
- def wrapper(self, *func_args, **func_kwargs):
- service_list = get_service_list()
-
- for service in args:
- if not service_list[service]:
- msg = 'Skipped because the %s service is not available' % (
- service)
- raise testtools.TestCase.skipException(msg)
- return f(self, *func_args, **func_kwargs)
- return wrapper
- return decorator
+requires_ext = debtcollector.moves.moved_function(
+ utils.requires_ext, 'requires_ext', __name__,
+ version='Pike', removal_version='?')
-def stresstest(**kwargs):
- """Add stress test decorator
-
- For all functions with this decorator a attr stress will be
- set automatically.
-
- @param class_setup_per: allowed values are application, process, action
- ``application``: once in the stress job lifetime
- ``process``: once in the worker process lifetime
- ``action``: on each action
- @param allow_inheritance: allows inheritance of this attribute
- """
- def decorator(f):
- if 'class_setup_per' in kwargs:
- setattr(f, "st_class_setup_per", kwargs['class_setup_per'])
- else:
- setattr(f, "st_class_setup_per", 'process')
- if 'allow_inheritance' in kwargs:
- setattr(f, "st_allow_inheritance", kwargs['allow_inheritance'])
- else:
- setattr(f, "st_allow_inheritance", False)
- attr(type='stress')(f)
- return f
- return decorator
-
-
-def requires_ext(**kwargs):
- """A decorator to skip tests if an extension is not enabled
-
- @param extension
- @param service
- """
- def decorator(func):
- @functools.wraps(func)
- def wrapper(*func_args, **func_kwargs):
- if not is_extension_enabled(kwargs['extension'],
- kwargs['service']):
- msg = "Skipped because %s extension: %s is not enabled" % (
- kwargs['service'], kwargs['extension'])
- raise testtools.TestCase.skipException(msg)
- return func(*func_args, **func_kwargs)
- return wrapper
- return decorator
-
-
-def is_extension_enabled(extension_name, service):
- """A function that will check the list of enabled extensions from config
-
- """
- config_dict = {
- 'compute': CONF.compute_feature_enabled.api_extensions,
- 'volume': CONF.volume_feature_enabled.api_extensions,
- 'network': CONF.network_feature_enabled.api_extensions,
- 'object': CONF.object_storage_feature_enabled.discoverable_apis,
- 'identity': CONF.identity_feature_enabled.api_extensions
- }
- if len(config_dict[service]) == 0:
- return False
- if config_dict[service][0] == 'all':
- return True
- if extension_name in config_dict[service]:
- return True
- return False
-
+is_extension_enabled = debtcollector.moves.moved_function(
+ utils.is_extension_enabled, 'is_extension_enabled', __name__,
+ version='Pike', removal_version='?')
at_exit_set = set()
@@ -186,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)
@@ -215,18 +97,24 @@
- resource_cleanup
"""
- setUpClassCalled = False
- _service = None
-
# NOTE(andreaf) credentials holds a list of the credentials to be allocated
# at class setup time. Credential types can be 'primary', 'alt', 'admin' or
# a list of roles - the first element of the list being a label, and the
# rest the actual roles
credentials = []
+
+ # Track if setUpClass was invoked
+ __setupclass_called = False
+
+ # Network resources to be provisioned for the requested test credentials.
+ # Only used with the dynamic credentials provider.
+ _network_resources = {}
+
+ # Stack of resource cleanups
+ _class_cleanups = []
+
# Resources required to validate a server using ssh
- validation_resources = {}
- network_resources = {}
- services_microversion = {}
+ _validation_resources = {}
# NOTE(sdague): log_format is defined inline here instead of using the oslo
# default because going through the config path recouples config to the
@@ -234,29 +122,51 @@
log_format = ('%(asctime)s %(process)d %(levelname)-8s '
'[%(name)s] %(message)s')
+ # Client manager class to use in this test case.
+ client_manager = clients.Manager
+
+ # A way to adjust slow test classes
+ TIMEOUT_SCALING_FACTOR = 1
+
+ @classmethod
+ def _reset_class(cls):
+ cls.__setup_credentials_called = False
+ cls.__resource_cleanup_called = False
+ cls.__skip_checks_called = False
+ # Stack of callable to be invoked in reverse order
+ cls._class_cleanups = []
+ # Stack of (name, callable) to be invoked in reverse order at teardown
+ cls._teardowns = []
+
@classmethod
def setUpClass(cls):
+ cls.__setupclass_called = True
+ # Reset state
+ cls._reset_class()
# It should never be overridden by descendants
if hasattr(super(BaseTestCase, cls), 'setUpClass'):
super(BaseTestCase, cls).setUpClass()
- cls.setUpClassCalled = True
- # Stack of (name, callable) to be invoked in reverse order at teardown
- cls.teardowns = []
# All the configuration checks that may generate a skip
cls.skip_checks()
+ if not cls.__skip_checks_called:
+ raise RuntimeError("skip_checks for %s did not call the super's "
+ "skip_checks" % cls.__name__)
try:
# Allocation of all required credentials and client managers
- cls.teardowns.append(('credentials', cls.clear_credentials))
+ cls._teardowns.append(('credentials', cls.clear_credentials))
cls.setup_credentials()
+ if not cls.__setup_credentials_called:
+ raise RuntimeError("setup_credentials for %s did not call the "
+ "super's setup_credentials" % cls.__name__)
# Shortcuts to clients
cls.setup_clients()
# Additional class-wide test resources
- cls.teardowns.append(('resources', cls.resource_cleanup))
+ cls._teardowns.append(('resources', cls.resource_cleanup))
cls.resource_setup()
except Exception:
etype, value, trace = sys.exc_info()
- LOG.info("%s raised in %s.setUpClass. Invoking tearDownClass." % (
- etype, cls.__name__))
+ LOG.info("%s raised in %s.setUpClass. Invoking tearDownClass.",
+ etype, cls.__name__)
cls.tearDownClass()
try:
six.reraise(etype, value, trace)
@@ -265,6 +175,9 @@
@classmethod
def tearDownClass(cls):
+ # insert pdb breakpoint when pause_teardown is enabled
+ if CONF.pause_teardown:
+ cls.insert_pdb_breakpoint()
at_exit_set.discard(cls)
# It should never be overridden by descendants
if hasattr(super(BaseTestCase, cls), 'tearDownClass'):
@@ -275,25 +188,36 @@
# If there was no exception during setup we shall re-raise the first
# exception in teardown
re_raise = (etype is None)
- while cls.teardowns:
- name, teardown = cls.teardowns.pop()
+ while cls._teardowns:
+ name, teardown = cls._teardowns.pop()
# Catch any exception in tearDown so we can re-raise the original
# exception at the end
try:
teardown()
+ if name == 'resources':
+ if not cls.__resource_cleanup_called:
+ raise RuntimeError(
+ "resource_cleanup for %s did not call the "
+ "super's resource_cleanup" % cls.__name__)
except Exception as te:
sys_exec_info = sys.exc_info()
tetype = sys_exec_info[0]
- # TODO(andreaf): Till we have the ability to cleanup only
- # resources that were successfully setup in resource_cleanup,
- # log AttributeError as info instead of exception.
+ # TODO(andreaf): Resource cleanup is often implemented by
+ # storing an array of resources at class level, and cleaning
+ # them up during `resource_cleanup`.
+ # In case of failure during setup, some resource arrays might
+ # not be defined at all, in which case the cleanup code might
+ # trigger an AttributeError. In such cases we log
+ # AttributeError as info instead of exception. Once all
+ # cleanups are migrated to addClassResourceCleanup we can
+ # remove this.
if tetype is AttributeError and name == 'resources':
- LOG.info("tearDownClass of %s failed: %s" % (name, te))
+ LOG.info("tearDownClass of %s failed: %s", name, te)
else:
- LOG.exception("teardown of %s failed: %s" % (name, te))
+ LOG.exception("teardown of %s failed: %s", name, te)
if not etype:
etype, value, trace = sys_exec_info
- # If exceptions were raised during teardown, an not before, re-raise
+ # If exceptions were raised during teardown, and not before, re-raise
# the first one
if re_raise and etype is not None:
try:
@@ -301,23 +225,66 @@
finally:
del trace # to avoid circular refs
+ def tearDown(self):
+ super(BaseTestCase, self).tearDown()
+ # insert pdb breakpoint when pause_teardown is enabled
+ if CONF.pause_teardown:
+ BaseTestCase.insert_pdb_breakpoint()
+
+ @classmethod
+ def insert_pdb_breakpoint(cls):
+ """Add pdb breakpoint.
+
+ This can help in debugging process, cleaning of resources is
+ paused, so they can be examined.
+ """
+ import pdb
+ pdb.set_trace()
+
@classmethod
def skip_checks(cls):
"""Class level skip checks.
Subclasses verify in here all conditions that might prevent the
- execution of the entire test class.
- Checks implemented here may not make use API calls, and should rely on
- configuration alone.
- In general skip checks that require an API call are discouraged.
- If one is really needed it may be implemented either in the
- resource_setup or at test level.
+ execution of the entire test class. Skipping here prevents any other
+ class fixture from being executed i.e. no credentials or other
+ resource allocation will happen.
+
+ Tests defined in the test class will no longer appear in test results.
+ The `setUpClass` for the entire test class will be marked as SKIPPED
+ instead.
+
+ At this stage no test credentials are available, so skip checks
+ should rely on configuration alone. This is deliberate since skips
+ based on the result of an API call are discouraged.
+
+ The following checks are implemented in `test.py` already:
+ - check that alt credentials are available when requested by the test
+ - check that admin credentials are available when requested by the test
+ - check that the identity version specified by the test is marked as
+ enabled in the configuration
+
+ Overriders of skip_checks must always invoke skip_check on `super`
+ first.
+
+ Example::
+
+ @classmethod
+ def skip_checks(cls):
+ super(Example, cls).skip_checks()
+ if not CONF.service_available.my_service:
+ skip_msg = ("%s skipped as my_service is not available")
+ raise cls.skipException(skip_msg % cls.__name__)
"""
+ cls.__skip_checks_called = True
identity_version = cls.get_identity_version()
- if 'admin' in cls.credentials and not credentials.is_admin_available(
- identity_version=identity_version):
- msg = "Missing Identity Admin API credentials in configuration."
- raise cls.skipException(msg)
+ # setting force_tenant_isolation to True also needs admin credentials.
+ if ('admin' in cls.credentials or
+ getattr(cls, 'force_tenant_isolation', False)):
+ if not credentials.is_admin_available(
+ identity_version=identity_version):
+ raise cls.skipException(
+ "Missing Identity Admin API credentials in configuration.")
if 'alt' in cls.credentials and not credentials.is_alt_available(
identity_version=identity_version):
msg = "Missing a 2nd set of API credentials in configuration."
@@ -332,12 +299,69 @@
@classmethod
def setup_credentials(cls):
- """Allocate credentials and the client managers from them.
+ """Allocate credentials and create the client managers from them.
- A test class that requires network resources must override
- setup_credentials and defined the required resources before super
- is invoked.
+ `setup_credentials` looks for the content of the `credentials`
+ attribute in the test class. If the value is a non-empty collection,
+ a credentials provider is setup, and credentials are provisioned or
+ allocated based on the content of the collection. Every set of
+ credentials is associated to an object of type `cls.client_manager`.
+ The client manager is accessible by tests via class attribute
+ `os_[type]`:
+
+ Valid values in `credentials` are:
+ - 'primary':
+ A normal user is provisioned.
+ It can be used only once. Multiple entries will be ignored.
+ Clients are available at os_primary.
+ - 'alt':
+ A normal user other than 'primary' is provisioned.
+ It can be used only once. Multiple entries will be ignored.
+ Clients are available at os_alt.
+ - 'admin':
+ An admin user is provisioned.
+ It can be used only once. Multiple entries will be ignored.
+ Clients are available at os_admin.
+ - A list in the format ['any_label', 'role1', ... , 'roleN']:
+ A client with roles <list>[1:] is provisioned.
+ It can be used multiple times, with unique labels.
+ Clients are available at os_roles_<list>[0].
+
+ By default network resources are allocated (in case of dynamic
+ credentials). Tests that do not need network or that require a
+ custom network setup must specify which network resources shall
+ be provisioned using the `set_network_resources()` method (note
+ that it must be invoked before the `setup_credentials` is
+ invoked on super).
+
+ Example::
+
+ class TestWithCredentials(test.BaseTestCase):
+
+ credentials = ['primary', 'admin',
+ ['special', 'special_role1']]
+
+ @classmethod
+ def setup_credentials(cls):
+ # set_network_resources must be called first
+ cls.set_network_resources(network=True)
+ super(TestWithCredentials, cls).setup_credentials()
+
+ @classmethod
+ def setup_clients(cls):
+ cls.servers = cls.os_primary.compute.ServersClient()
+ cls.admin_servers = cls.os_admin.compute.ServersClient()
+ # certain API calls may require a user with a specific
+ # role assigned. In this example `special_role1` is
+ # assigned to the user in `cls.os_roles_special`.
+ cls.special_servers = (
+ cls.os_roles_special.compute.ServersClient())
+
+ def test_special_servers(self):
+ # Do something with servers
+ pass
"""
+ cls.__setup_credentials_called = True
for credentials_type in cls.credentials:
# This may raise an exception in case credentials are not available
# In that case we want to let the exception through and the test
@@ -346,16 +370,32 @@
manager = cls.get_client_manager(
credential_type=credentials_type)
setattr(cls, 'os_%s' % credentials_type, manager)
+ # NOTE(jordanP): Tempest should use os_primary, os_admin
+ # and os_alt throughout its code base but we keep the aliases
+ # around for a while for Tempest plugins. Aliases should be
+ # removed eventually.
# Setup some common aliases
- # TODO(andreaf) The aliases below are a temporary hack
- # to avoid changing too much code in one patch. They should
- # be removed eventually
if credentials_type == 'primary':
- cls.os = cls.manager = cls.os_primary
+ cls.os = debtcollector.moves.moved_read_only_property(
+ 'os', 'os_primary', version='Pike',
+ removal_version='Queens')
+ cls.manager =\
+ debtcollector.moves.moved_read_only_property(
+ 'manager', 'os_primary', version='Pike',
+ removal_version='Queens')
if credentials_type == 'admin':
- cls.os_adm = cls.admin_manager = cls.os_admin
+ cls.os_adm = debtcollector.moves.moved_read_only_property(
+ 'os_adm', 'os_admin', version='Pike',
+ removal_version='Queens')
+ cls.admin_manager =\
+ debtcollector.moves.moved_read_only_property(
+ 'admin_manager', 'os_admin', version='Pike',
+ removal_version='Queens')
if credentials_type == 'alt':
- cls.alt_manager = cls.os_alt
+ cls.alt_manager =\
+ debtcollector.moves.moved_read_only_property(
+ 'alt_manager', 'os_alt', version='Pike',
+ removal_version='Queens')
elif isinstance(credentials_type, list):
manager = cls.get_client_manager(roles=credentials_type[1:],
force_new=True)
@@ -363,48 +403,191 @@
@classmethod
def setup_clients(cls):
- """Create links to the clients into the test object."""
- # TODO(andreaf) There is a fair amount of code that could me moved from
- # base / test classes in here. Ideally tests should be able to only
- # specify which client is `client` and nothing else.
+ """Create aliases to the clients in the client managers.
+
+ `setup_clients` is invoked after the credential provisioning step.
+ Client manager objects are available to tests already. The purpose
+ of this helper is to setup shortcuts to specific clients that are
+ useful for the tests implemented in the test class.
+
+ Its purpose is mostly for code readability, however it should be used
+ carefully to avoid doing exactly the opposite, i.e. making the code
+ unreadable and hard to debug. If aliases are defined in a super class
+ it won't be obvious what they refer to, so it's good practice to define
+ all aliases used in the class. Aliases are meant to be shortcuts to
+ be used in tests, not shortcuts to avoid helper method attributes.
+ If an helper method starts relying on a client alias and a subclass
+ overrides that alias, it will become rather difficult to understand
+ what the helper method actually does.
+
+ Example::
+
+ class TestDoneItRight(test.BaseTestCase):
+
+ credentials = ['primary', 'alt']
+
+ @classmethod
+ def setup_clients(cls):
+ super(TestDoneItRight, cls).setup_clients()
+ cls.servers = cls.os_primary.ServersClient()
+ cls.servers_alt = cls.os_alt.ServersClient()
+
+ def _a_good_helper(self, clients):
+ # Some complex logic we're going to use many times
+ servers = clients.ServersClient()
+ vm = servers.create_server(...)
+
+ def delete_server():
+ test_utils.call_and_ignore_notfound_exc(
+ servers.delete_server, vm['id'])
+
+ self.addCleanup(self.delete_server)
+ return vm
+
+ def test_with_servers(self):
+ vm = self._a_good_helper(os.primary)
+ vm_alt = self._a_good_helper(os.alt)
+ cls.servers.show_server(vm['id'])
+ cls.servers_alt.show_server(vm_alt['id'])
+ """
pass
@classmethod
def resource_setup(cls):
- """Class level resource setup for test cases."""
- if hasattr(cls, "os"):
- cls.validation_resources = vresources.create_validation_resources(
- cls.os, cls.validation_resources)
- else:
- LOG.warning("Client manager not found, validation resources not"
- " created")
+ """Class level resource setup for test cases.
+
+ `resource_setup` is invoked once all credentials (and related network
+ resources have been provisioned and after client aliases - if any -
+ have been defined.
+
+ The use case for `resource_setup` is test optimization: provisioning
+ of project-specific "expensive" resources that are not dirtied by tests
+ and can thus safely be re-used by multiple tests.
+
+ System wide resources shared by all tests could instead be provisioned
+ only once, before the test run.
+
+ Resources provisioned here must be cleaned up during
+ `resource_cleanup`. This is best achieved by scheduling a cleanup via
+ `addClassResourceCleanup`.
+
+ Some test resources have an asynchronous delete process. It's best
+ practice for them to schedule a wait for delete via
+ `addClassResourceCleanup` to avoid having resources in process of
+ deletion when we reach the credentials cleanup step.
+
+ Example::
+
+ @classmethod
+ def resource_setup(cls):
+ super(MyTest, cls).resource_setup()
+ servers = cls.os_primary.compute.ServersClient()
+ # Schedule delete and wait so that we can first delete the
+ # two servers and then wait for both to delete
+ # Create server 1
+ cls.shared_server = servers.create_server()
+ # Create server 2. If something goes wrong we schedule cleanup
+ # of server 1 anyways.
+ try:
+ cls.shared_server2 = servers.create_server()
+ # Wait server 2
+ cls.addClassResourceCleanup(
+ waiters.wait_for_server_termination,
+ servers, cls.shared_server2['id'],
+ ignore_error=False)
+ finally:
+ # Wait server 1
+ cls.addClassResourceCleanup(
+ waiters.wait_for_server_termination,
+ servers, cls.shared_server['id'],
+ ignore_error=False)
+ # Delete server 1
+ cls.addClassResourceCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ servers.delete_server,
+ cls.shared_server['id'])
+ # Delete server 2 (if it was created)
+ if hasattr(cls, 'shared_server2'):
+ cls.addClassResourceCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ servers.delete_server,
+ cls.shared_server2['id'])
+ """
+ pass
@classmethod
def resource_cleanup(cls):
"""Class level resource cleanup for test cases.
- Resource cleanup must be able to handle the case of partially setup
- resources, in case a failure during `resource_setup` should happen.
+ Resource cleanup processes the stack of cleanups produced by
+ `addClassResourceCleanup` and then cleans up validation resources
+ if any were provisioned.
+
+ All cleanups are processed whatever the outcome. Exceptions are
+ accumulated and re-raised as a `MultipleExceptions` at the end.
+
+ In most cases test cases won't need to override `resource_cleanup`,
+ but if they do they must invoke `resource_cleanup` on super.
+
+ Example::
+
+ class TestWithReallyComplexCleanup(test.BaseTestCase):
+
+ @classmethod
+ def resource_setup(cls):
+ # provision resource A
+ cls.addClassResourceCleanup(delete_resource, A)
+ # provision resource B
+ cls.addClassResourceCleanup(delete_resource, B)
+
+ @classmethod
+ def resource_cleanup(cls):
+ # It's possible to override resource_cleanup but in most
+ # cases it shouldn't be required. Nothing that may fail
+ # should be executed before the call to super since it
+ # might cause resource leak in case of error.
+ super(TestWithReallyComplexCleanup, cls).resource_cleanup()
+ # At this point test credentials are still available but
+ # anything from the cleanup stack has been already deleted.
"""
- if cls.validation_resources:
- if hasattr(cls, "os"):
- vresources.clear_validation_resources(cls.os,
- cls.validation_resources)
- cls.validation_resources = {}
- else:
- LOG.warning("Client manager not found, validation resources "
- "not deleted")
+ cls.__resource_cleanup_called = True
+ cleanup_errors = []
+ while cls._class_cleanups:
+ try:
+ fn, args, kwargs = cls._class_cleanups.pop()
+ fn(*args, **kwargs)
+ except Exception:
+ cleanup_errors.append(sys.exc_info())
+ if cleanup_errors:
+ raise testtools.MultipleExceptions(*cleanup_errors)
+
+ @classmethod
+ def addClassResourceCleanup(cls, fn, *arguments, **keywordArguments):
+ """Add a cleanup function to be called during resource_cleanup.
+
+ Functions added with addClassResourceCleanup will be called in reverse
+ order of adding at the beginning of resource_cleanup, before any
+ credential, networking or validation resources cleanup is processed.
+
+ If a function added with addClassResourceCleanup raises an exception,
+ the error will be recorded as a test error, and the next cleanup will
+ then be run.
+
+ Cleanup functions are always called during the test class tearDown
+ fixture, even if an exception occured during setUp or tearDown.
+ """
+ cls._class_cleanups.append((fn, arguments, keywordArguments))
def setUp(self):
super(BaseTestCase, self).setUp()
- if not self.setUpClassCalled:
+ 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:
- test_timeout = int(test_timeout)
+ test_timeout = int(test_timeout) * self.TIMEOUT_SCALING_FACTOR
except ValueError:
test_timeout = 0
if test_timeout > 0:
@@ -419,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))
@@ -428,31 +611,6 @@
def credentials_provider(self):
return self._get_credentials_provider()
- @property
- def identity_utils(self):
- """A client that abstracts v2 and v3 identity operations.
-
- This can be used for creating and tearing down projects in tests. It
- should not be used for testing identity features.
- """
- if CONF.identity.auth_version == 'v2':
- client = self.os_admin.identity_client
- project_client = self.os_admin.tenants_client
- roles_client = self.os_admin.roles_client
- else:
- client = self.os_admin.identity_v3_client
- project_client = None
- roles_client = None
-
- try:
- domain = client.auth_provider.credentials.project_domain_name
- except AttributeError:
- domain = 'Default'
-
- return cred_client.get_creds_client(client, project_client,
- roles_client,
- project_domain_name=domain)
-
@classmethod
def get_identity_version(cls):
"""Returns the identity version used by the test class"""
@@ -464,7 +622,9 @@
"""Returns a credentials provider
If no credential provider exists yet creates one.
- It uses self.identity_version if defined, or the configuration value
+ It always use the configuration value from identity.auth_version,
+ since we always want to provision accounts with the current version
+ of the identity API.
"""
if (not hasattr(cls, '_creds_provider') or not cls._creds_provider or
not cls._creds_provider.name == cls.__name__):
@@ -472,9 +632,8 @@
False)
cls._creds_provider = credentials.get_credentials_provider(
- name=cls.__name__, network_resources=cls.network_resources,
- force_tenant_isolation=force_tenant_isolation,
- identity_version=cls.get_identity_version())
+ name=cls.__name__, network_resources=cls._network_resources,
+ force_tenant_isolation=force_tenant_isolation)
return cls._creds_provider
@classmethod
@@ -514,10 +673,13 @@
if hasattr(cred_provider, credentials_method):
creds = getattr(cred_provider, credentials_method)()
else:
- raise exceptions.InvalidCredentials(
+ raise lib_exc.InvalidCredentials(
"Invalid credentials type %s" % credential_type)
- return clients.Manager(credentials=creds, service=cls._service,
- api_microversions=cls.services_microversion)
+ manager = cls.client_manager(credentials=creds.credentials)
+ # NOTE(andreaf) Ensure credentials have user and project id fields.
+ # It may not be the case when using pre-provisioned credentials.
+ manager.auth_provider.set_auth()
+ return manager
@classmethod
def clear_credentials(cls):
@@ -525,332 +687,188 @@
if hasattr(cls, '_creds_provider'):
cls._creds_provider.clear_creds()
+ @staticmethod
+ def _validation_resources_params_from_conf():
+ return dict(
+ keypair=(CONF.validation.auth_method.lower() == "keypair"),
+ floating_ip=(CONF.validation.connect_method.lower() == "floating"),
+ security_group=CONF.validation.security_group,
+ security_group_rules=CONF.validation.security_group_rules,
+ use_neutron=CONF.service_available.neutron,
+ ethertype='IPv' + str(CONF.validation.ip_version_for_ssh),
+ floating_network_id=CONF.network.public_network_id,
+ floating_network_name=CONF.network.floating_network_name)
+
@classmethod
- def set_validation_resources(cls, keypair=None, floating_ip=None,
- security_group=None,
- security_group_rules=None):
- """Specify which ssh server validation resources should be created.
+ def get_class_validation_resources(cls, os_clients):
+ """Provision validation resources according to configuration
- Each of the argument must be set to either None, True or False, with
- None - use default from config (security groups and security group
- rules get created when set to None)
- False - Do not create the validation resource
- True - create the validation resource
+ This is a wrapper around `create_validation_resources` from
+ `tempest.common.validation_resources` that passes parameters from
+ Tempest configuration. Only one instance of class level
+ validation resources is managed by the helper, so If resources
+ were already provisioned before, existing ones will be returned.
- @param keypair
- @param security_group
- @param security_group_rules
- @param floating_ip
+ Resources are returned as a dictionary. They are also scheduled for
+ automatic cleanup during class teardown using
+ `addClassResourcesCleanup`.
+
+ If `CONF.validation.run_validation` is False no resource will be
+ provisioned at all.
+
+ @param os_clients: Clients to be used to provision the resources.
"""
if not CONF.validation.run_validation:
return
- if keypair is None:
- if CONF.validation.auth_method.lower() == "keypair":
- keypair = True
- else:
- keypair = False
- if floating_ip is None:
- if CONF.validation.connect_method.lower() == "floating":
- floating_ip = True
- else:
- floating_ip = False
- if security_group is None:
- security_group = CONF.validation.security_group
- if security_group_rules is None:
- security_group_rules = CONF.validation.security_group_rules
- if not cls.validation_resources:
- cls.validation_resources = {
- 'keypair': keypair,
- 'security_group': security_group,
- 'security_group_rules': security_group_rules,
- 'floating_ip': floating_ip}
+ if os_clients in cls._validation_resources:
+ return cls._validation_resources[os_clients]
+
+ if (CONF.validation.ip_version_for_ssh not in (4, 6) and
+ CONF.service_available.neutron):
+ msg = "Invalid IP version %s in ip_version_for_ssh. Use 4 or 6"
+ raise lib_exc.InvalidConfiguration(
+ msg % CONF.validation.ip_version_for_ssh)
+
+ resources = vr.create_validation_resources(
+ os_clients,
+ **cls._validation_resources_params_from_conf())
+
+ cls.addClassResourceCleanup(
+ vr.clear_validation_resources, os_clients,
+ use_neutron=CONF.service_available.neutron,
+ **resources)
+ cls._validation_resources[os_clients] = resources
+ return resources
+
+ def get_test_validation_resources(self, os_clients):
+ """Returns a dict of validation resources according to configuration
+
+ Initialise a validation resources fixture based on configuration.
+ Start the fixture and returns the validation resources.
+
+ If `CONF.validation.run_validation` is False no resource will be
+ provisioned at all.
+
+ @param os_clients: Clients to be used to provision the resources.
+ """
+
+ params = {}
+ # Test will try to use the fixture, so for this to be useful
+ # we must return a fixture. If validation is disabled though
+ # we don't need to provision anything, which is the default
+ # behavior for the fixture.
+ if CONF.validation.run_validation:
+ params = self._validation_resources_params_from_conf()
+
+ validation = self.useFixture(
+ vr.ValidationResourcesFixture(os_clients, **params))
+ return validation.resources
@classmethod
def set_network_resources(cls, network=False, router=False, subnet=False,
dhcp=False):
"""Specify which network resources should be created
+ The dynamic credentials provider by default provisions network
+ resources for each user/project that is provisioned. This behavior
+ can be altered using this method, which allows tests to define which
+ specific network resources to be provisioned - none if no parameter
+ is specified.
+
+ This method is designed so that only the network resources set on the
+ leaf class are honoured.
+
+ Credentials are provisioned as part of the class setup fixture,
+ during the `setup_credentials` step. For this to be effective this
+ helper must be invoked before super's `setup_credentials` is executed.
+
@param network
@param router
@param subnet
@param dhcp
+
+ Example::
+
+ @classmethod
+ def setup_credentials(cls):
+ # Do not setup network resources for this test
+ cls.set_network_resources()
+ super(MyTest, cls).setup_credentials()
"""
- # network resources should be set only once from callers
+ # If this is invoked after the credentials are setup, it won't take
+ # any effect. To avoid this situation, fail the test in case this was
+ # invoked too late in the test lifecycle.
+ if cls.__setup_credentials_called:
+ raise RuntimeError(
+ "set_network_resources invoked after setup_credentials on the "
+ "super class has been already invoked. For "
+ "set_network_resources to have effect please invoke it before "
+ "the call to super().setup_credentials")
+
+ # Network resources should be set only once from callers
# in order to ensure that even if it's called multiple times in
# a chain of overloaded methods, the attribute is set only
- # in the leaf class
- if not cls.network_resources:
- cls.network_resources = {
+ # in the leaf class.
+ if not cls._network_resources:
+ cls._network_resources = {
'network': network,
'router': router,
'subnet': subnet,
'dhcp': dhcp}
@classmethod
- def get_tenant_network(cls):
+ def get_tenant_network(cls, credentials_type='primary'):
"""Get the network to be used in testing
+ :param credentials_type: The type of credentials for which to get the
+ tenant network
+
:return: network dict including 'id' and 'name'
"""
+ # Get a manager for the given credentials_type, but at least
+ # always fall back on getting the manager for primary credentials
+ if isinstance(credentials_type, six.string_types):
+ manager = cls.get_client_manager(credential_type=credentials_type)
+ elif isinstance(credentials_type, list):
+ manager = cls.get_client_manager(roles=credentials_type[1:])
+ else:
+ manager = cls.get_client_manager()
+
# Make sure cred_provider exists and get a network client
- networks_client = cls.get_client_manager().compute_networks_client
+ networks_client = manager.networks_client
cred_provider = cls._get_credentials_provider()
# In case of nova network, isolated tenants are not able to list the
- # network configured in fixed_network_name, even if the can use it
+ # network configured in fixed_network_name, even if they can use it
# for their servers, so using an admin network client to validate
# the network name
if (not CONF.service_available.neutron and
credentials.is_admin_available(
identity_version=cls.get_identity_version())):
admin_creds = cred_provider.get_admin_creds()
- admin_manager = clients.Manager(
- admin_creds, api_microversions=cls.services_microversion)
+ admin_manager = clients.Manager(admin_creds.credentials)
networks_client = admin_manager.compute_networks_client
return fixed_network.get_tenant_network(
cred_provider, networks_client, CONF.compute.fixed_network_name)
- def assertEmpty(self, list, msg=None):
- self.assertTrue(len(list) == 0, msg)
+ def assertEmpty(self, items, msg=None):
+ """Asserts whether a sequence or collection is empty
- def assertNotEmpty(self, list, msg=None):
- self.assertTrue(len(list) > 0, msg)
-
-
-class NegativeAutoTest(BaseTestCase):
-
- _resources = {}
-
- @classmethod
- def setUpClass(cls):
- super(NegativeAutoTest, cls).setUpClass()
- os = cls.get_client_manager(credential_type='primary')
- cls.client = os.negative_client
-
- @staticmethod
- def load_tests(*args):
- """Wrapper for testscenarios
-
- To set the mandatory scenarios variable only in case a real test
- loader is in place. Will be automatically called in case the variable
- "load_tests" is set.
+ :param items: sequence or collection to be tested
+ :param msg: message to be passed to the AssertionError
+ :raises AssertionError: when items is not empty
"""
- if getattr(args[0], 'suiteClass', None) is not None:
- loader, standard_tests, pattern = args
- else:
- standard_tests, module, loader = args
- for test in testtools.iterate_tests(standard_tests):
- schema = getattr(test, '_schema', None)
- if schema is not None:
- setattr(test, 'scenarios',
- NegativeAutoTest.generate_scenario(schema))
- return testscenarios.load_tests_apply_scenarios(*args)
+ if msg is None:
+ msg = "sequence or collection is not empty: %s" % items
+ self.assertFalse(items, msg)
- @staticmethod
- def generate_scenario(description):
- """Generates the test scenario list for a given description.
+ def assertNotEmpty(self, items, msg=None):
+ """Asserts whether a sequence or collection is not empty
- :param description: A file or dictionary with the following entries:
- name (required) name for the api
- http-method (required) one of HEAD,GET,PUT,POST,PATCH,DELETE
- url (required) the url to be appended to the catalog url with '%s'
- for each resource mentioned
- resources: (optional) A list of resource names such as "server",
- "flavor", etc. with an element for each '%s' in the url. This
- method will call self.get_resource for each element when
- constructing the positive test case template so negative
- subclasses are expected to return valid resource ids when
- appropriate.
- json-schema (optional) A valid json schema that will be used to
- create invalid data for the api calls. For "GET" and "HEAD",
- the data is used to generate query strings appended to the url,
- otherwise for the body of the http call.
+ :param items: sequence or collection to be tested
+ :param msg: message to be passed to the AssertionError
+ :raises AssertionError: when items is empty
"""
- LOG.debug(description)
- generator = importutils.import_class(
- CONF.negative.test_generator)()
- generator.validate_schema(description)
- schema = description.get("json-schema", None)
- resources = description.get("resources", [])
- scenario_list = []
- expected_result = None
- for resource in resources:
- if isinstance(resource, dict):
- expected_result = resource['expected_result']
- resource = resource['name']
- LOG.debug("Add resource to test %s" % resource)
- scn_name = "inv_res_%s" % (resource)
- scenario_list.append((scn_name, {"resource": (resource,
- str(uuid.uuid4())),
- "expected_result": expected_result
- }))
- if schema is not None:
- for scenario in generator.generate_scenarios(schema):
- scenario_list.append((scenario['_negtest_name'],
- scenario))
- LOG.debug(scenario_list)
- return scenario_list
-
- def execute(self, description):
- """Execute a http call
-
- Execute a http call on an api that are expected to
- result in client errors. First it uses invalid resources that are part
- of the url, and then invalid data for queries and http request bodies.
-
- :param description: A json file or dictionary with the following
- entries:
- name (required) name for the api
- http-method (required) one of HEAD,GET,PUT,POST,PATCH,DELETE
- url (required) the url to be appended to the catalog url with '%s'
- for each resource mentioned
- resources: (optional) A list of resource names such as "server",
- "flavor", etc. with an element for each '%s' in the url. This
- method will call self.get_resource for each element when
- constructing the positive test case template so negative
- subclasses are expected to return valid resource ids when
- appropriate.
- json-schema (optional) A valid json schema that will be used to
- create invalid data for the api calls. For "GET" and "HEAD",
- the data is used to generate query strings appended to the url,
- otherwise for the body of the http call.
-
- """
- LOG.info("Executing %s" % description["name"])
- LOG.debug(description)
- generator = importutils.import_class(
- CONF.negative.test_generator)()
- schema = description.get("json-schema", None)
- method = description["http-method"]
- url = description["url"]
- expected_result = None
- if "default_result_code" in description:
- expected_result = description["default_result_code"]
-
- resources = [self.get_resource(r) for
- r in description.get("resources", [])]
-
- if hasattr(self, "resource"):
- # Note(mkoderer): The resources list already contains an invalid
- # entry (see get_resource).
- # We just send a valid json-schema with it
- valid_schema = None
- if schema:
- valid_schema = \
- valid.ValidTestGenerator().generate_valid(schema)
- new_url, body = self._http_arguments(valid_schema, url, method)
- elif hasattr(self, "_negtest_name"):
- schema_under_test = \
- valid.ValidTestGenerator().generate_valid(schema)
- local_expected_result = \
- generator.generate_payload(self, schema_under_test)
- if local_expected_result is not None:
- expected_result = local_expected_result
- new_url, body = \
- self._http_arguments(schema_under_test, url, method)
- else:
- raise Exception("testscenarios are not active. Please make sure "
- "that your test runner supports the load_tests "
- "mechanism")
-
- if "admin_client" in description and description["admin_client"]:
- if not credentials.is_admin_available(
- identity_version=self.get_identity_version()):
- msg = ("Missing Identity Admin API credentials in"
- "configuration.")
- raise self.skipException(msg)
- creds = self.credentials_provider.get_admin_creds()
- os_adm = clients.Manager(credentials=creds)
- client = os_adm.negative_client
- else:
- client = self.client
- resp, resp_body = client.send_request(method, new_url,
- resources, body=body)
- self._check_negative_response(expected_result, resp.status, resp_body)
-
- def _http_arguments(self, json_dict, url, method):
- LOG.debug("dict: %s url: %s method: %s" % (json_dict, url, method))
- if not json_dict:
- return url, None
- elif method in ["GET", "HEAD", "PUT", "DELETE"]:
- return "%s?%s" % (url, urllib.urlencode(json_dict)), None
- else:
- return url, json.dumps(json_dict)
-
- def _check_negative_response(self, expected_result, result, body):
- self.assertTrue(result >= 400 and result < 500 and result != 413,
- "Expected client error, got %s:%s" %
- (result, body))
- self.assertTrue(expected_result is None or expected_result == result,
- "Expected %s, got %s:%s" %
- (expected_result, result, body))
-
- @classmethod
- def set_resource(cls, name, resource):
- """Register a resoruce for a test
-
- This function can be used in setUpClass context to register a resoruce
- for a test.
-
- :param name: The name of the kind of resource such as "flavor", "role",
- etc.
- :resource: The id of the resource
- """
- cls._resources[name] = resource
-
- def get_resource(self, name):
- """Return a valid uuid for a type of resource.
-
- If a real resource is needed as part of a url then this method should
- return one. Otherwise it can return None.
-
- :param name: The name of the kind of resource such as "flavor", "role",
- etc.
- """
- if isinstance(name, dict):
- name = name['name']
- if hasattr(self, "resource") and self.resource[0] == name:
- LOG.debug("Return invalid resource (%s) value: %s" %
- (self.resource[0], self.resource[1]))
- return self.resource[1]
- if name in self._resources:
- return self._resources[name]
- return None
-
-
-def SimpleNegativeAutoTest(klass):
- """This decorator registers a test function on basis of the class name."""
- @attr(type=['negative'])
- def generic_test(self):
- if hasattr(self, '_schema'):
- self.execute(self._schema)
-
- cn = klass.__name__
- cn = cn.replace('JSON', '')
- cn = cn.replace('Test', '')
- # NOTE(mkoderer): replaces uppercase chars inside the class name with '_'
- lower_cn = re.sub('(?<!^)(?=[A-Z])', '_', cn).lower()
- func_name = 'test_%s' % lower_cn
- setattr(klass, func_name, generic_test)
- return klass
-
-
-def call_until_true(func, duration, sleep_for):
- """Call the given function until it returns True (and return True)
-
- or until the specified duration (in seconds) elapses (and return False).
-
- :param func: A zero argument callable that returns True on success.
- :param duration: The number of seconds for which to attempt a
- successful call of the function.
- :param sleep_for: The number of seconds to sleep after an unsuccessful
- invocation of the function.
- """
- now = time.time()
- timeout = now + duration
- while now < timeout:
- if func():
- return True
- time.sleep(sleep_for)
- now = time.time()
- return False
+ if msg is None:
+ msg = "sequence or collection is empty."
+ self.assertTrue(items, msg)
diff --git a/tempest/test_discover/plugins.py b/tempest/test_discover/plugins.py
index 108b50d..9c18052 100644
--- a/tempest/test_discover/plugins.py
+++ b/tempest/test_discover/plugins.py
@@ -13,12 +13,13 @@
# under the License.
import abc
-import logging
+from oslo_log import log as logging
import six
import stevedore
-from tempest_lib.common.utils import misc
+from tempest.lib.common.utils import misc
+from tempest.lib.services import clients
LOG = logging.getLogger(__name__)
@@ -45,10 +46,42 @@
"""Add additional configuration options to tempest.
This method will be run for the plugin during the register_opts()
- function in tempest.config
+ function in tempest.config.
:param ConfigOpts conf: The conf object that can be used to register
additional options on.
+
+ Example::
+
+ # Config options are defined in a config.py module
+ service_option = cfg.BoolOpt(
+ "my_service",
+ default=True,
+ help="Whether or not my service is available")
+
+ # Note: as long as the group is listed in get_opt_lists,
+ # it will be possible to access its optins in the plugin code
+ # via ("-" in the group name are replaces with "_"):
+ # CONF.my_service.<option_name>
+ my_service_group = cfg.OptGroup(name="my-service",
+ title="My service options")
+
+ MyServiceGroup = [<list of options>]
+ # (...) More groups and options...
+
+ # Plugin is implemented in a plugin.py module
+ from my_plugin import config as my_config
+
+ def register_opts(self, conf):
+ conf.register_opt(my_config.service_option,
+ group='service_available')
+ conf.register_group(my_config.my_service_group)
+ conf.register_opts(my_config.MyServiceGroup,
+ my_config.my_service_group)
+
+ conf.register_group(my_config.my_service_feature_group)
+ conf.register_opts(my_config.MyServiceFeaturesGroup,
+ my_config.my_service_feature_group)
"""
return
@@ -59,6 +92,82 @@
:return option_list: A list of tuples with the group name and options
in that group.
:rtype: list
+
+ Example::
+
+ # Config options are defined in a config.py module
+ service_option = cfg.BoolOpt(
+ "my_service", default=True,
+ help="Whether or not my service is available")
+
+ my_service_group = cfg.OptGroup(name="my-service",
+ title="My service options")
+ my_service_features_group = cfg.OptGroup(
+ name="my-service-features",
+ title="My service available features")
+
+ MyServiceGroup = [<list of options>]
+ MyServiceFeaturesGroup = [<list of options>]
+
+ # Plugin is implemented in a plugin.py module
+ from my_plugin import config as my_config
+
+ def get_opt_lists(self, conf):
+ return [
+ (my_service_group.name, MyServiceGroup),
+ (my_service_features_group.name, MyServiceFeaturesGroup)
+ ]
+ """
+ return []
+
+ def get_service_clients(self):
+ """Get a list of the service clients for registration
+
+ If the plugin implements service clients for one or more APIs, it
+ may return their details by this method for automatic registration
+ in any ServiceClients object instantiated by tests.
+ The default implementation returns an empty list.
+
+ :returns: Each element of the list represents the service client for an
+ API. Each dict must define all parameters required for the invocation
+ of `service_clients.ServiceClients.register_service_client_module`.
+ :rtype: list of dictionaries
+
+ Example implementation with one service client::
+
+ def get_service_clients(self):
+ # Example implementation with one service client
+ myservice_config = config.service_client_config('myservice')
+ params = {
+ 'name': 'myservice',
+ 'service_version': 'myservice',
+ 'module_path': 'myservice_tempest_tests.services',
+ 'client_names': ['API1Client', 'API2Client'],
+ }
+ params.update(myservice_config)
+ return [params]
+
+ Example implementation with two service clients::
+
+ def get_service_clients(self):
+ # Example implementation with two service clients
+ foo1_config = config.service_client_config('foo')
+ params_foo1 = {
+ 'name': 'foo_v1',
+ 'service_version': 'foo.v1',
+ 'module_path': 'bar_tempest_tests.services.foo.v1',
+ 'client_names': ['API1Client', 'API2Client'],
+ }
+ params_foo1.update(foo_config)
+ foo2_config = config.service_client_config('foo')
+ params_foo2 = {
+ 'name': 'foo_v2',
+ 'service_version': 'foo.v2',
+ 'module_path': 'bar_tempest_tests.services.foo.v2',
+ 'client_names': ['API1Client', 'API2Client'],
+ }
+ params_foo2.update(foo2_config)
+ return [params_foo1, params_foo2]
"""
return []
@@ -93,7 +202,7 @@
plug.obj.register_opts(conf)
except Exception:
LOG.exception('Plugin %s raised an exception trying to run '
- 'register_opts' % plug.name)
+ 'register_opts', plug.name)
def get_plugin_options_list(self):
plugin_options = []
@@ -102,3 +211,15 @@
if opt_list:
plugin_options.extend(opt_list)
return plugin_options
+
+ def _register_service_clients(self):
+ registry = clients.ClientsRegistry()
+ for plug in self.ext_plugins:
+ try:
+ service_clients = plug.obj.get_service_clients()
+ if service_clients:
+ registry.register_service_client(
+ plug.name, service_clients)
+ except Exception:
+ LOG.exception('Plugin %s raised an exception trying to run '
+ 'get_service_clients', plug.name)
diff --git a/tempest/tests/README.rst b/tempest/tests/README.rst
index e54d4c0..0587e7b 100644
--- a/tempest/tests/README.rst
+++ b/tempest/tests/README.rst
@@ -7,16 +7,16 @@
---------------------
Unit tests are the self checks for Tempest. They provide functional
-verification and regression checking for the internal components of tempest.
-They should be used to just verify that the individual pieces of tempest are
+verification and regression checking for the internal components of Tempest.
+They should be used to just verify that the individual pieces of Tempest are
working as expected. They should not require an external service to be running
-and should be able to run solely from the tempest tree.
+and should be able to run solely from the Tempest tree.
-Why are these tests in tempest?
+Why are these tests in Tempest?
-------------------------------
These tests exist to make sure that the mechanisms that we use inside of
-tempest to are valid and remain functional. They are only here for self
-validation of tempest.
+Tempest are valid and remain functional. They are only here for self
+validation of Tempest.
Scope of these tests
diff --git a/tempest/tests/stress/__init__.py b/tempest/tests/api/__init__.py
similarity index 100%
rename from tempest/tests/stress/__init__.py
rename to tempest/tests/api/__init__.py
diff --git a/tempest/tests/services/compute/__init__.py b/tempest/tests/api/compute/__init__.py
similarity index 100%
copy from tempest/tests/services/compute/__init__.py
copy to tempest/tests/api/compute/__init__.py
diff --git a/tempest/tests/api/compute/test_base.py b/tempest/tests/api/compute/test_base.py
new file mode 100644
index 0000000..47f4ad6
--- /dev/null
+++ b/tempest/tests/api/compute/test_base.py
@@ -0,0 +1,192 @@
+# Copyright 2017 IBM Corp.
+#
+# 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 mock
+
+from oslo_utils import uuidutils
+import six
+
+from tempest.api.compute import base as compute_base
+from tempest.common import waiters
+from tempest import exceptions
+from tempest.lib import exceptions as lib_exc
+from tempest.tests import base
+
+
+class TestBaseV2ComputeTest(base.TestCase):
+ """Unit tests for utility functions in BaseV2ComputeTest."""
+
+ @mock.patch.multiple(compute_base.BaseV2ComputeTest,
+ compute_images_client=mock.DEFAULT,
+ images=[], create=True)
+ def test_create_image_from_server_no_wait(self, compute_images_client):
+ """Tests create_image_from_server without the wait_until kwarg."""
+ # setup mocks
+ image_id = uuidutils.generate_uuid()
+ fake_image = mock.Mock(response={'location': image_id})
+ compute_images_client.create_image.return_value = fake_image
+ # call the utility method
+ cleanup_path = 'tempest.test.BaseTestCase.addClassResourceCleanup'
+ with mock.patch(cleanup_path) as mock_cleanup:
+ image = compute_base.BaseV2ComputeTest.create_image_from_server(
+ mock.sentinel.server_id, name='fake-snapshot-name')
+ self.assertEqual(fake_image, image)
+ # make our assertions
+ compute_images_client.create_image.assert_called_once_with(
+ mock.sentinel.server_id, name='fake-snapshot-name')
+ mock_cleanup.assert_called_once()
+ self.assertIn(image_id, mock_cleanup.call_args[0])
+
+ @mock.patch.multiple(compute_base.BaseV2ComputeTest,
+ compute_images_client=mock.DEFAULT,
+ servers_client=mock.DEFAULT,
+ images=[], create=True)
+ @mock.patch.object(waiters, 'wait_for_image_status')
+ @mock.patch.object(waiters, 'wait_for_server_status')
+ def test_create_image_from_server_wait_until_active(self,
+ wait_for_server_status,
+ wait_for_image_status,
+ servers_client,
+ compute_images_client):
+ """Tests create_image_from_server with wait_until='ACTIVE' kwarg."""
+ # setup mocks
+ image_id = uuidutils.generate_uuid()
+ fake_image = mock.Mock(response={'location': image_id})
+ compute_images_client.create_image.return_value = fake_image
+ compute_images_client.show_image.return_value = (
+ {'image': fake_image})
+ # call the utility method
+ image = compute_base.BaseV2ComputeTest.create_image_from_server(
+ mock.sentinel.server_id, wait_until='ACTIVE')
+ self.assertEqual(fake_image, image)
+ # make our assertions
+ wait_for_image_status.assert_called_once_with(
+ compute_images_client, image_id, 'ACTIVE')
+ wait_for_server_status.assert_called_once_with(
+ servers_client, mock.sentinel.server_id, 'ACTIVE')
+ compute_images_client.show_image.assert_called_once_with(image_id)
+
+ @mock.patch.multiple(compute_base.BaseV2ComputeTest,
+ compute_images_client=mock.DEFAULT,
+ servers_client=mock.DEFAULT,
+ images=[], create=True)
+ @mock.patch.object(waiters, 'wait_for_image_status')
+ @mock.patch.object(waiters, 'wait_for_server_status')
+ def test_create_image_from_server_wait_until_active_no_server_wait(
+ self, wait_for_server_status, wait_for_image_status,
+ servers_client, compute_images_client):
+ """Tests create_image_from_server with wait_until='ACTIVE' kwarg."""
+ # setup mocks
+ image_id = uuidutils.generate_uuid()
+ fake_image = mock.Mock(response={'location': image_id})
+ compute_images_client.create_image.return_value = fake_image
+ compute_images_client.show_image.return_value = (
+ {'image': fake_image})
+ # call the utility method
+ image = compute_base.BaseV2ComputeTest.create_image_from_server(
+ mock.sentinel.server_id, wait_until='ACTIVE',
+ wait_for_server=False)
+ self.assertEqual(fake_image, image)
+ # make our assertions
+ wait_for_image_status.assert_called_once_with(
+ compute_images_client, image_id, 'ACTIVE')
+ self.assertEqual(0, wait_for_server_status.call_count)
+ compute_images_client.show_image.assert_called_once_with(image_id)
+
+ @mock.patch.multiple(compute_base.BaseV2ComputeTest,
+ compute_images_client=mock.DEFAULT,
+ servers_client=mock.DEFAULT,
+ images=[], create=True)
+ @mock.patch.object(waiters, 'wait_for_image_status',
+ side_effect=lib_exc.NotFound)
+ def _test_create_image_from_server_wait_until_active_not_found(
+ self, wait_for_image_status, compute_images_client,
+ servers_client, fault=None):
+ # setup mocks
+ image_id = uuidutils.generate_uuid()
+ fake_image = mock.Mock(response={'location': image_id})
+ compute_images_client.create_image.return_value = fake_image
+ fake_server = {'id': mock.sentinel.server_id}
+ if fault:
+ fake_server['fault'] = fault
+ servers_client.show_server.return_value = {'server': fake_server}
+ # call the utility method
+ ex = self.assertRaises(
+ exceptions.SnapshotNotFoundException,
+ compute_base.BaseV2ComputeTest.create_image_from_server,
+ mock.sentinel.server_id, wait_until='active')
+ # make our assertions
+ if fault:
+ self.assertIn(fault, six.text_type(ex))
+ else:
+ self.assertNotIn(fault, six.text_type(ex))
+ wait_for_image_status.assert_called_once_with(
+ compute_images_client, image_id, 'active')
+ servers_client.show_server.assert_called_once_with(
+ mock.sentinel.server_id)
+
+ def test_create_image_from_server_wait_until_active_not_found_no_fault(
+ self):
+ # Tests create_image_from_server with wait_until='active' kwarg and
+ # the a 404 is raised while waiting for the image status to change. In
+ # this test the server does not have a fault associated with it.
+ self._test_create_image_from_server_wait_until_active_not_found()
+
+ def test_create_image_from_server_wait_until_active_not_found_with_fault(
+ self):
+ # Tests create_image_from_server with wait_until='active' kwarg and
+ # the a 404 is raised while waiting for the image status to change. In
+ # this test the server has a fault associated with it.
+ self._test_create_image_from_server_wait_until_active_not_found(
+ fault='Lost connection to hypervisor!')
+
+ @mock.patch.multiple(compute_base.BaseV2ComputeTest,
+ compute_images_client=mock.DEFAULT,
+ images=[], create=True)
+ @mock.patch.object(waiters, 'wait_for_image_status',
+ side_effect=lib_exc.NotFound)
+ def test_create_image_from_server_wait_until_saving_not_found(
+ self, wait_for_image_status, compute_images_client):
+ # Tests create_image_from_server with wait_until='SAVING' kwarg and
+ # the a 404 is raised while waiting for the image status to change. In
+ # this case we do not get the server details and just re-raise the 404.
+ # setup mocks
+ image_id = uuidutils.generate_uuid()
+ fake_image = mock.Mock(response={'location': image_id})
+ compute_images_client.create_image.return_value = fake_image
+ # call the utility method
+ self.assertRaises(
+ lib_exc.NotFound,
+ compute_base.BaseV2ComputeTest.create_image_from_server,
+ mock.sentinel.server_id, wait_until='SAVING')
+ # 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 fe9268e..0b53b45 100644
--- a/tempest/tests/base.py
+++ b/tempest/tests/base.py
@@ -14,18 +14,11 @@
import mock
from oslotest import base
-from oslotest import moxstubout
class TestCase(base.BaseTestCase):
- def setUp(self):
- super(TestCase, self).setUp()
- mox_fixture = self.useFixture(moxstubout.MoxStubout())
- self.mox = mox_fixture.mox
- self.stubs = mox_fixture.stubs
-
- 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.
@@ -34,11 +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, *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, *args, **kwargs)
m = p.start()
self.addCleanup(p.stop)
return m
diff --git a/tempest/tests/cmd/sample_streams/calls.subunit b/tempest/tests/cmd/sample_streams/calls.subunit
new file mode 100644
index 0000000..d5b4790
--- /dev/null
+++ b/tempest/tests/cmd/sample_streams/calls.subunit
Binary files differ
diff --git a/tempest/tests/cmd/test_account_generator.py b/tempest/tests/cmd/test_account_generator.py
new file mode 100644
index 0000000..fd9af08
--- /dev/null
+++ b/tempest/tests/cmd/test_account_generator.py
@@ -0,0 +1,322 @@
+# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P.
+#
+# 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 fixtures
+import mock
+from oslo_config import cfg
+
+from tempest.cmd import account_generator
+from tempest import config
+from tempest.tests import base
+from tempest.tests import fake_config
+
+
+class FakeOpts(object):
+
+ def __init__(self, version=3):
+ self.os_username = 'fake_user'
+ self.os_password = 'fake_password'
+ self.os_project_name = 'fake_project_name'
+ self.os_tenant_name = None
+ self.os_domain_name = 'fake_domain'
+ self.tag = 'fake'
+ self.concurrency = 2
+ self.with_admin = True
+ self.identity_version = version
+ self.accounts = 'fake_accounts.yml'
+
+
+class MockHelpersMixin(object):
+
+ def mock_config_and_opts(self, identity_version):
+ self.useFixture(fake_config.ConfigFixture())
+ self.patchobject(config, 'TempestConfigPrivate',
+ fake_config.FakePrivate)
+ self.opts = FakeOpts(version=identity_version)
+ self.patch('oslo_log.log.setup', autospec=True)
+
+ def mock_resource_creation(self):
+ fake_resource = dict(id='id', name='name')
+ self.user_create_fixture = self.useFixture(fixtures.MockPatch(
+ self.cred_client + '.create_user', return_value=fake_resource))
+ self.useFixture(fixtures.MockPatch(
+ self.cred_client + '.create_project',
+ return_value=fake_resource))
+ self.useFixture(fixtures.MockPatch(
+ self.cred_client + '.assign_user_role'))
+ self.useFixture(fixtures.MockPatch(
+ self.cred_client + '._check_role_exists',
+ return_value=fake_resource))
+ self.useFixture(fixtures.MockPatch(
+ self.dynamic_creds + '._create_network',
+ return_value=fake_resource))
+ self.useFixture(fixtures.MockPatch(
+ self.dynamic_creds + '._create_subnet',
+ return_value=fake_resource))
+ self.useFixture(fixtures.MockPatch(
+ self.dynamic_creds + '._create_router',
+ return_value=fake_resource))
+ self.useFixture(fixtures.MockPatch(
+ self.dynamic_creds + '._add_router_interface',
+ return_value=fake_resource))
+
+ def mock_domains(self):
+ fake_domain_list = {'domains': [{'id': 'fake_domain',
+ 'name': 'Fake_Domain'}]}
+ self.useFixture(fixtures.MockPatch(''.join([
+ 'tempest.lib.services.identity.v3.domains_client.'
+ 'DomainsClient.list_domains']),
+ return_value=fake_domain_list))
+ self.useFixture(fixtures.MockPatch(
+ self.cred_client + '.assign_user_role_on_domain'))
+
+
+class TestAccountGeneratorV2(base.TestCase, MockHelpersMixin):
+
+ identity_version = 2
+
+ def setUp(self):
+ super(TestAccountGeneratorV2, self).setUp()
+ self.mock_config_and_opts(self.identity_version)
+
+ def test_get_credential_provider(self):
+ cp = account_generator.get_credential_provider(self.opts)
+ admin_creds = cp.default_admin_creds
+ self.assertEqual(self.opts.tag, cp.name)
+ self.assertIn(str(self.opts.identity_version), cp.identity_version)
+ self.assertEqual(self.opts.os_username, admin_creds.username)
+ self.assertEqual(self.opts.os_project_name, admin_creds.tenant_name)
+ self.assertEqual(self.opts.os_password, admin_creds.password)
+ self.assertFalse(hasattr(admin_creds, 'domain_name'))
+
+ def test_get_credential_provider_with_tenant(self):
+ self.opts.os_project_name = None
+ self.opts.os_tenant_name = 'fake_tenant'
+ cp = account_generator.get_credential_provider(self.opts)
+ admin_creds = cp.default_admin_creds
+ self.assertEqual(self.opts.os_tenant_name, admin_creds.tenant_name)
+
+
+class TestAccountGeneratorV3(TestAccountGeneratorV2):
+
+ identity_version = 3
+
+ def setUp(self):
+ super(TestAccountGeneratorV3, self).setUp()
+ fake_domain_list = {'domains': [{'id': 'fake_domain'}]}
+ self.useFixture(fixtures.MockPatch(''.join([
+ 'tempest.lib.services.identity.v3.domains_client.'
+ 'DomainsClient.list_domains']),
+ return_value=fake_domain_list))
+
+ def test_get_credential_provider(self):
+ cp = account_generator.get_credential_provider(self.opts)
+ admin_creds = cp.default_admin_creds
+ self.assertEqual(self.opts.tag, cp.name)
+ self.assertIn(str(self.opts.identity_version), cp.identity_version)
+ self.assertEqual(self.opts.os_username, admin_creds.username)
+ self.assertEqual(self.opts.os_project_name, admin_creds.tenant_name)
+ self.assertEqual(self.opts.os_password, admin_creds.password)
+ self.assertEqual(self.opts.os_domain_name, admin_creds.domain_name)
+
+ def test_get_credential_provider_without_domain(self):
+ self.opts.os_domain_name = None
+ cp = account_generator.get_credential_provider(self.opts)
+ admin_creds = cp.default_admin_creds
+ self.assertIsNotNone(admin_creds.domain_name)
+
+
+class TestGenerateResourcesV2(base.TestCase, MockHelpersMixin):
+
+ identity_version = 2
+ cred_client = 'tempest.lib.common.cred_client.V2CredsClient'
+ dynamic_creds = ('tempest.lib.common.dynamic_creds.'
+ 'DynamicCredentialProvider')
+
+ def setUp(self):
+ super(TestGenerateResourcesV2, self).setUp()
+ self.mock_config_and_opts(self.identity_version)
+ self.cred_provider = account_generator.get_credential_provider(
+ self.opts)
+ self.mock_resource_creation()
+
+ def test_generate_resources_no_admin(self):
+ cfg.CONF.set_default('swift', False, group='service_available')
+ cfg.CONF.set_default('operator_role', 'fake_operator',
+ group='object-storage')
+ cfg.CONF.set_default('reseller_admin_role', 'fake_reseller',
+ group='object-storage')
+ resources = account_generator.generate_resources(
+ self.cred_provider, admin=False)
+ resource_types = [k for k, _ in resources]
+ # No admin, no swift, expect two credentials only
+ self.assertEqual(2, len(resources))
+ # Ensure create_user was invoked twice (two distinct users)
+ self.assertEqual(2, self.user_create_fixture.mock.call_count)
+ self.assertIn('primary', resource_types)
+ self.assertIn('alt', resource_types)
+ self.assertNotIn('admin', resource_types)
+ self.assertNotIn(['fake_operator'], resource_types)
+ self.assertNotIn(['fake_reseller'], resource_types)
+ self.assertNotIn(['fake_owner'], resource_types)
+ for resource in resources:
+ self.assertIsNotNone(resource[1].network)
+ self.assertIsNotNone(resource[1].router)
+ self.assertIsNotNone(resource[1].subnet)
+
+ def test_generate_resources_admin(self):
+ cfg.CONF.set_default('swift', False, group='service_available')
+ cfg.CONF.set_default('operator_role', 'fake_operator',
+ group='object-storage')
+ cfg.CONF.set_default('reseller_admin_role', 'fake_reseller',
+ group='object-storage')
+ resources = account_generator.generate_resources(
+ self.cred_provider, admin=True)
+ resource_types = [k for k, _ in resources]
+ # Admin, no swift, expect three credentials only
+ self.assertEqual(3, len(resources))
+ # Ensure create_user was invoked 3 times (3 distinct users)
+ self.assertEqual(3, self.user_create_fixture.mock.call_count)
+ self.assertIn('primary', resource_types)
+ self.assertIn('alt', resource_types)
+ self.assertIn('admin', resource_types)
+ self.assertNotIn(['fake_operator'], resource_types)
+ self.assertNotIn(['fake_reseller'], resource_types)
+ self.assertNotIn(['fake_owner'], resource_types)
+ for resource in resources:
+ self.assertIsNotNone(resource[1].network)
+ self.assertIsNotNone(resource[1].router)
+ self.assertIsNotNone(resource[1].subnet)
+
+ def test_generate_resources_swift_admin(self):
+ cfg.CONF.set_default('swift', True, group='service_available')
+ cfg.CONF.set_default('operator_role', 'fake_operator',
+ group='object-storage')
+ cfg.CONF.set_default('reseller_admin_role', 'fake_reseller',
+ group='object-storage')
+ resources = account_generator.generate_resources(
+ self.cred_provider, admin=True)
+ resource_types = [k for k, _ in resources]
+ # all options on, expect six credentials
+ self.assertEqual(6, len(resources))
+ # Ensure create_user was invoked 6 times (6 distinct users)
+ self.assertEqual(5, self.user_create_fixture.mock.call_count)
+ self.assertIn('primary', resource_types)
+ self.assertIn('alt', resource_types)
+ self.assertIn('admin', resource_types)
+ self.assertIn(['fake_operator'], resource_types)
+ self.assertIn(['fake_reseller'], resource_types)
+ for resource in resources:
+ self.assertIsNotNone(resource[1].network)
+ self.assertIsNotNone(resource[1].router)
+ self.assertIsNotNone(resource[1].subnet)
+
+
+class TestGenerateResourcesV3(TestGenerateResourcesV2):
+
+ identity_version = 3
+ cred_client = 'tempest.lib.common.cred_client.V3CredsClient'
+
+ def setUp(self):
+ self.mock_domains()
+ super(TestGenerateResourcesV3, self).setUp()
+
+
+class TestDumpAccountsV2(base.TestCase, MockHelpersMixin):
+
+ identity_version = 2
+ cred_client = 'tempest.lib.common.cred_client.V2CredsClient'
+ dynamic_creds = ('tempest.lib.common.dynamic_creds.'
+ 'DynamicCredentialProvider')
+ domain_is_in = False
+
+ def setUp(self):
+ super(TestDumpAccountsV2, self).setUp()
+ self.mock_config_and_opts(self.identity_version)
+ self.cred_provider = account_generator.get_credential_provider(
+ self.opts)
+ self.mock_resource_creation()
+ cfg.CONF.set_default('swift', True, group='service_available')
+ self.resources = account_generator.generate_resources(
+ self.cred_provider, admin=True)
+
+ def test_dump_accounts(self):
+ self.useFixture(fixtures.MockPatch('os.path.exists',
+ return_value=False))
+ mocked_open = mock.mock_open()
+ with mock.patch('{}.open'.format(account_generator.__name__),
+ mocked_open, create=True):
+ with mock.patch('yaml.safe_dump') as yaml_dump_mock:
+ account_generator.setup_logging()
+ account_generator.dump_accounts(self.resources,
+ self.opts.identity_version,
+ self.opts.accounts)
+ mocked_open.assert_called_once_with(self.opts.accounts, 'w')
+ handle = mocked_open()
+ # Ordered args in [0], keyword args in [1]
+ accounts, f = yaml_dump_mock.call_args[0]
+ self.assertEqual(handle, f)
+ self.assertEqual(6, len(accounts))
+ if self.domain_is_in:
+ self.assertIn('domain_name', accounts[0].keys())
+ else:
+ self.assertNotIn('domain_name', accounts[0].keys())
+ self.assertEqual(1, len([x for x in accounts if
+ x.get('types') == ['admin']]))
+ self.assertEqual(3, len([x for x in accounts if 'roles' in x]))
+ for account in accounts:
+ self.assertIn('resources', account)
+ self.assertIn('network', account.get('resources'))
+
+ def test_dump_accounts_existing_file(self):
+ self.useFixture(fixtures.MockPatch('os.path.exists',
+ return_value=True))
+ rename_mock = self.useFixture(fixtures.MockPatch('os.rename')).mock
+ backup_file = '.'.join((self.opts.accounts, 'bak'))
+ mocked_open = mock.mock_open()
+ with mock.patch('{}.open'.format(account_generator.__name__),
+ mocked_open, create=True):
+ with mock.patch('yaml.safe_dump') as yaml_dump_mock:
+ account_generator.setup_logging()
+ account_generator.dump_accounts(self.resources,
+ self.opts.identity_version,
+ self.opts.accounts)
+ rename_mock.assert_called_once_with(self.opts.accounts, backup_file)
+ mocked_open.assert_called_once_with(self.opts.accounts, 'w')
+ handle = mocked_open()
+ # Ordered args in [0], keyword args in [1]
+ accounts, f = yaml_dump_mock.call_args[0]
+ self.assertEqual(handle, f)
+ self.assertEqual(6, len(accounts))
+ if self.domain_is_in:
+ self.assertIn('domain_name', accounts[0].keys())
+ else:
+ self.assertNotIn('domain_name', accounts[0].keys())
+ self.assertEqual(1, len([x for x in accounts if
+ x.get('types') == ['admin']]))
+ self.assertEqual(3, len([x for x in accounts if 'roles' in x]))
+ for account in accounts:
+ self.assertIn('resources', account)
+ self.assertIn('network', account.get('resources'))
+
+
+class TestDumpAccountsV3(TestDumpAccountsV2):
+
+ identity_version = 3
+ cred_client = 'tempest.lib.common.cred_client.V3CredsClient'
+ domain_is_in = True
+
+ def setUp(self):
+ self.mock_domains()
+ super(TestDumpAccountsV3, self).setUp()
diff --git a/tempest/tests/cmd/test_javelin.py b/tempest/tests/cmd/test_javelin.py
deleted file mode 100644
index 56bc96c..0000000
--- a/tempest/tests/cmd/test_javelin.py
+++ /dev/null
@@ -1,422 +0,0 @@
-#!/usr/bin/env python
-#
-# 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 mock
-from oslotest import mockpatch
-from tempest_lib import exceptions as lib_exc
-
-from tempest.cmd import javelin
-from tempest.tests import base
-
-
-class JavelinUnitTest(base.TestCase):
-
- def setUp(self):
- super(JavelinUnitTest, self).setUp()
- javelin.setup_logging()
- self.fake_client = mock.MagicMock()
- self.fake_object = mock.MagicMock()
-
- def test_load_resources(self):
- with mock.patch('six.moves.builtins.open', mock.mock_open(),
- create=True) as open_mock:
- with mock.patch('yaml.load', mock.MagicMock(),
- create=True) as load_mock:
- javelin.load_resources(self.fake_object)
- load_mock.assert_called_once_with(open_mock(self.fake_object))
-
- def test_keystone_admin(self):
- self.useFixture(mockpatch.PatchObject(javelin, "OSClient"))
- javelin.OPTS = self.fake_object
- javelin.keystone_admin()
- javelin.OSClient.assert_called_once_with(
- self.fake_object.os_username,
- self.fake_object.os_password,
- self.fake_object.os_tenant_name)
-
- def test_client_for_user(self):
- fake_user = mock.MagicMock()
- javelin.USERS = {fake_user['name']: fake_user}
- self.useFixture(mockpatch.PatchObject(javelin, "OSClient"))
- javelin.client_for_user(fake_user['name'])
- javelin.OSClient.assert_called_once_with(
- fake_user['name'], fake_user['pass'], fake_user['tenant'])
-
- def test_client_for_non_existing_user(self):
- fake_non_existing_user = self.fake_object
- fake_user = mock.MagicMock()
- javelin.USERS = {fake_user['name']: fake_user}
- self.useFixture(mockpatch.PatchObject(javelin, "OSClient"))
- javelin.client_for_user(fake_non_existing_user['name'])
- self.assertFalse(javelin.OSClient.called)
-
- def test_attach_volumes(self):
- self.useFixture(mockpatch.PatchObject(javelin, "client_for_user",
- return_value=self.fake_client))
-
- self.useFixture(mockpatch.PatchObject(
- javelin, "_get_volume_by_name",
- return_value=self.fake_object.volume))
-
- self.useFixture(mockpatch.PatchObject(
- javelin, "_get_server_by_name",
- return_value=self.fake_object.server))
-
- javelin.attach_volumes([self.fake_object])
-
- mocked_function = self.fake_client.volumes.attach_volume
- mocked_function.assert_called_once_with(
- self.fake_object.volume['id'],
- self.fake_object.server['id'],
- self.fake_object['device'])
-
-
-class TestCreateResources(JavelinUnitTest):
- def test_create_tenants(self):
-
- self.fake_client.tenants.list_tenants.return_value = {'tenants': []}
- self.useFixture(mockpatch.PatchObject(javelin, "keystone_admin",
- return_value=self.fake_client))
-
- javelin.create_tenants([self.fake_object['name']])
-
- mocked_function = self.fake_client.tenants.create_tenant
- mocked_function.assert_called_once_with(self.fake_object['name'])
-
- def test_create_duplicate_tenant(self):
- self.fake_client.tenants.list_tenants.return_value = {'tenants': [
- {'name': self.fake_object['name']}]}
- self.useFixture(mockpatch.PatchObject(javelin, "keystone_admin",
- return_value=self.fake_client))
-
- javelin.create_tenants([self.fake_object['name']])
-
- mocked_function = self.fake_client.tenants.create_tenant
- self.assertFalse(mocked_function.called)
-
- def test_create_users(self):
- self.useFixture(mockpatch.Patch(
- 'tempest.common.identity.get_tenant_by_name',
- return_value=self.fake_object['tenant']))
- self.useFixture(mockpatch.Patch(
- 'tempest.common.identity.get_user_by_username',
- side_effect=lib_exc.NotFound("user is not found")))
- self.useFixture(mockpatch.PatchObject(javelin, "keystone_admin",
- return_value=self.fake_client))
-
- javelin.create_users([self.fake_object])
-
- fake_tenant_id = self.fake_object['tenant']['id']
- fake_email = "%s@%s" % (self.fake_object['user'], fake_tenant_id)
- mocked_function = self.fake_client.identity.create_user
- mocked_function.assert_called_once_with(self.fake_object['name'],
- self.fake_object['password'],
- fake_tenant_id,
- fake_email,
- enabled=True)
-
- def test_create_user_missing_tenant(self):
- self.useFixture(mockpatch.Patch(
- 'tempest.common.identity.get_tenant_by_name',
- side_effect=lib_exc.NotFound("tenant is not found")))
- self.useFixture(mockpatch.PatchObject(javelin, "keystone_admin",
- return_value=self.fake_client))
-
- javelin.create_users([self.fake_object])
-
- mocked_function = self.fake_client.identity.create_user
- self.assertFalse(mocked_function.called)
-
- def test_create_objects(self):
-
- self.useFixture(mockpatch.PatchObject(javelin, "client_for_user",
- return_value=self.fake_client))
- self.useFixture(mockpatch.PatchObject(javelin, "_assign_swift_role"))
- self.useFixture(mockpatch.PatchObject(javelin, "_file_contents",
- return_value=self.fake_object.content))
-
- javelin.create_objects([self.fake_object])
-
- mocked_function = self.fake_client.containers.create_container
- mocked_function.assert_called_once_with(self.fake_object['container'])
- mocked_function = self.fake_client.objects.create_object
- mocked_function.assert_called_once_with(self.fake_object['container'],
- self.fake_object['name'],
- self.fake_object.content)
-
- def test_create_images(self):
- self.fake_client.images.create_image.return_value = \
- self.fake_object['body']
-
- self.useFixture(mockpatch.PatchObject(javelin, "client_for_user",
- return_value=self.fake_client))
- self.useFixture(mockpatch.PatchObject(javelin, "_get_image_by_name",
- return_value=[]))
- self.useFixture(mockpatch.PatchObject(javelin, "_resolve_image",
- return_value=(None, None)))
-
- with mock.patch('six.moves.builtins.open', mock.mock_open(),
- create=True) as open_mock:
- javelin.create_images([self.fake_object])
-
- mocked_function = self.fake_client.images.create_image
- mocked_function.assert_called_once_with(self.fake_object['name'],
- self.fake_object['format'],
- self.fake_object['format'])
-
- mocked_function = self.fake_client.images.store_image_file
- fake_image_id = self.fake_object['body'].get('id')
- mocked_function.assert_called_once_with(fake_image_id, open_mock())
-
- def test_create_networks(self):
- self.fake_client.networks.list_networks.return_value = {
- 'networks': []}
-
- self.useFixture(mockpatch.PatchObject(javelin, "client_for_user",
- return_value=self.fake_client))
-
- javelin.create_networks([self.fake_object])
-
- mocked_function = self.fake_client.networks.create_network
- mocked_function.assert_called_once_with(name=self.fake_object['name'])
-
- def test_create_subnet(self):
-
- fake_network = self.fake_object['network']
-
- self.useFixture(mockpatch.PatchObject(javelin, "client_for_user",
- return_value=self.fake_client))
- self.useFixture(mockpatch.PatchObject(javelin, "_get_resource_by_name",
- return_value=fake_network))
-
- fake_netaddr = mock.MagicMock()
- self.useFixture(mockpatch.PatchObject(javelin, "netaddr",
- return_value=fake_netaddr))
- fake_version = javelin.netaddr.IPNetwork().version
-
- javelin.create_subnets([self.fake_object])
-
- mocked_function = self.fake_client.networks.create_subnet
- mocked_function.assert_called_once_with(network_id=fake_network['id'],
- cidr=self.fake_object['range'],
- name=self.fake_object['name'],
- ip_version=fake_version)
-
- def test_create_volumes(self):
-
- self.useFixture(mockpatch.PatchObject(javelin, "client_for_user",
- return_value=self.fake_client))
- self.useFixture(mockpatch.PatchObject(javelin, "_get_volume_by_name",
- return_value=None))
- self.fake_client.volumes.create_volume.return_value = \
- self.fake_object.body
-
- javelin.create_volumes([self.fake_object])
-
- mocked_function = self.fake_client.volumes.create_volume
- mocked_function.assert_called_once_with(
- size=self.fake_object['gb'],
- display_name=self.fake_object['name'])
- mocked_function = self.fake_client.volumes.wait_for_volume_status
- mocked_function.assert_called_once_with(
- self.fake_object.body['volume']['id'],
- 'available')
-
- def test_create_volume_existing(self):
- self.useFixture(mockpatch.PatchObject(javelin, "client_for_user",
- return_value=self.fake_client))
- self.useFixture(mockpatch.PatchObject(javelin, "_get_volume_by_name",
- return_value=self.fake_object))
- self.fake_client.volumes.create_volume.return_value = \
- self.fake_object.body
-
- javelin.create_volumes([self.fake_object])
-
- mocked_function = self.fake_client.volumes.create_volume
- self.assertFalse(mocked_function.called)
- mocked_function = self.fake_client.volumes.wait_for_volume_status
- self.assertFalse(mocked_function.called)
-
- def test_create_router(self):
-
- self.fake_client.networks.list_routers.return_value = {'routers': []}
- self.useFixture(mockpatch.PatchObject(javelin, "client_for_user",
- return_value=self.fake_client))
-
- javelin.create_routers([self.fake_object])
-
- mocked_function = self.fake_client.networks.create_router
- mocked_function.assert_called_once_with(self.fake_object['name'])
-
- def test_create_router_existing(self):
- self.fake_client.networks.list_routers.return_value = {
- 'routers': [self.fake_object]}
- self.useFixture(mockpatch.PatchObject(javelin, "client_for_user",
- return_value=self.fake_client))
-
- javelin.create_routers([self.fake_object])
-
- mocked_function = self.fake_client.networks.create_router
- self.assertFalse(mocked_function.called)
-
- def test_create_secgroup(self):
- self.useFixture(mockpatch.PatchObject(javelin, "client_for_user",
- return_value=self.fake_client))
- self.fake_client.secgroups.list_security_groups.return_value = (
- {'security_groups': []})
- self.fake_client.secgroups.create_security_group.return_value = \
- {'security_group': {'id': self.fake_object['secgroup_id']}}
-
- javelin.create_secgroups([self.fake_object])
-
- mocked_function = self.fake_client.secgroups.create_security_group
- mocked_function.assert_called_once_with(
- name=self.fake_object['name'],
- description=self.fake_object['description'])
-
-
-class TestDestroyResources(JavelinUnitTest):
-
- def test_destroy_tenants(self):
-
- fake_tenant = self.fake_object['tenant']
- fake_auth = self.fake_client
- self.useFixture(mockpatch.Patch(
- 'tempest.common.identity.get_tenant_by_name',
- return_value=fake_tenant))
- self.useFixture(mockpatch.PatchObject(javelin, "keystone_admin",
- return_value=fake_auth))
- javelin.destroy_tenants([fake_tenant])
-
- mocked_function = fake_auth.tenants.delete_tenant
- mocked_function.assert_called_once_with(fake_tenant['id'])
-
- def test_destroy_users(self):
-
- fake_user = self.fake_object['user']
- fake_tenant = self.fake_object['tenant']
-
- fake_auth = self.fake_client
- fake_auth.tenants.list_tenants.return_value = \
- {'tenants': [fake_tenant]}
- fake_auth.identity.list_users.return_value = {'users': [fake_user]}
-
- self.useFixture(mockpatch.Patch(
- 'tempest.common.identity.get_user_by_username',
- return_value=fake_user))
- self.useFixture(mockpatch.PatchObject(javelin, "keystone_admin",
- return_value=fake_auth))
-
- javelin.destroy_users([fake_user])
-
- mocked_function = fake_auth.identity.delete_user
- mocked_function.assert_called_once_with(fake_user['id'])
-
- def test_destroy_objects(self):
-
- self.fake_client.objects.delete_object.return_value = \
- {'status': "200"}, ""
- self.useFixture(mockpatch.PatchObject(javelin, "client_for_user",
- return_value=self.fake_client))
- javelin.destroy_objects([self.fake_object])
-
- mocked_function = self.fake_client.objects.delete_object
- mocked_function.asswert_called_once(self.fake_object['container'],
- self.fake_object['name'])
-
- def test_destroy_images(self):
-
- self.useFixture(mockpatch.PatchObject(javelin, "client_for_user",
- return_value=self.fake_client))
- self.useFixture(mockpatch.PatchObject(javelin, "_get_image_by_name",
- return_value=self.fake_object['image']))
-
- javelin.destroy_images([self.fake_object])
-
- mocked_function = self.fake_client.images.delete_image
- mocked_function.assert_called_once_with(
- self.fake_object['image']['id'])
-
- def test_destroy_networks(self):
-
- self.useFixture(mockpatch.PatchObject(javelin, "client_for_user",
- return_value=self.fake_client))
- self.useFixture(mockpatch.PatchObject(
- javelin, "_get_resource_by_name",
- return_value=self.fake_object['resource']))
-
- javelin.destroy_networks([self.fake_object])
-
- mocked_function = self.fake_client.networks.delete_network
- mocked_function.assert_called_once_with(
- self.fake_object['resource']['id'])
-
- def test_destroy_volumes(self):
- self.useFixture(mockpatch.PatchObject(javelin, "client_for_user",
- return_value=self.fake_client))
-
- self.useFixture(mockpatch.PatchObject(
- javelin, "_get_volume_by_name",
- return_value=self.fake_object.volume))
-
- javelin.destroy_volumes([self.fake_object])
-
- mocked_function = self.fake_client.volumes.detach_volume
- mocked_function.assert_called_once_with(self.fake_object.volume['id'])
- mocked_function = self.fake_client.volumes.delete_volume
- mocked_function.assert_called_once_with(self.fake_object.volume['id'])
-
- def test_destroy_subnets(self):
-
- self.useFixture(mockpatch.PatchObject(javelin, "client_for_user",
- return_value=self.fake_client))
- fake_subnet_id = self.fake_object['subnet_id']
- self.useFixture(mockpatch.PatchObject(javelin, "_get_resource_by_name",
- return_value={
- 'id': fake_subnet_id}))
-
- javelin.destroy_subnets([self.fake_object])
-
- mocked_function = self.fake_client.subnets.delete_subnet
- mocked_function.assert_called_once_with(fake_subnet_id)
-
- def test_destroy_routers(self):
- self.useFixture(mockpatch.PatchObject(javelin, "client_for_user",
- return_value=self.fake_client))
-
- # this function is used on 2 different occasions in the code
- def _fake_get_resource_by_name(*args):
- if args[1] == "routers":
- return {"id": self.fake_object['router_id']}
- elif args[1] == "subnets":
- return {"id": self.fake_object['subnet_id']}
- javelin._get_resource_by_name = _fake_get_resource_by_name
-
- javelin.destroy_routers([self.fake_object])
-
- mocked_function = self.fake_client.networks.delete_router
- mocked_function.assert_called_once_with(
- self.fake_object['router_id'])
-
- def test_destroy_secgroup(self):
- self.useFixture(mockpatch.PatchObject(javelin, "client_for_user",
- return_value=self.fake_client))
- fake_secgroup = {'id': self.fake_object['id']}
- self.useFixture(mockpatch.PatchObject(javelin, "_get_resource_by_name",
- return_value=fake_secgroup))
-
- javelin.destroy_secgroups([self.fake_object])
-
- mocked_function = self.fake_client.secgroups.delete_security_group
- mocked_function.assert_called_once_with(self.fake_object['id'])
diff --git a/tempest/tests/cmd/test_list_plugins.py b/tempest/tests/cmd/test_list_plugins.py
new file mode 100644
index 0000000..17ddb18
--- /dev/null
+++ b/tempest/tests/cmd/test_list_plugins.py
@@ -0,0 +1,24 @@
+# Copyright 2015 Hewlett-Packard Development Company, L.P.
+#
+# 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 subprocess
+
+from tempest.tests import base
+
+
+class TestTempestListPlugins(base.TestCase):
+ def test_run_list_plugins(self):
+ return_code = subprocess.call(
+ ['tempest', 'list-plugins'], stdout=subprocess.PIPE)
+ self.assertEqual(return_code, 0)
diff --git a/tempest/tests/cmd/test_run.py b/tempest/tests/cmd/test_run.py
new file mode 100644
index 0000000..6cc356e
--- /dev/null
+++ b/tempest/tests/cmd/test_run.py
@@ -0,0 +1,199 @@
+# Copyright 2015 Hewlett-Packard Development Company, L.P.
+#
+# 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 argparse
+import atexit
+import os
+import shutil
+import subprocess
+import tempfile
+
+import fixtures
+import mock
+import six
+
+from tempest.cmd import run
+from tempest.tests import base
+
+DEVNULL = open(os.devnull, 'wb')
+atexit.register(DEVNULL.close)
+
+
+class TestTempestRun(base.TestCase):
+
+ def setUp(self):
+ super(TestTempestRun, self).setUp()
+ self.run_cmd = run.TempestRun(None, None)
+
+ def test__build_regex_default(self):
+ args = mock.Mock(spec=argparse.Namespace)
+ setattr(args, 'smoke', False)
+ setattr(args, 'regex', '')
+ setattr(args, 'whitelist_file', None)
+ setattr(args, 'blacklist_file', None)
+ self.assertIsNone(None, self.run_cmd._build_regex(args))
+
+ def test__build_regex_smoke(self):
+ args = mock.Mock(spec=argparse.Namespace)
+ setattr(args, "smoke", True)
+ setattr(args, 'regex', '')
+ setattr(args, 'whitelist_file', None)
+ setattr(args, 'blacklist_file', None)
+ self.assertEqual(['smoke'], self.run_cmd._build_regex(args))
+
+ def test__build_regex_regex(self):
+ args = mock.Mock(spec=argparse.Namespace)
+ setattr(args, 'smoke', False)
+ setattr(args, "regex", 'i_am_a_fun_little_regex')
+ setattr(args, 'whitelist_file', None)
+ setattr(args, 'blacklist_file', None)
+ self.assertEqual(['i_am_a_fun_little_regex'],
+ self.run_cmd._build_regex(args))
+
+
+class TestRunReturnCode(base.TestCase):
+ def setUp(self):
+ super(TestRunReturnCode, self).setUp()
+ # Setup test dirs
+ self.directory = tempfile.mkdtemp(prefix='tempest-unit')
+ self.addCleanup(shutil.rmtree, self.directory)
+ self.test_dir = os.path.join(self.directory, 'tests')
+ os.mkdir(self.test_dir)
+ # Setup Test files
+ self.stestr_conf_file = os.path.join(self.directory, '.stestr.conf')
+ self.setup_cfg_file = os.path.join(self.directory, 'setup.cfg')
+ self.passing_file = os.path.join(self.test_dir, 'test_passing.py')
+ self.failing_file = os.path.join(self.test_dir, 'test_failing.py')
+ self.init_file = os.path.join(self.test_dir, '__init__.py')
+ self.setup_py = os.path.join(self.directory, 'setup.py')
+ shutil.copy('tempest/tests/files/testr-conf', self.stestr_conf_file)
+ shutil.copy('tempest/tests/files/passing-tests', self.passing_file)
+ shutil.copy('tempest/tests/files/failing-tests', self.failing_file)
+ shutil.copy('setup.py', self.setup_py)
+ shutil.copy('tempest/tests/files/setup.cfg', self.setup_cfg_file)
+ shutil.copy('tempest/tests/files/__init__.py', self.init_file)
+ # Change directory, run wrapper and check result
+ self.addCleanup(os.chdir, os.path.abspath(os.curdir))
+ os.chdir(self.directory)
+
+ def assertRunExit(self, cmd, expected):
+ p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ out, err = p.communicate()
+ msg = ("Running %s got an unexpected returncode\n"
+ "Stdout: %s\nStderr: %s" % (' '.join(cmd), out, err))
+ self.assertEqual(p.returncode, expected, msg)
+ return out, err
+
+ def test_tempest_run_passes(self):
+ self.assertRunExit(['tempest', 'run', '--regex', 'passing'], 0)
+
+ def test_tempest_run_passes_with_stestr_repository(self):
+ subprocess.call(['stestr', 'init'])
+ self.assertRunExit(['tempest', 'run', '--regex', 'passing'], 0)
+
+ def test_tempest_run_fails(self):
+ self.assertRunExit(['tempest', 'run'], 1)
+
+ def test_run_list(self):
+ subprocess.call(['stestr', 'init'])
+ out, err = self.assertRunExit(['tempest', 'run', '-l'], 0)
+ tests = out.split()
+ tests = sorted([six.text_type(x.rstrip()) for x in tests if x])
+ result = [
+ six.text_type('tests.test_failing.FakeTestClass.test_pass'),
+ six.text_type('tests.test_failing.FakeTestClass.test_pass_list'),
+ six.text_type('tests.test_passing.FakeTestClass.test_pass'),
+ six.text_type('tests.test_passing.FakeTestClass.test_pass_list'),
+ ]
+ # NOTE(mtreinish): on python 3 the subprocess prints b'' around
+ # stdout.
+ if six.PY3:
+ result = ["b\'" + x + "\'" for x in result]
+ self.assertEqual(result, tests)
+
+ def test_tempest_run_with_whitelist(self):
+ fd, path = tempfile.mkstemp()
+ self.addCleanup(os.remove, path)
+ whitelist_file = os.fdopen(fd, 'wb', 0)
+ self.addCleanup(whitelist_file.close)
+ whitelist_file.write('passing'.encode('utf-8'))
+ self.assertRunExit(['tempest', 'run', '--whitelist-file=%s' % path], 0)
+
+ def test_tempest_run_with_whitelist_with_regex(self):
+ fd, path = tempfile.mkstemp()
+ self.addCleanup(os.remove, path)
+ whitelist_file = os.fdopen(fd, 'wb', 0)
+ self.addCleanup(whitelist_file.close)
+ whitelist_file.write('passing'.encode('utf-8'))
+ self.assertRunExit(['tempest', 'run', '--whitelist-file=%s' % path,
+ '--regex', 'fail'], 1)
+
+ def test_tempest_run_passes_with_config_file(self):
+ self.assertRunExit(['tempest', 'run',
+ '--config-file', self.stestr_conf_file,
+ '--regex', 'passing'], 0)
+
+
+class TestTakeAction(base.TestCase):
+ def test_workspace_not_registered(self):
+ class Exception_(Exception):
+ pass
+
+ m_exit = self.useFixture(fixtures.MockPatch('sys.exit')).mock
+ # sys.exit must not continue (or exit)
+ m_exit.side_effect = Exception_
+
+ workspace = self.getUniqueString()
+
+ tempest_run = run.TempestRun(app=mock.Mock(), app_args=mock.Mock())
+ parsed_args = mock.Mock()
+ parsed_args.config_file = []
+
+ # Override $HOME so that empty workspace gets created in temp dir.
+ self.useFixture(fixtures.TempHomeDir())
+
+ # Force use of the temporary home directory.
+ parsed_args.workspace_path = None
+
+ # Simulate --workspace argument.
+ parsed_args.workspace = workspace
+
+ self.assertRaises(Exception_, tempest_run.take_action, parsed_args)
+ exit_msg = m_exit.call_args[0][0]
+ self.assertIn(workspace, exit_msg)
+
+ def test_config_file_specified(self):
+ # Setup test dirs
+ self.directory = tempfile.mkdtemp(prefix='tempest-unit')
+ self.addCleanup(shutil.rmtree, self.directory)
+ self.test_dir = os.path.join(self.directory, 'tests')
+ os.mkdir(self.test_dir)
+ # Change directory, run wrapper and check result
+ self.addCleanup(os.chdir, os.path.abspath(os.curdir))
+ os.chdir(self.directory)
+
+ tempest_run = run.TempestRun(app=mock.Mock(), app_args=mock.Mock())
+ parsed_args = mock.Mock()
+ parsed_args.config_file = []
+
+ parsed_args.workspace = None
+ parsed_args.state = None
+ parsed_args.list_tests = False
+ parsed_args.config_file = '.stestr.conf'
+
+ with mock.patch('stestr.commands.run_command') as m:
+ m.return_value = 0
+ self.assertEqual(0, tempest_run.take_action(parsed_args))
+ m.assert_called()
diff --git a/tempest/tests/cmd/test_subunit_describe_calls.py b/tempest/tests/cmd/test_subunit_describe_calls.py
new file mode 100644
index 0000000..cb34ba6
--- /dev/null
+++ b/tempest/tests/cmd/test_subunit_describe_calls.py
@@ -0,0 +1,233 @@
+# Copyright 2016 Rackspace
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import os
+import subprocess
+import tempfile
+
+from tempest.cmd import subunit_describe_calls
+from tempest.tests import base
+
+
+class TestSubunitDescribeCalls(base.TestCase):
+ def test_return_code(self):
+ subunit_file = os.path.join(
+ os.path.dirname(os.path.abspath(__file__)),
+ 'sample_streams/calls.subunit')
+ p = subprocess.Popen([
+ 'subunit-describe-calls', '-s', subunit_file,
+ '-o', tempfile.mkstemp()[1]], stdin=subprocess.PIPE)
+ p.communicate()
+ self.assertEqual(0, p.returncode)
+
+ def test_verbose(self):
+ subunit_file = os.path.join(
+ os.path.dirname(os.path.abspath(__file__)),
+ 'sample_streams/calls.subunit')
+ p = subprocess.Popen([
+ 'subunit-describe-calls', '-s', subunit_file,
+ '-v'], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
+ stdout = p.communicate()
+ self.assertEqual(0, p.returncode)
+ self.assertIn(b'- request headers:', stdout[0])
+ self.assertIn(b'- request body:', stdout[0])
+ self.assertIn(b'- response headers:', stdout[0])
+ self.assertIn(b'- response body:', stdout[0])
+
+ def test_return_code_no_output(self):
+ subunit_file = os.path.join(
+ os.path.dirname(os.path.abspath(__file__)),
+ 'sample_streams/calls.subunit')
+ p = subprocess.Popen([
+ 'subunit-describe-calls', '-s', subunit_file],
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE)
+ stdout = p.communicate()
+ self.assertEqual(0, p.returncode)
+ self.assertIn(b'foo', stdout[0])
+ self.assertIn(b'- 200 POST request for Nova to v2.1/<id>/',
+ stdout[0])
+ self.assertIn(b'- 200 DELETE request for Nova to v2.1/<id>/',
+ stdout[0])
+ self.assertIn(b'- 200 GET request for Nova to v2.1/<id>/',
+ stdout[0])
+ self.assertIn(b'- 404 DELETE request for Nova to v2.1/<id>/',
+ stdout[0])
+ self.assertNotIn(b'- request headers:', stdout[0])
+ self.assertNotIn(b'- request body:', stdout[0])
+ self.assertNotIn(b'- response headers:', stdout[0])
+ self.assertNotIn(b'- response body:', stdout[0])
+
+ def test_parse(self):
+ subunit_file = os.path.join(
+ os.path.dirname(os.path.abspath(__file__)),
+ 'sample_streams/calls.subunit')
+ parser = subunit_describe_calls.parse(
+ open(subunit_file), "pythonlogging", None)
+ expected_result = {
+ 'bar': [{
+ 'name': 'AgentsAdminTestJSON:setUp',
+ 'request_body': '{"agent": {"url": "xxx://xxxx/xxx/xxx", '
+ '"hypervisor": "common", "md5hash": '
+ '"add6bb58e139be103324d04d82d8f545", "version": "7.0", '
+ '"architecture": "tempest-x86_64-424013832", "os": "linux"}}',
+ 'request_headers': "{'Content-Type': 'application/json', "
+ "'Accept': 'application/json', 'X-Auth-Token': '<omitted>'}",
+ 'response_body': '{"agent": {"url": "xxx://xxxx/xxx/xxx", '
+ '"hypervisor": "common", "md5hash": '
+ '"add6bb58e139be103324d04d82d8f545", "version": "7.0", '
+ '"architecture": "tempest-x86_64-424013832", "os": "linux", '
+ '"agent_id": 1}}',
+ 'response_headers': "{'status': '200', 'content-length': "
+ "'203', 'x-compute-request-id': "
+ "'req-25ddaae2-0ef1-40d1-8228-59bd64a7e75b', 'vary': "
+ "'X-OpenStack-Nova-API-Version', 'connection': 'close', "
+ "'x-openstack-nova-api-version': '2.1', 'date': "
+ "'Tue, 02 Feb 2016 03:27:00 GMT', 'content-type': "
+ "'application/json'}",
+ 'service': 'Nova',
+ 'status_code': '200',
+ 'url': 'v2.1/<id>/os-agents',
+ 'verb': 'POST'}, {
+ 'name': 'AgentsAdminTestJSON:test_create_agent',
+ 'request_body': '{"agent": {"url": "xxx://xxxx/xxx/xxx", '
+ '"hypervisor": "kvm", "md5hash": '
+ '"add6bb58e139be103324d04d82d8f545", "version": "7.0", '
+ '"architecture": "tempest-x86-252246646", "os": "win"}}',
+ 'request_headers': "{'Content-Type': 'application/json', "
+ "'Accept': 'application/json', 'X-Auth-Token': '<omitted>'}",
+ 'response_body': '{"agent": {"url": "xxx://xxxx/xxx/xxx", '
+ '"hypervisor": "kvm", "md5hash": '
+ '"add6bb58e139be103324d04d82d8f545", "version": "7.0", '
+ '"architecture": "tempest-x86-252246646", "os": "win", '
+ '"agent_id": 2}}',
+ 'response_headers': "{'status': '200', 'content-length': "
+ "'195', 'x-compute-request-id': "
+ "'req-b4136f06-c015-4e7e-995f-c43831e3ecce', 'vary': "
+ "'X-OpenStack-Nova-API-Version', 'connection': 'close', "
+ "'x-openstack-nova-api-version': '2.1', 'date': "
+ "'Tue, 02 Feb 2016 03:27:00 GMT', 'content-type': "
+ "'application/json'}",
+ 'service': 'Nova',
+ 'status_code': '200',
+ 'url': 'v2.1/<id>/os-agents',
+ 'verb': 'POST'}, {
+ 'name': 'AgentsAdminTestJSON:tearDown',
+ 'request_body': 'None',
+ 'request_headers': "{'Content-Type': 'application/json', "
+ "'Accept': 'application/json', 'X-Auth-Token': '<omitted>'}",
+ 'response_body': '',
+ 'response_headers': "{'status': '200', 'content-length': "
+ "'0', 'x-compute-request-id': "
+ "'req-ee905fd6-a5b5-4da4-8c37-5363cb25bd9d', 'vary': "
+ "'X-OpenStack-Nova-API-Version', 'connection': 'close', "
+ "'x-openstack-nova-api-version': '2.1', 'date': "
+ "'Tue, 02 Feb 2016 03:27:00 GMT', 'content-type': "
+ "'application/json'}",
+ 'service': 'Nova',
+ 'status_code': '200',
+ 'url': 'v2.1/<id>/os-agents/1',
+ 'verb': 'DELETE'}, {
+ 'name': 'AgentsAdminTestJSON:_run_cleanups',
+ 'request_body': 'None',
+ 'request_headers': "{'Content-Type': 'application/json', "
+ "'Accept': 'application/json', 'X-Auth-Token': '<omitted>'}",
+ 'response_headers': "{'status': '200', 'content-length': "
+ "'0', 'x-compute-request-id': "
+ "'req-e912cac0-63e0-4679-a68a-b6d18ddca074', 'vary': "
+ "'X-OpenStack-Nova-API-Version', 'connection': 'close', "
+ "'x-openstack-nova-api-version': '2.1', 'date': "
+ "'Tue, 02 Feb 2016 03:27:00 GMT', 'content-type': "
+ "'application/json'}",
+ 'service': 'Nova',
+ 'status_code': '200',
+ 'url': 'v2.1/<id>/os-agents/2',
+ 'verb': 'DELETE'}],
+ 'foo': [{
+ 'name': 'AgentsAdminTestJSON:setUp',
+ 'request_body': '{"agent": {"url": "xxx://xxxx/xxx/xxx", '
+ '"hypervisor": "common", "md5hash": '
+ '"add6bb58e139be103324d04d82d8f545", "version": "7.0", '
+ '"architecture": "tempest-x86_64-948635295", "os": "linux"}}',
+ 'request_headers': "{'Content-Type': 'application/json', "
+ "'Accept': 'application/json', 'X-Auth-Token': '<omitted>'}",
+ 'response_body': '{"agent": {"url": "xxx://xxxx/xxx/xxx", '
+ '"hypervisor": "common", "md5hash": '
+ '"add6bb58e139be103324d04d82d8f545", "version": "7.0", '
+ '"architecture": "tempest-x86_64-948635295", "os": "linux", '
+ '"agent_id": 3}}',
+ 'response_headers': "{'status': '200', 'content-length': "
+ "'203', 'x-compute-request-id': "
+ "'req-ccd2116d-04b1-4ffe-ae32-fb623f68bf1c', 'vary': "
+ "'X-OpenStack-Nova-API-Version', 'connection': 'close', "
+ "'x-openstack-nova-api-version': '2.1', 'date': "
+ "'Tue, 02 Feb 2016 03:27:01 GMT', 'content-type': "
+ "'application/json'}",
+ 'service': 'Nova',
+ 'status_code': '200',
+ 'url': 'v2.1/<id>/os-agents',
+ 'verb': 'POST'}, {
+ 'name': 'AgentsAdminTestJSON:test_delete_agent',
+ 'request_body': 'None',
+ 'request_headers': "{'Content-Type': 'application/json', "
+ "'Accept': 'application/json', 'X-Auth-Token': '<omitted>'}",
+ 'response_body': '',
+ 'response_headers': "{'status': '200', 'content-length': "
+ "'0', 'x-compute-request-id': "
+ "'req-6e7fa28f-ae61-4388-9a78-947c58bc0588', 'vary': "
+ "'X-OpenStack-Nova-API-Version', 'connection': 'close', "
+ "'x-openstack-nova-api-version': '2.1', 'date': "
+ "'Tue, 02 Feb 2016 03:27:01 GMT', 'content-type': "
+ "'application/json'}",
+ 'service': 'Nova',
+ 'status_code': '200',
+ 'url': 'v2.1/<id>/os-agents/3',
+ 'verb': 'DELETE'}, {
+ 'name': 'AgentsAdminTestJSON:test_delete_agent',
+ 'request_body': 'None',
+ 'request_headers': "{'Content-Type': 'application/json', "
+ "'Accept': 'application/json', 'X-Auth-Token': '<omitted>'}",
+ 'response_body': '{"agents": []}',
+ 'response_headers': "{'status': '200', 'content-length': "
+ "'14', 'content-location': "
+ "'http://23.253.76.97:8774/v2.1/"
+ "cf6b1933fe5b476fbbabb876f6d1b924/os-agents', "
+ "'x-compute-request-id': "
+ "'req-e41aa9b4-41a6-4138-ae04-220b768eb644', 'vary': "
+ "'X-OpenStack-Nova-API-Version', 'connection': 'close', "
+ "'x-openstack-nova-api-version': '2.1', 'date': "
+ "'Tue, 02 Feb 2016 03:27:01 GMT', 'content-type': "
+ "'application/json'}",
+ 'service': 'Nova',
+ 'status_code': '200',
+ 'url': 'v2.1/<id>/os-agents',
+ 'verb': 'GET'}, {
+ 'name': 'AgentsAdminTestJSON:tearDown',
+ 'request_body': 'None',
+ 'request_headers': "{'Content-Type': 'application/json', "
+ "'Accept': 'application/json', 'X-Auth-Token': '<omitted>'}",
+ 'response_headers': "{'status': '404', 'content-length': "
+ "'82', 'x-compute-request-id': "
+ "'req-e297aeea-91cf-4f26-b49c-8f46b1b7a926', 'vary': "
+ "'X-OpenStack-Nova-API-Version', 'connection': 'close', "
+ "'x-openstack-nova-api-version': '2.1', 'date': "
+ "'Tue, 02 Feb 2016 03:27:02 GMT', 'content-type': "
+ "'application/json; charset=UTF-8'}",
+ 'service': 'Nova',
+ 'status_code': '404',
+ 'url': 'v2.1/<id>/os-agents/3',
+ 'verb': 'DELETE'}]}
+
+ self.assertEqual(expected_result, parser.test_logs)
diff --git a/tempest/tests/cmd/test_tempest_init.py b/tempest/tests/cmd/test_tempest_init.py
index 1048a52..5f39ac9 100644
--- a/tempest/tests/cmd/test_tempest_init.py
+++ b/tempest/tests/cmd/test_tempest_init.py
@@ -13,7 +13,6 @@
# under the License.
import os
-import shutil
import fixtures
@@ -28,36 +27,79 @@
conf_dir = self.useFixture(fixtures.TempDir())
init_cmd = init.TempestInit(None, None)
- init_cmd.generate_testr_conf(conf_dir.path)
+ init_cmd.generate_stestr_conf(conf_dir.path)
# Generate expected file contents
top_level_path = os.path.dirname(os.path.dirname(init.__file__))
discover_path = os.path.join(top_level_path, 'test_discover')
- testr_conf_file = init.TESTR_CONF % (top_level_path, discover_path)
+ stestr_conf_file = init.STESTR_CONF % (discover_path, top_level_path)
- conf_path = conf_dir.join('.testr.conf')
- conf_file = open(conf_path, 'r')
- self.addCleanup(conf_file.close)
- self.assertEqual(conf_file.read(), testr_conf_file)
+ conf_path = conf_dir.join('.stestr.conf')
+ with open(conf_path, 'r') as conf_file:
+ self.assertEqual(conf_file.read(), stestr_conf_file)
def test_generate_sample_config(self):
local_dir = self.useFixture(fixtures.TempDir())
etc_dir_path = os.path.join(local_dir.path, 'etc/')
os.mkdir(etc_dir_path)
- tmp_dir = self.useFixture(fixtures.TempDir())
- config_dir = os.path.join(tmp_dir.path, 'config/')
- shutil.copytree('etc/', config_dir)
init_cmd = init.TempestInit(None, None)
local_sample_conf_file = os.path.join(etc_dir_path,
'tempest.conf.sample')
+
# Verify no sample config file exist
self.assertFalse(os.path.isfile(local_sample_conf_file))
- init_cmd.generate_sample_config(local_dir.path, config_dir)
+ init_cmd.generate_sample_config(local_dir.path)
# Verify sample config file exist with some content
self.assertTrue(os.path.isfile(local_sample_conf_file))
self.assertGreater(os.path.getsize(local_sample_conf_file), 0)
+ def test_update_local_conf(self):
+ local_dir = self.useFixture(fixtures.TempDir())
+ etc_dir_path = os.path.join(local_dir.path, 'etc/')
+ os.mkdir(etc_dir_path)
+ lock_dir = os.path.join(local_dir.path, 'tempest_lock')
+ config_path = os.path.join(etc_dir_path, 'tempest.conf')
+ log_dir = os.path.join(local_dir.path, 'logs')
+
+ init_cmd = init.TempestInit(None, None)
+
+ # Generate the config file
+ init_cmd.generate_sample_config(local_dir.path)
+
+ # Create a conf file with populated values
+ config_parser_pre = init_cmd.get_configparser(config_path)
+ with open(config_path, 'w+') as conf_file:
+ # create the same section init will check for and add values to
+ config_parser_pre.add_section('oslo_concurrency')
+ config_parser_pre.set('oslo_concurrency', 'TEST', local_dir.path)
+ # create a new section
+ config_parser_pre.add_section('TEST')
+ config_parser_pre.set('TEST', 'foo', "bar")
+ config_parser_pre.write(conf_file)
+
+ # Update the config file the same way tempest init does
+ init_cmd.update_local_conf(config_path, lock_dir, log_dir)
+
+ # parse the new config file to verify it
+ config_parser_post = init_cmd.get_configparser(config_path)
+
+ # check that our value in oslo_concurrency wasn't overwritten
+ self.assertTrue(config_parser_post.has_section('oslo_concurrency'))
+ self.assertEqual(config_parser_post.get('oslo_concurrency', 'TEST'),
+ local_dir.path)
+ # check that the lock directory was set correctly
+ self.assertEqual(config_parser_post.get('oslo_concurrency',
+ 'lock_path'), lock_dir)
+
+ # check that our new section still exists and wasn't modified
+ self.assertTrue(config_parser_post.has_section('TEST'))
+ self.assertEqual(config_parser_post.get('TEST', 'foo'), 'bar')
+
+ # check that the DEFAULT values are correct
+ # NOTE(auggy): has_section ignores DEFAULT
+ self.assertEqual(config_parser_post.get('DEFAULT', 'log_dir'), log_dir)
+
def test_create_working_dir_with_existing_local_dir_non_empty(self):
fake_local_dir = self.useFixture(fixtures.TempDir())
fake_local_conf_dir = self.useFixture(fixtures.TempDir())
@@ -83,15 +125,30 @@
lock_path = os.path.join(fake_local_dir.path, 'tempest_lock')
etc_dir = os.path.join(fake_local_dir.path, 'etc')
log_dir = os.path.join(fake_local_dir.path, 'logs')
- testr_dir = os.path.join(fake_local_dir.path, '.testrepository')
+ stestr_dir = os.path.join(fake_local_dir.path, '.stestr')
self.assertTrue(os.path.isdir(lock_path))
self.assertTrue(os.path.isdir(etc_dir))
self.assertTrue(os.path.isdir(log_dir))
- self.assertTrue(os.path.isdir(testr_dir))
+ self.assertTrue(os.path.isdir(stestr_dir))
# Assert file creation
fake_file_moved = os.path.join(etc_dir, 'conf_file.conf')
local_conf_file = os.path.join(etc_dir, 'tempest.conf')
- local_testr_conf = os.path.join(fake_local_dir.path, '.testr.conf')
+ local_stestr_conf = os.path.join(fake_local_dir.path, '.stestr.conf')
self.assertTrue(os.path.isfile(fake_file_moved))
self.assertTrue(os.path.isfile(local_conf_file))
- self.assertTrue(os.path.isfile(local_testr_conf))
+ self.assertTrue(os.path.isfile(local_stestr_conf))
+
+ def test_take_action_fails(self):
+ class ParsedArgs(object):
+ workspace_dir = self.useFixture(fixtures.TempDir()).path
+ workspace_path = os.path.join(workspace_dir, 'workspace.yaml')
+ name = 'test'
+ dir_base = self.useFixture(fixtures.TempDir()).path
+ dir = os.path.join(dir_base, 'foo', 'bar')
+ config_dir = self.useFixture(fixtures.TempDir()).path
+ show_global_dir = False
+ pa = ParsedArgs()
+ init_cmd = init.TempestInit(None, None)
+ self.assertRaises(OSError, init_cmd.take_action, pa)
+ # one more trying should be a same error not "workspace already exists"
+ self.assertRaises(OSError, init_cmd.take_action, pa)
diff --git a/tempest/tests/cmd/test_verify_tempest_config.py b/tempest/tests/cmd/test_verify_tempest_config.py
index a5dea54..8641b63 100644
--- a/tempest/tests/cmd/test_verify_tempest_config.py
+++ b/tempest/tests/cmd/test_verify_tempest_config.py
@@ -12,28 +12,60 @@
# License for the specific language governing permissions and limitations
# under the License.
-
+import fixtures
import mock
from oslo_serialization import jsonutils as json
-from oslotest import mockpatch
+from tempest import clients
from tempest.cmd import verify_tempest_config
+from tempest.common import credentials_factory
from tempest import config
+from tempest.lib.common import rest_client
+from tempest.lib.common.utils import data_utils
+from tempest.lib import exceptions as lib_exc
from tempest.tests import base
from tempest.tests import fake_config
class TestGetAPIVersions(base.TestCase):
+ def test_remove_version_project(self):
+ f = verify_tempest_config._remove_version_project
+ self.assertEqual('/', f('/v2.1/%s/' % data_utils.rand_uuid_hex()))
+ self.assertEqual('', f('/v2.1/tenant_id'))
+ self.assertEqual('', f('/v3'))
+ self.assertEqual('/', f('/v3/'))
+ self.assertEqual('/something/', f('/something/v2.1/tenant_id/'))
+ self.assertEqual('/something', f('/something/v2.1/tenant_id'))
+ self.assertEqual('/something', f('/something/v3'))
+ self.assertEqual('/something/', f('/something/v3/'))
+ self.assertEqual('/', f('/')) # http://localhost/
+ self.assertEqual('', f('')) # http://localhost
+
def test_url_grab_versioned_nova_nossl(self):
base_url = 'http://127.0.0.1:8774/v2/'
endpoint = verify_tempest_config._get_unversioned_endpoint(base_url)
- self.assertEqual('http://127.0.0.1:8774', endpoint)
+ self.assertEqual('http://127.0.0.1:8774/', endpoint)
def test_url_grab_versioned_nova_ssl(self):
base_url = 'https://127.0.0.1:8774/v3/'
endpoint = verify_tempest_config._get_unversioned_endpoint(base_url)
- self.assertEqual('https://127.0.0.1:8774', endpoint)
+ self.assertEqual('https://127.0.0.1:8774/', endpoint)
+
+ def test_get_unversioned_endpoint_base(self):
+ base_url = 'https://127.0.0.1:5000/'
+ endpoint = verify_tempest_config._get_unversioned_endpoint(base_url)
+ self.assertEqual('https://127.0.0.1:5000/', endpoint)
+
+ def test_get_unversioned_endpoint_subpath(self):
+ base_url = 'https://127.0.0.1/identity/v3'
+ endpoint = verify_tempest_config._get_unversioned_endpoint(base_url)
+ self.assertEqual('https://127.0.0.1/identity', endpoint)
+
+ def test_get_unversioned_endpoint_subpath_trailing_solidus(self):
+ base_url = 'https://127.0.0.1/identity/v3/'
+ endpoint = verify_tempest_config._get_unversioned_endpoint(base_url)
+ self.assertEqual('https://127.0.0.1/identity/', endpoint)
class TestDiscovery(base.TestCase):
@@ -41,47 +73,74 @@
def setUp(self):
super(TestDiscovery, self).setUp()
self.useFixture(fake_config.ConfigFixture())
- self.stubs.Set(config, 'TempestConfigPrivate', fake_config.FakePrivate)
+ self.patchobject(config, 'TempestConfigPrivate',
+ fake_config.FakePrivate)
def test_get_keystone_api_versions(self):
- self.useFixture(mockpatch.PatchObject(
+ self.useFixture(fixtures.MockPatchObject(
verify_tempest_config, '_get_unversioned_endpoint',
return_value='http://fake_endpoint:5000'))
fake_resp = {'versions': {'values': [{'id': 'v2.0'}, {'id': 'v3.0'}]}}
fake_resp = json.dumps(fake_resp)
- self.useFixture(mockpatch.Patch('httplib2.Http.request',
- return_value=(None, fake_resp)))
+ self.useFixture(fixtures.MockPatch(
+ 'tempest.lib.common.http.ClosingHttp.request',
+ return_value=(None, fake_resp)))
fake_os = mock.MagicMock()
versions = verify_tempest_config._get_api_versions(fake_os, 'keystone')
self.assertIn('v2.0', versions)
self.assertIn('v3.0', versions)
def test_get_cinder_api_versions(self):
- self.useFixture(mockpatch.PatchObject(
+ self.useFixture(fixtures.MockPatchObject(
verify_tempest_config, '_get_unversioned_endpoint',
return_value='http://fake_endpoint:5000'))
fake_resp = {'versions': [{'id': 'v1.0'}, {'id': 'v2.0'}]}
fake_resp = json.dumps(fake_resp)
- self.useFixture(mockpatch.Patch('httplib2.Http.request',
- return_value=(None, fake_resp)))
+ self.useFixture(fixtures.MockPatch(
+ 'tempest.lib.common.http.ClosingHttp.request',
+ return_value=(None, fake_resp)))
fake_os = mock.MagicMock()
versions = verify_tempest_config._get_api_versions(fake_os, 'cinder')
self.assertIn('v1.0', versions)
self.assertIn('v2.0', versions)
def test_get_nova_versions(self):
- self.useFixture(mockpatch.PatchObject(
+ self.useFixture(fixtures.MockPatchObject(
verify_tempest_config, '_get_unversioned_endpoint',
return_value='http://fake_endpoint:5000'))
fake_resp = {'versions': [{'id': 'v2.0'}, {'id': 'v3.0'}]}
fake_resp = json.dumps(fake_resp)
- self.useFixture(mockpatch.Patch('httplib2.Http.request',
- return_value=(None, fake_resp)))
+ self.useFixture(fixtures.MockPatch(
+ 'tempest.lib.common.http.ClosingHttp.request',
+ return_value=(None, fake_resp)))
fake_os = mock.MagicMock()
versions = verify_tempest_config._get_api_versions(fake_os, 'nova')
self.assertIn('v2.0', versions)
self.assertIn('v3.0', versions)
+ def test_get_versions_invalid_response(self):
+ # When the response doesn't contain a JSON response, an error is
+ # logged.
+ mock_log_error = self.useFixture(fixtures.MockPatchObject(
+ verify_tempest_config.LOG, 'error')).mock
+
+ self.useFixture(fixtures.MockPatchObject(
+ verify_tempest_config, '_get_unversioned_endpoint'))
+
+ # Simulated response is not JSON.
+ sample_body = (
+ '<html><head>Sample Response</head><body>This is the sample page '
+ 'for the web server. Why are you requesting it?</body></html>')
+ self.useFixture(fixtures.MockPatch(
+ 'tempest.lib.common.http.ClosingHttp.request',
+ return_value=(None, sample_body)))
+
+ # service value doesn't matter, just needs to match what
+ # _get_api_versions puts in its client_dict.
+ self.assertRaises(ValueError, verify_tempest_config._get_api_versions,
+ os=mock.MagicMock(), service='keystone')
+ self.assertTrue(mock_log_error.called)
+
def test_verify_api_versions(self):
api_services = ['cinder', 'glance', 'keystone']
fake_os = mock.MagicMock()
@@ -100,14 +159,14 @@
verify_tempest_config.verify_api_versions(fake_os, 'foo', True)
self.assertFalse(verify_mock.called)
- def test_verify_keystone_api_versions_no_v3(self):
- self.useFixture(mockpatch.PatchObject(
+ @mock.patch('tempest.lib.common.http.ClosingHttp.request')
+ def test_verify_keystone_api_versions_no_v3(self, mock_request):
+ self.useFixture(fixtures.MockPatchObject(
verify_tempest_config, '_get_unversioned_endpoint',
return_value='http://fake_endpoint:5000'))
fake_resp = {'versions': {'values': [{'id': 'v2.0'}]}}
fake_resp = json.dumps(fake_resp)
- self.useFixture(mockpatch.Patch('httplib2.Http.request',
- return_value=(None, fake_resp)))
+ mock_request.return_value = (None, fake_resp)
fake_os = mock.MagicMock()
with mock.patch.object(verify_tempest_config,
'print_and_or_update') as print_mock:
@@ -116,57 +175,62 @@
'identity-feature-enabled',
False, True)
- def test_verify_keystone_api_versions_no_v2(self):
- self.useFixture(mockpatch.PatchObject(
- verify_tempest_config, '_get_unversioned_endpoint',
- return_value='http://fake_endpoint:5000'))
- fake_resp = {'versions': {'values': [{'id': 'v3.0'}]}}
- fake_resp = json.dumps(fake_resp)
- self.useFixture(mockpatch.Patch('httplib2.Http.request',
- return_value=(None, fake_resp)))
- fake_os = mock.MagicMock()
- with mock.patch.object(verify_tempest_config,
- 'print_and_or_update') as print_mock:
- verify_tempest_config.verify_keystone_api_versions(fake_os, True)
- print_mock.assert_called_once_with('api_v2',
- 'identity-feature-enabled',
- False, True)
-
- def test_verify_cinder_api_versions_no_v2(self):
- self.useFixture(mockpatch.PatchObject(
- verify_tempest_config, '_get_unversioned_endpoint',
- return_value='http://fake_endpoint:5000'))
- fake_resp = {'versions': [{'id': 'v1.0'}]}
- fake_resp = json.dumps(fake_resp)
- self.useFixture(mockpatch.Patch('httplib2.Http.request',
- return_value=(None, fake_resp)))
- fake_os = mock.MagicMock()
- with mock.patch.object(verify_tempest_config,
- 'print_and_or_update') as print_mock:
- verify_tempest_config.verify_cinder_api_versions(fake_os, True)
- print_mock.assert_called_once_with('api_v2', 'volume-feature-enabled',
- False, True)
-
- def test_verify_cinder_api_versions_no_v1(self):
- self.useFixture(mockpatch.PatchObject(
+ @mock.patch('tempest.lib.common.http.ClosingHttp.request')
+ def test_verify_cinder_api_versions_no_v3(self, mock_request):
+ self.useFixture(fixtures.MockPatchObject(
verify_tempest_config, '_get_unversioned_endpoint',
return_value='http://fake_endpoint:5000'))
fake_resp = {'versions': [{'id': 'v2.0'}]}
fake_resp = json.dumps(fake_resp)
- self.useFixture(mockpatch.Patch('httplib2.Http.request',
- return_value=(None, fake_resp)))
+ mock_request.return_value = (None, fake_resp)
fake_os = mock.MagicMock()
with mock.patch.object(verify_tempest_config,
'print_and_or_update') as print_mock:
verify_tempest_config.verify_cinder_api_versions(fake_os, True)
- print_mock.assert_called_once_with('api_v1', 'volume-feature-enabled',
- False, True)
+ print_mock.assert_any_call('api_v3', 'volume-feature-enabled',
+ False, True)
+ self.assertEqual(1, print_mock.call_count)
+
+ @mock.patch('tempest.lib.common.http.ClosingHttp.request')
+ def test_verify_cinder_api_versions_no_v2(self, mock_request):
+ self.useFixture(fixtures.MockPatchObject(
+ verify_tempest_config, '_get_unversioned_endpoint',
+ return_value='http://fake_endpoint:5000'))
+ fake_resp = {'versions': [{'id': 'v3.0'}]}
+ fake_resp = json.dumps(fake_resp)
+ mock_request.return_value = (None, fake_resp)
+ fake_os = mock.MagicMock()
+ with mock.patch.object(verify_tempest_config,
+ 'print_and_or_update') as print_mock:
+ verify_tempest_config.verify_cinder_api_versions(fake_os, True)
+ print_mock.assert_any_call('api_v2', 'volume-feature-enabled',
+ False, True)
+ self.assertEqual(1, print_mock.call_count)
+
+ @mock.patch('tempest.lib.common.http.ClosingHttp.request')
+ def test_verify_cinder_api_versions_no_v1(self, mock_request):
+ self.useFixture(fixtures.MockPatchObject(
+ verify_tempest_config, '_get_unversioned_endpoint',
+ return_value='http://fake_endpoint:5000'))
+ fake_resp = {'versions': [{'id': 'v2.0'}, {'id': 'v3.0'}]}
+ fake_resp = json.dumps(fake_resp)
+ mock_request.return_value = (None, fake_resp)
+ fake_os = mock.MagicMock()
+ with mock.patch.object(verify_tempest_config,
+ 'print_and_or_update') as print_mock:
+ verify_tempest_config.verify_cinder_api_versions(fake_os, True)
+ print_mock.assert_not_called()
def test_verify_glance_version_no_v2_with_v1_1(self):
- def fake_get_versions():
- return (None, ['v1.1'])
+ # This test verifies that wrong config api_v2 = True is detected
+ class FakeClient(object):
+ def get_versions(self):
+ return (None, ['v1.0'])
+
fake_os = mock.MagicMock()
- fake_os.image_client.get_versions = fake_get_versions
+ fake_module = mock.MagicMock()
+ fake_module.ImagesClient = FakeClient
+ fake_os.image_v1 = fake_module
with mock.patch.object(verify_tempest_config,
'print_and_or_update') as print_mock:
verify_tempest_config.verify_glance_api_versions(fake_os, True)
@@ -174,10 +238,15 @@
False, True)
def test_verify_glance_version_no_v2_with_v1_0(self):
- def fake_get_versions():
- return (None, ['v1.0'])
+ # This test verifies that wrong config api_v2 = True is detected
+ class FakeClient(object):
+ def get_versions(self):
+ return (None, ['v1.0'])
+
fake_os = mock.MagicMock()
- fake_os.image_client.get_versions = fake_get_versions
+ fake_module = mock.MagicMock()
+ fake_module.ImagesClient = FakeClient
+ fake_os.image_v1 = fake_module
with mock.patch.object(verify_tempest_config,
'print_and_or_update') as print_mock:
verify_tempest_config.verify_glance_api_versions(fake_os, True)
@@ -185,24 +254,60 @@
False, True)
def test_verify_glance_version_no_v1(self):
- def fake_get_versions():
- return (None, ['v2.0'])
+ # This test verifies that wrong config api_v1 = True is detected
+ class FakeClient(object):
+ def get_versions(self):
+ raise lib_exc.NotFound()
+
+ def list_versions(self):
+ return {'versions': [{'id': 'v2.0'}]}
+
fake_os = mock.MagicMock()
- fake_os.image_client.get_versions = fake_get_versions
+ fake_module = mock.MagicMock()
+ fake_module.ImagesClient = FakeClient
+ fake_module.VersionsClient = FakeClient
+ fake_os.image_v1 = fake_module
+ fake_os.image_v2 = fake_module
with mock.patch.object(verify_tempest_config,
'print_and_or_update') as print_mock:
verify_tempest_config.verify_glance_api_versions(fake_os, True)
print_mock.assert_called_once_with('api_v1', 'image-feature-enabled',
False, True)
+ def test_verify_glance_version_no_version(self):
+ # This test verifies that wrong config api_v1 = True is detected
+ class FakeClient(object):
+ def get_versions(self):
+ raise lib_exc.NotFound()
+
+ def list_versions(self):
+ raise lib_exc.NotFound()
+
+ fake_os = mock.MagicMock()
+ fake_module = mock.MagicMock()
+ fake_module.ImagesClient = FakeClient
+ fake_module.VersionsClient = FakeClient
+ fake_os.image_v1 = fake_module
+ fake_os.image_v2 = fake_module
+ with mock.patch.object(verify_tempest_config,
+ 'print_and_or_update') as print_mock:
+ verify_tempest_config.verify_glance_api_versions(fake_os, True)
+ print_mock.assert_called_once_with('glance',
+ 'service-available',
+ False, True)
+
def test_verify_extensions_neutron(self):
def fake_list_extensions():
return {'extensions': [{'alias': 'fake1'},
{'alias': 'fake2'},
{'alias': 'not_fake'}]}
fake_os = mock.MagicMock()
- fake_os.network_client.list_extensions = fake_list_extensions
- self.useFixture(mockpatch.PatchObject(
+ fake_client = mock.MagicMock()
+ fake_client.list_extensions = fake_list_extensions
+ self.useFixture(fixtures.MockPatchObject(
+ verify_tempest_config, 'get_extension_client',
+ return_value=fake_client))
+ self.useFixture(fixtures.MockPatchObject(
verify_tempest_config, 'get_enabled_extensions',
return_value=(['fake1', 'fake2', 'fake3'])))
results = verify_tempest_config.verify_extensions(fake_os,
@@ -223,8 +328,12 @@
{'alias': 'fake2'},
{'alias': 'not_fake'}]}
fake_os = mock.MagicMock()
- fake_os.network_client.list_extensions = fake_list_extensions
- self.useFixture(mockpatch.PatchObject(
+ fake_client = mock.MagicMock()
+ fake_client.list_extensions = fake_list_extensions
+ self.useFixture(fixtures.MockPatchObject(
+ verify_tempest_config, 'get_extension_client',
+ return_value=fake_client))
+ self.useFixture(fixtures.MockPatchObject(
verify_tempest_config, 'get_enabled_extensions',
return_value=(['all'])))
results = verify_tempest_config.verify_extensions(fake_os,
@@ -240,15 +349,17 @@
{'alias': 'fake2'},
{'alias': 'not_fake'}]}
fake_os = mock.MagicMock()
- # NOTE (e0ne): mock both v1 and v2 APIs
- fake_os.volumes_extension_client.list_extensions = fake_list_extensions
- fake_os.volumes_v2_extension_client.list_extensions = (
- fake_list_extensions)
- self.useFixture(mockpatch.PatchObject(
+ fake_client = mock.MagicMock()
+ fake_client.list_extensions = fake_list_extensions
+ self.useFixture(fixtures.MockPatchObject(
+ verify_tempest_config, 'get_extension_client',
+ return_value=fake_client))
+ self.useFixture(fixtures.MockPatchObject(
verify_tempest_config, 'get_enabled_extensions',
return_value=(['fake1', 'fake2', 'fake3'])))
results = verify_tempest_config.verify_extensions(fake_os,
'cinder', {})
+
self.assertIn('cinder', results)
self.assertIn('fake1', results['cinder'])
self.assertTrue(results['cinder']['fake1'])
@@ -265,11 +376,12 @@
{'alias': 'fake2'},
{'alias': 'not_fake'}]}
fake_os = mock.MagicMock()
- # NOTE (e0ne): mock both v1 and v2 APIs
- fake_os.volumes_extension_client.list_extensions = fake_list_extensions
- fake_os.volumes_v2_extension_client.list_extensions = (
- fake_list_extensions)
- self.useFixture(mockpatch.PatchObject(
+ fake_client = mock.MagicMock()
+ fake_client.list_extensions = fake_list_extensions
+ self.useFixture(fixtures.MockPatchObject(
+ verify_tempest_config, 'get_extension_client',
+ return_value=fake_client))
+ self.useFixture(fixtures.MockPatchObject(
verify_tempest_config, 'get_enabled_extensions',
return_value=(['all'])))
results = verify_tempest_config.verify_extensions(fake_os,
@@ -284,8 +396,12 @@
return ([{'alias': 'fake1'}, {'alias': 'fake2'},
{'alias': 'not_fake'}])
fake_os = mock.MagicMock()
- fake_os.extensions_client.list_extensions = fake_list_extensions
- self.useFixture(mockpatch.PatchObject(
+ fake_client = mock.MagicMock()
+ fake_client.list_extensions = fake_list_extensions
+ self.useFixture(fixtures.MockPatchObject(
+ verify_tempest_config, 'get_extension_client',
+ return_value=fake_client))
+ self.useFixture(fixtures.MockPatchObject(
verify_tempest_config, 'get_enabled_extensions',
return_value=(['fake1', 'fake2', 'fake3'])))
results = verify_tempest_config.verify_extensions(fake_os,
@@ -306,8 +422,12 @@
{'alias': 'fake2'},
{'alias': 'not_fake'}]})
fake_os = mock.MagicMock()
- fake_os.extensions_client.list_extensions = fake_list_extensions
- self.useFixture(mockpatch.PatchObject(
+ fake_client = mock.MagicMock()
+ fake_client.list_extensions = fake_list_extensions
+ self.useFixture(fixtures.MockPatchObject(
+ verify_tempest_config, 'get_extension_client',
+ return_value=fake_client))
+ self.useFixture(fixtures.MockPatchObject(
verify_tempest_config, 'get_enabled_extensions',
return_value=(['all'])))
results = verify_tempest_config.verify_extensions(fake_os,
@@ -319,13 +439,17 @@
def test_verify_extensions_swift(self):
def fake_list_extensions():
- return (None, {'fake1': 'metadata',
- 'fake2': 'metadata',
- 'not_fake': 'metadata',
- 'swift': 'metadata'})
+ return {'fake1': 'metadata',
+ 'fake2': 'metadata',
+ 'not_fake': 'metadata',
+ 'swift': 'metadata'}
fake_os = mock.MagicMock()
- fake_os.account_client.list_extensions = fake_list_extensions
- self.useFixture(mockpatch.PatchObject(
+ fake_client = mock.MagicMock()
+ fake_client.list_capabilities = fake_list_extensions
+ self.useFixture(fixtures.MockPatchObject(
+ verify_tempest_config, 'get_extension_client',
+ return_value=fake_client))
+ self.useFixture(fixtures.MockPatchObject(
verify_tempest_config, 'get_enabled_extensions',
return_value=(['fake1', 'fake2', 'fake3'])))
results = verify_tempest_config.verify_extensions(fake_os, 'swift', {})
@@ -341,13 +465,17 @@
def test_verify_extensions_swift_all(self):
def fake_list_extensions():
- return (None, {'fake1': 'metadata',
- 'fake2': 'metadata',
- 'not_fake': 'metadata',
- 'swift': 'metadata'})
+ return {'fake1': 'metadata',
+ 'fake2': 'metadata',
+ 'not_fake': 'metadata',
+ 'swift': 'metadata'}
fake_os = mock.MagicMock()
- fake_os.account_client.list_extensions = fake_list_extensions
- self.useFixture(mockpatch.PatchObject(
+ fake_client = mock.MagicMock()
+ fake_client.list_capabilities = fake_list_extensions
+ self.useFixture(fixtures.MockPatchObject(
+ verify_tempest_config, 'get_extension_client',
+ return_value=fake_client))
+ self.useFixture(fixtures.MockPatchObject(
verify_tempest_config, 'get_enabled_extensions',
return_value=(['all'])))
results = verify_tempest_config.verify_extensions(fake_os,
@@ -356,3 +484,13 @@
self.assertIn('extensions', results['swift'])
self.assertEqual(sorted(['not_fake', 'fake1', 'fake2']),
sorted(results['swift']['extensions']))
+
+ def test_get_extension_client(self):
+ creds = credentials_factory.get_credentials(
+ fill_in=False, username='fake_user', project_name='fake_project',
+ password='fake_password')
+ os = clients.Manager(creds)
+ for service in ['nova', 'neutron', 'swift', 'cinder']:
+ extensions_client = verify_tempest_config.get_extension_client(
+ os, service)
+ self.assertIsInstance(extensions_client, rest_client.RestClient)
diff --git a/tempest/tests/cmd/test_workspace.py b/tempest/tests/cmd/test_workspace.py
new file mode 100644
index 0000000..3ed8a10
--- /dev/null
+++ b/tempest/tests/cmd/test_workspace.py
@@ -0,0 +1,186 @@
+# Copyright 2016 Rackspace
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import os
+import shutil
+import subprocess
+import tempfile
+
+from mock import patch
+try:
+ from StringIO import StringIO
+except ImportError:
+ from io import StringIO
+from tempest.cmd import workspace
+from tempest.lib.common.utils import data_utils
+from tempest.tests import base
+
+
+class TestTempestWorkspaceBase(base.TestCase):
+ def setUp(self):
+ super(TestTempestWorkspaceBase, self).setUp()
+ self.name = data_utils.rand_uuid()
+ self.path = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, self.path, ignore_errors=True)
+ store_dir = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, store_dir, ignore_errors=True)
+ self.store_file = os.path.join(store_dir, 'workspace.yaml')
+ self.workspace_manager = workspace.WorkspaceManager(
+ path=self.store_file)
+ self.workspace_manager.register_new_workspace(self.name, self.path)
+
+
+class TestTempestWorkspace(TestTempestWorkspaceBase):
+ def _run_cmd_gets_return_code(self, cmd, expected):
+ process = subprocess.Popen(cmd, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ stdout, stderr = process.communicate()
+ return_code = process.returncode
+ msg = ("%s failed with:\nstdout: %s\nstderr: %s" % (' '.join(cmd),
+ stdout, stderr))
+ self.assertEqual(return_code, expected, msg)
+
+ def test_run_workspace_list(self):
+ cmd = ['tempest', 'workspace', 'list',
+ '--workspace-path', self.store_file]
+ self._run_cmd_gets_return_code(cmd, 0)
+
+ def test_run_workspace_register(self):
+ name = data_utils.rand_uuid()
+ path = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, path, ignore_errors=True)
+ cmd = ['tempest', 'workspace', 'register',
+ '--workspace-path', self.store_file,
+ '--name', name, '--path', path]
+ self._run_cmd_gets_return_code(cmd, 0)
+ self.assertIsNotNone(self.workspace_manager.get_workspace(name))
+
+ def test_run_workspace_rename(self):
+ new_name = data_utils.rand_uuid()
+ cmd = ['tempest', 'workspace', 'rename',
+ '--workspace-path', self.store_file,
+ '--old-name', self.name, '--new-name', new_name]
+ self._run_cmd_gets_return_code(cmd, 0)
+ self.assertIsNone(self.workspace_manager.get_workspace(self.name))
+ self.assertIsNotNone(self.workspace_manager.get_workspace(new_name))
+
+ def test_run_workspace_move(self):
+ new_path = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, new_path, ignore_errors=True)
+ cmd = ['tempest', 'workspace', 'move',
+ '--workspace-path', self.store_file,
+ '--name', self.name, '--path', new_path]
+ self._run_cmd_gets_return_code(cmd, 0)
+ self.assertEqual(
+ self.workspace_manager.get_workspace(self.name), new_path)
+
+ def test_run_workspace_remove_entry(self):
+ cmd = ['tempest', 'workspace', 'remove',
+ '--workspace-path', self.store_file,
+ '--name', self.name]
+ self._run_cmd_gets_return_code(cmd, 0)
+ self.assertIsNone(self.workspace_manager.get_workspace(self.name))
+
+ def test_run_workspace_remove_directory(self):
+ cmd = ['tempest', 'workspace', 'remove',
+ '--workspace-path', self.store_file,
+ '--name', self.name, '--rmdir']
+ self._run_cmd_gets_return_code(cmd, 0)
+ self.assertIsNone(self.workspace_manager.get_workspace(self.name))
+
+
+class TestTempestWorkspaceManager(TestTempestWorkspaceBase):
+ def setUp(self):
+ super(TestTempestWorkspaceManager, self).setUp()
+ self.name = data_utils.rand_uuid()
+ self.path = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, self.path, ignore_errors=True)
+ store_dir = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, store_dir, ignore_errors=True)
+ self.store_file = os.path.join(store_dir, 'workspace.yaml')
+ self.workspace_manager = workspace.WorkspaceManager(
+ path=self.store_file)
+ self.workspace_manager.register_new_workspace(self.name, self.path)
+
+ def test_workspace_manager_get(self):
+ self.assertIsNotNone(self.workspace_manager.get_workspace(self.name))
+
+ def test_workspace_manager_rename(self):
+ new_name = data_utils.rand_uuid()
+ self.workspace_manager.rename_workspace(self.name, new_name)
+ self.assertIsNone(self.workspace_manager.get_workspace(self.name))
+ self.assertIsNotNone(self.workspace_manager.get_workspace(new_name))
+
+ def test_workspace_manager_move(self):
+ new_path = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, new_path, ignore_errors=True)
+ self.workspace_manager.move_workspace(self.name, new_path)
+ self.assertEqual(
+ self.workspace_manager.get_workspace(self.name), new_path)
+
+ def test_workspace_manager_remove_entry(self):
+ self.workspace_manager.remove_workspace_entry(self.name)
+ self.assertIsNone(self.workspace_manager.get_workspace(self.name))
+
+ def test_workspace_manager_remove_directory(self):
+ path = self.workspace_manager.remove_workspace_entry(self.name)
+ self.workspace_manager.remove_workspace_directory(path)
+ self.assertIsNone(self.workspace_manager.get_workspace(self.name))
+
+ def test_path_expansion(self):
+ name = data_utils.rand_uuid()
+ path = os.path.join("~", name)
+ os.makedirs(os.path.expanduser(path))
+ self.addCleanup(shutil.rmtree, path, ignore_errors=True)
+ self.workspace_manager.register_new_workspace(name, path)
+ self.assertIsNotNone(self.workspace_manager.get_workspace(name))
+
+ def test_workspace_name_not_exists(self):
+ nonexistent_name = data_utils.rand_uuid()
+ with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
+ ex = self.assertRaises(SystemExit,
+ self.workspace_manager._name_exists,
+ nonexistent_name)
+ self.assertEqual(1, ex.code)
+ self.assertEqual(mock_stdout.getvalue(),
+ "A workspace was not found with name: %s\n" %
+ nonexistent_name)
+
+ def test_workspace_name_already_exists(self):
+ duplicate_name = self.name
+ with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
+ ex = self.assertRaises(SystemExit,
+ self.workspace_manager.
+ _workspace_name_exists,
+ duplicate_name)
+ self.assertEqual(1, ex.code)
+ self.assertEqual(mock_stdout.getvalue(),
+ "A workspace already exists with name: %s.\n"
+ % duplicate_name)
+
+ def test_workspace_manager_path_not_exist(self):
+ fake_path = "fake_path"
+ with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
+ ex = self.assertRaises(SystemExit,
+ self.workspace_manager._validate_path,
+ fake_path)
+ self.assertEqual(1, ex.code)
+ self.assertEqual(mock_stdout.getvalue(),
+ "Path does not exist.\n")
+
+ def test_workspace_manager_list_workspaces(self):
+ listed = self.workspace_manager.list_workspaces()
+ self.assertEqual(1, len(listed))
+ self.assertIn(self.name, listed)
+ self.assertEqual(self.path, listed.get(self.name))
diff --git a/tempest/tests/common/test_admin_available.py b/tempest/tests/common/test_admin_available.py
index 75401db..7b3b1b0 100644
--- a/tempest/tests/common/test_admin_available.py
+++ b/tempest/tests/common/test_admin_available.py
@@ -12,8 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+import fixtures
from oslo_config import cfg
-from oslotest import mockpatch
from tempest.common import credentials_factory as credentials
from tempest import config
@@ -28,7 +28,8 @@
def setUp(self):
super(TestAdminAvailable, self).setUp()
self.useFixture(fake_config.ConfigFixture())
- self.stubs.Set(config, 'TempestConfigPrivate', fake_config.FakePrivate)
+ self.patchobject(config, 'TempestConfigPrivate',
+ fake_config.FakePrivate)
def run_test(self, dynamic_creds, use_accounts_file, admin_creds):
@@ -36,44 +37,44 @@
dynamic_creds, group='auth')
if use_accounts_file:
accounts = [{'username': 'u1',
- 'tenant_name': 't1',
+ 'project_name': 't1',
'password': 'p'},
{'username': 'u2',
- 'tenant_name': 't2',
+ 'project_name': 't2',
'password': 'p'}]
if admin_creds == 'role':
accounts.append({'username': 'admin',
- 'tenant_name': 'admin',
+ 'project_name': 'admin',
'password': 'p',
'roles': ['admin']})
elif admin_creds == 'type':
accounts.append({'username': 'admin',
- 'tenant_name': 'admin',
+ 'project_name': 'admin',
'password': 'p',
'types': ['admin']})
- self.useFixture(mockpatch.Patch(
- 'tempest.common.preprov_creds.read_accounts_yaml',
+ self.useFixture(fixtures.MockPatch(
+ 'tempest.lib.common.preprov_creds.read_accounts_yaml',
return_value=accounts))
cfg.CONF.set_default('test_accounts_file',
use_accounts_file, group='auth')
- self.useFixture(mockpatch.Patch('os.path.isfile',
- return_value=True))
+ self.useFixture(fixtures.MockPatch('os.path.isfile',
+ return_value=True))
else:
- self.useFixture(mockpatch.Patch('os.path.isfile',
- return_value=False))
+ self.useFixture(fixtures.MockPatch('os.path.isfile',
+ return_value=False))
if admin_creds:
username = 'u'
- tenant = 't'
+ project = 't'
password = 'p'
domain = 'd'
else:
username = None
- tenant = None
+ project = None
password = None
domain = None
cfg.CONF.set_default('admin_username', username, group='auth')
- cfg.CONF.set_default('admin_tenant_name', tenant, group='auth')
+ cfg.CONF.set_default('admin_project_name', project, group='auth')
cfg.CONF.set_default('admin_password', password, group='auth')
cfg.CONF.set_default('admin_domain_name', domain, group='auth')
diff --git a/tempest/tests/common/test_alt_available.py b/tempest/tests/common/test_alt_available.py
index db3f5ec..a425bb8 100644
--- a/tempest/tests/common/test_alt_available.py
+++ b/tempest/tests/common/test_alt_available.py
@@ -12,8 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+import fixtures
from oslo_config import cfg
-from oslotest import mockpatch
from tempest.common import credentials_factory as credentials
from tempest import config
@@ -28,7 +28,8 @@
def setUp(self):
super(TestAltAvailable, self).setUp()
self.useFixture(fake_config.ConfigFixture())
- self.stubs.Set(config, 'TempestConfigPrivate', fake_config.FakePrivate)
+ self.patchobject(config, 'TempestConfigPrivate',
+ fake_config.FakePrivate)
def run_test(self, dynamic_creds, use_accounts_file, creds):
@@ -36,40 +37,18 @@
dynamic_creds, group='auth')
if use_accounts_file:
accounts = [dict(username="u%s" % ii,
- tenant_name="t%s" % ii,
+ project_name="t%s" % ii,
password="p") for ii in creds]
- self.useFixture(mockpatch.Patch(
- 'tempest.common.preprov_creds.read_accounts_yaml',
+ self.useFixture(fixtures.MockPatch(
+ 'tempest.lib.common.preprov_creds.read_accounts_yaml',
return_value=accounts))
cfg.CONF.set_default('test_accounts_file',
use_accounts_file, group='auth')
- self.useFixture(mockpatch.Patch('os.path.isfile',
- return_value=True))
+ self.useFixture(fixtures.MockPatch('os.path.isfile',
+ return_value=True))
else:
- self.useFixture(mockpatch.Patch('os.path.isfile',
- return_value=False))
- cred_prefix = ['', 'alt_']
- for ii in range(0, 2):
- if len(creds) > ii:
- username = 'u%s' % creds[ii]
- tenant = 't%s' % creds[ii]
- password = 'p'
- domain = 'd'
- else:
- username = None
- tenant = None
- password = None
- domain = None
-
- cfg.CONF.set_default('%susername' % cred_prefix[ii], username,
- group='identity')
- cfg.CONF.set_default('%stenant_name' % cred_prefix[ii], tenant,
- group='identity')
- cfg.CONF.set_default('%spassword' % cred_prefix[ii], password,
- group='identity')
- cfg.CONF.set_default('%sdomain_name' % cred_prefix[ii], domain,
- group='identity')
-
+ self.useFixture(fixtures.MockPatch('os.path.isfile',
+ return_value=False))
expected = len(set(creds)) > 1 or dynamic_creds
observed = credentials.is_alt_available(
identity_version=self.identity_version)
@@ -96,21 +75,6 @@
use_accounts_file=True,
creds=['1', '1'])
- def test__no_dynamic_creds__no_accounts_file__one_user(self):
- self.run_test(dynamic_creds=False,
- use_accounts_file=False,
- creds=['1'])
-
- def test__no_dynamic_creds__no_accounts_file__two_users(self):
- self.run_test(dynamic_creds=False,
- use_accounts_file=False,
- creds=['1', '2'])
-
- def test__no_dynamic_creds__no_accounts_file__two_users_identical(self):
- self.run_test(dynamic_creds=False,
- use_accounts_file=False,
- creds=['1', '1'])
-
class TestAltAvailableV3(TestAltAvailable):
diff --git a/tempest/tests/common/test_api_version_request.py b/tempest/tests/common/test_api_version_request.py
deleted file mode 100644
index 38fbfc1..0000000
--- a/tempest/tests/common/test_api_version_request.py
+++ /dev/null
@@ -1,146 +0,0 @@
-# Copyright 2014 IBM Corp.
-#
-# 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 api_version_request
-from tempest import exceptions
-from tempest.tests import base
-
-
-class APIVersionRequestTests(base.TestCase):
- def test_valid_version_strings(self):
- def _test_string(version, exp_major, exp_minor):
- v = api_version_request.APIVersionRequest(version)
- self.assertEqual(v.ver_major, exp_major)
- self.assertEqual(v.ver_minor, exp_minor)
-
- _test_string("1.1", 1, 1)
- _test_string("2.10", 2, 10)
- _test_string("5.234", 5, 234)
- _test_string("12.5", 12, 5)
- _test_string("2.0", 2, 0)
- _test_string("2.200", 2, 200)
-
- def test_null_version(self):
- v = api_version_request.APIVersionRequest()
- self.assertTrue(v.is_null())
-
- def test_invalid_version_strings(self):
- self.assertRaises(exceptions.InvalidAPIVersionString,
- api_version_request.APIVersionRequest, "2")
-
- self.assertRaises(exceptions.InvalidAPIVersionString,
- api_version_request.APIVersionRequest, "200")
-
- self.assertRaises(exceptions.InvalidAPIVersionString,
- api_version_request.APIVersionRequest, "2.1.4")
-
- self.assertRaises(exceptions.InvalidAPIVersionString,
- api_version_request.APIVersionRequest, "200.23.66.3")
-
- self.assertRaises(exceptions.InvalidAPIVersionString,
- api_version_request.APIVersionRequest, "5 .3")
-
- self.assertRaises(exceptions.InvalidAPIVersionString,
- api_version_request.APIVersionRequest, "5. 3")
-
- self.assertRaises(exceptions.InvalidAPIVersionString,
- api_version_request.APIVersionRequest, "5.03")
-
- self.assertRaises(exceptions.InvalidAPIVersionString,
- api_version_request.APIVersionRequest, "02.1")
-
- self.assertRaises(exceptions.InvalidAPIVersionString,
- api_version_request.APIVersionRequest, "2.001")
-
- self.assertRaises(exceptions.InvalidAPIVersionString,
- api_version_request.APIVersionRequest, "")
-
- self.assertRaises(exceptions.InvalidAPIVersionString,
- api_version_request.APIVersionRequest, " 2.1")
-
- self.assertRaises(exceptions.InvalidAPIVersionString,
- api_version_request.APIVersionRequest, "2.1 ")
-
- def test_version_comparisons(self):
- vers2_0 = api_version_request.APIVersionRequest("2.0")
- vers2_5 = api_version_request.APIVersionRequest("2.5")
- vers5_23 = api_version_request.APIVersionRequest("5.23")
- v_null = api_version_request.APIVersionRequest()
- v_latest = api_version_request.APIVersionRequest('latest')
-
- self.assertTrue(v_null < vers2_5)
- self.assertTrue(vers2_0 < vers2_5)
- self.assertTrue(vers2_0 <= vers2_5)
- self.assertTrue(vers2_0 <= vers2_0)
- self.assertTrue(vers2_5 > v_null)
- self.assertTrue(vers5_23 > vers2_5)
- self.assertTrue(vers2_0 >= vers2_0)
- self.assertTrue(vers5_23 >= vers2_5)
- self.assertTrue(vers2_0 != vers2_5)
- self.assertTrue(vers2_0 == vers2_0)
- self.assertTrue(vers2_0 != v_null)
- self.assertTrue(v_null == v_null)
- self.assertTrue(vers2_0 <= v_latest)
- self.assertTrue(vers2_0 != v_latest)
- self.assertTrue(v_latest == v_latest)
- self.assertRaises(TypeError, vers2_0.__lt__, "2.1")
-
- def test_version_matches(self):
- vers2_0 = api_version_request.APIVersionRequest("2.0")
- vers2_5 = api_version_request.APIVersionRequest("2.5")
- vers2_45 = api_version_request.APIVersionRequest("2.45")
- vers3_3 = api_version_request.APIVersionRequest("3.3")
- vers3_23 = api_version_request.APIVersionRequest("3.23")
- vers4_0 = api_version_request.APIVersionRequest("4.0")
- v_null = api_version_request.APIVersionRequest()
- v_latest = api_version_request.APIVersionRequest('latest')
-
- def _check_version_matches(version, version1, version2, check=True):
- if check:
- msg = "Version %s does not matches with [%s - %s] range"
- self.assertTrue(version.matches(version1, version2),
- msg % (version.get_string(),
- version1.get_string(),
- version2.get_string()))
- else:
- msg = "Version %s matches with [%s - %s] range"
- self.assertFalse(version.matches(version1, version2),
- msg % (version.get_string(),
- version1.get_string(),
- version2.get_string()))
-
- _check_version_matches(vers2_5, vers2_0, vers2_45)
- _check_version_matches(vers2_5, vers2_0, v_null)
- _check_version_matches(vers2_0, vers2_0, vers2_5)
- _check_version_matches(vers3_3, vers2_5, vers3_3)
- _check_version_matches(vers3_3, v_null, vers3_3)
- _check_version_matches(vers3_3, v_null, vers4_0)
- _check_version_matches(vers2_0, vers2_5, vers2_45, False)
- _check_version_matches(vers3_23, vers2_5, vers3_3, False)
- _check_version_matches(vers2_5, vers2_45, vers2_0, False)
- _check_version_matches(vers2_5, vers2_0, v_latest)
- _check_version_matches(v_latest, v_latest, v_latest)
- _check_version_matches(vers2_5, v_latest, v_latest, False)
- _check_version_matches(v_latest, vers2_0, vers4_0, False)
-
- self.assertRaises(ValueError, v_null.matches, vers2_0, vers2_45)
-
- def test_get_string(self):
- vers_string = ["3.23", "latest"]
- for ver in vers_string:
- ver_obj = api_version_request.APIVersionRequest(ver)
- self.assertEqual(ver, ver_obj.get_string())
-
- self.assertIsNotNone(
- api_version_request.APIVersionRequest().get_string)
diff --git a/tempest/tests/common/test_api_version_utils.py b/tempest/tests/common/test_api_version_utils.py
deleted file mode 100644
index 9f399a2..0000000
--- a/tempest/tests/common/test_api_version_utils.py
+++ /dev/null
@@ -1,221 +0,0 @@
-# Copyright 2015 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 oslo_config import cfg
-import testtools
-
-from tempest.api.compute import base as compute_base
-from tempest.common import api_version_utils
-from tempest import config
-from tempest import exceptions
-from tempest.tests import base
-from tempest.tests import fake_config
-
-
-class VersionTestNoneTolatest(compute_base.BaseV2ComputeTest):
- min_microversion = None
- max_microversion = 'latest'
-
-
-class VersionTestNoneTo2_2(compute_base.BaseV2ComputeTest):
- min_microversion = None
- max_microversion = '2.2'
-
-
-class VersionTest2_3ToLatest(compute_base.BaseV2ComputeTest):
- min_microversion = '2.3'
- max_microversion = 'latest'
-
-
-class VersionTest2_5To2_10(compute_base.BaseV2ComputeTest):
- min_microversion = '2.5'
- max_microversion = '2.10'
-
-
-class VersionTest2_10To2_10(compute_base.BaseV2ComputeTest):
- min_microversion = '2.10'
- max_microversion = '2.10'
-
-
-class InvalidVersionTest(compute_base.BaseV2ComputeTest):
- min_microversion = '2.11'
- max_microversion = '2.1'
-
-
-class TestMicroversionsTestsClass(base.TestCase):
-
- def setUp(self):
- super(TestMicroversionsTestsClass, self).setUp()
- self.useFixture(fake_config.ConfigFixture())
- self.stubs.Set(config, 'TempestConfigPrivate',
- fake_config.FakePrivate)
-
- def _test_version(self, cfg_min, cfg_max,
- expected_pass_tests,
- expected_skip_tests):
- cfg.CONF.set_default('min_microversion',
- cfg_min, group='compute-feature-enabled')
- cfg.CONF.set_default('max_microversion',
- cfg_max, group='compute-feature-enabled')
- try:
- for test_class in expected_pass_tests:
- test_class.skip_checks()
- for test_class in expected_skip_tests:
- self.assertRaises(testtools.TestCase.skipException,
- test_class.skip_checks)
- except testtools.TestCase.skipException as e:
- raise testtools.TestCase.failureException(e.message)
-
- def test_config_version_none_none(self):
- expected_pass_tests = [VersionTestNoneTolatest, VersionTestNoneTo2_2]
- expected_skip_tests = [VersionTest2_3ToLatest, VersionTest2_5To2_10,
- VersionTest2_10To2_10]
- self._test_version(None, None,
- expected_pass_tests,
- expected_skip_tests)
-
- def test_config_version_none_23(self):
- expected_pass_tests = [VersionTestNoneTolatest, VersionTestNoneTo2_2,
- VersionTest2_3ToLatest]
- expected_skip_tests = [VersionTest2_5To2_10, VersionTest2_10To2_10]
- self._test_version(None, '2.3',
- expected_pass_tests,
- expected_skip_tests)
-
- def test_config_version_22_latest(self):
- expected_pass_tests = [VersionTestNoneTolatest, VersionTestNoneTo2_2,
- VersionTest2_3ToLatest, VersionTest2_5To2_10,
- VersionTest2_10To2_10]
- expected_skip_tests = []
- self._test_version('2.2', 'latest',
- expected_pass_tests,
- expected_skip_tests)
-
- def test_config_version_22_23(self):
- expected_pass_tests = [VersionTestNoneTolatest, VersionTestNoneTo2_2,
- VersionTest2_3ToLatest]
- expected_skip_tests = [VersionTest2_5To2_10, VersionTest2_10To2_10]
- self._test_version('2.2', '2.3',
- expected_pass_tests,
- expected_skip_tests)
-
- def test_config_version_210_210(self):
- expected_pass_tests = [VersionTestNoneTolatest,
- VersionTest2_3ToLatest,
- VersionTest2_5To2_10,
- VersionTest2_10To2_10]
- expected_skip_tests = [VersionTestNoneTo2_2]
- self._test_version('2.10', '2.10',
- expected_pass_tests,
- expected_skip_tests)
-
- def test_config_version_none_latest(self):
- expected_pass_tests = [VersionTestNoneTolatest, VersionTestNoneTo2_2,
- VersionTest2_3ToLatest, VersionTest2_5To2_10,
- VersionTest2_10To2_10]
- expected_skip_tests = []
- self._test_version(None, 'latest',
- expected_pass_tests,
- expected_skip_tests)
-
- def test_config_version_latest_latest(self):
- expected_pass_tests = [VersionTestNoneTolatest, VersionTest2_3ToLatest]
- expected_skip_tests = [VersionTestNoneTo2_2, VersionTest2_5To2_10,
- VersionTest2_10To2_10]
- self._test_version('latest', 'latest',
- expected_pass_tests,
- expected_skip_tests)
-
- def test_config_invalid_version(self):
- cfg.CONF.set_default('min_microversion',
- '2.5', group='compute-feature-enabled')
- cfg.CONF.set_default('max_microversion',
- '2.1', group='compute-feature-enabled')
- self.assertRaises(exceptions.InvalidConfiguration,
- VersionTestNoneTolatest.skip_checks)
-
- def test_config_version_invalid_test_version(self):
- cfg.CONF.set_default('min_microversion',
- None, group='compute-feature-enabled')
- cfg.CONF.set_default('max_microversion',
- '2.13', group='compute-feature-enabled')
- self.assertRaises(exceptions.InvalidConfiguration,
- InvalidVersionTest.skip_checks)
-
-
-class TestVersionSkipLogic(base.TestCase):
-
- def _test_version(self, test_min_version, test_max_version,
- cfg_min_version, cfg_max_version, expected_skip=False):
- try:
- api_version_utils.check_skip_with_microversion(test_min_version,
- test_max_version,
- cfg_min_version,
- cfg_max_version)
- except testtools.TestCase.skipException as e:
- if not expected_skip:
- raise testtools.TestCase.failureException(e.message)
-
- def test_version_min_in_range(self):
- self._test_version('2.2', '2.10', '2.1', '2.7')
-
- def test_version_max_in_range(self):
- self._test_version('2.1', '2.3', '2.2', '2.7')
-
- def test_version_cfg_in_range(self):
- self._test_version('2.2', '2.9', '2.3', '2.7')
-
- def test_version_equal(self):
- self._test_version('2.2', '2.2', '2.2', '2.2')
-
- def test_version_below_cfg_min(self):
- self._test_version('2.2', '2.4', '2.5', '2.7', expected_skip=True)
-
- def test_version_above_cfg_max(self):
- self._test_version('2.8', '2.9', '2.3', '2.7', expected_skip=True)
-
- def test_version_min_greater_than_max(self):
- self.assertRaises(exceptions.InvalidConfiguration,
- self._test_version, '2.8', '2.7', '2.3', '2.7')
-
- def test_cfg_version_min_greater_than_max(self):
- self.assertRaises(exceptions.InvalidConfiguration,
- self._test_version, '2.2', '2.7', '2.9', '2.7')
-
-
-class TestSelectRequestMicroversion(base.TestCase):
-
- def _test_request_version(self, test_min_version,
- cfg_min_version, expected_version):
- selected_version = api_version_utils.select_request_microversion(
- test_min_version, cfg_min_version)
- self.assertEqual(expected_version, selected_version)
-
- def test_cfg_min_version_greater(self):
- self._test_request_version('2.1', '2.3', expected_version='2.3')
-
- def test_class_min_version_greater(self):
- self._test_request_version('2.5', '2.3', expected_version='2.5')
-
- def test_cfg_min_version_none(self):
- self._test_request_version('2.5', None, expected_version='2.5')
-
- def test_class_min_version_none(self):
- self._test_request_version(None, '2.3', expected_version='2.3')
-
- def test_both_min_version_none(self):
- self._test_request_version(None, None, expected_version=None)
-
- def test_both_min_version_equal(self):
- self._test_request_version('2.3', '2.3', expected_version='2.3')
diff --git a/tempest/tests/common/test_compute.py b/tempest/tests/common/test_compute.py
new file mode 100644
index 0000000..c108be9
--- /dev/null
+++ b/tempest/tests/common/test_compute.py
@@ -0,0 +1,106 @@
+# Copyright 2017 Citrix Systems
+# 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 six.moves.urllib import parse as urlparse
+
+import mock
+
+from tempest.common import compute
+from tempest.tests import base
+
+
+class TestCompute(base.TestCase):
+ def setUp(self):
+ super(TestCompute, self).setUp()
+ self.client_sock = mock.Mock()
+ self.url = urlparse.urlparse("http://www.fake.com:80")
+
+ def test_rfp_frame_not_cached(self):
+ # rfp negotiation frame arrived separately after upgrade
+ # response, so it's not cached.
+ RFP_VERSION = b'RFB.003.003\x0a'
+ rfp_frame_header = b'\x82\x0c'
+
+ self.client_sock.recv.side_effect = [
+ b'fake response start\r\n',
+ b'fake response end\r\n\r\n',
+ rfp_frame_header,
+ RFP_VERSION]
+ expect_response = b'fake response start\r\nfake response end\r\n\r\n'
+
+ webSocket = compute._WebSocket(self.client_sock, self.url)
+
+ self.assertEqual(webSocket.response, expect_response)
+ # no cache
+ self.assertEqual(webSocket.cached_stream, b'')
+ self.client_sock.recv.assert_has_calls([mock.call(4096),
+ mock.call(4096)])
+
+ self.client_sock.recv.reset_mock()
+ recv_version = webSocket.receive_frame()
+
+ self.assertEqual(recv_version, RFP_VERSION)
+ self.client_sock.recv.assert_has_calls([mock.call(2),
+ mock.call(12)])
+
+ def test_rfp_frame_fully_cached(self):
+ RFP_VERSION = b'RFB.003.003\x0a'
+ rfp_version_frame = b'\x82\x0c%s' % RFP_VERSION
+
+ self.client_sock.recv.side_effect = [
+ b'fake response start\r\n',
+ b'fake response end\r\n\r\n%s' % rfp_version_frame]
+ expect_response = b'fake response start\r\nfake response end\r\n\r\n'
+ webSocket = compute._WebSocket(self.client_sock, self.url)
+
+ self.client_sock.recv.assert_has_calls([mock.call(4096),
+ mock.call(4096)])
+ self.assertEqual(webSocket.response, expect_response)
+ self.assertEqual(webSocket.cached_stream, rfp_version_frame)
+
+ self.client_sock.recv.reset_mock()
+ recv_version = webSocket.receive_frame()
+
+ self.client_sock.recv.assert_not_called()
+ self.assertEqual(recv_version, RFP_VERSION)
+ # cached_stream should be empty in the end.
+ self.assertEqual(webSocket.cached_stream, b'')
+
+ def test_rfp_frame_partially_cached(self):
+ RFP_VERSION = b'RFB.003.003\x0a'
+ rfp_version_frame = b'\x82\x0c%s' % RFP_VERSION
+ frame_part1 = rfp_version_frame[:6]
+ frame_part2 = rfp_version_frame[6:]
+
+ self.client_sock.recv.side_effect = [
+ b'fake response start\r\n',
+ b'fake response end\r\n\r\n%s' % frame_part1,
+ frame_part2]
+ expect_response = b'fake response start\r\nfake response end\r\n\r\n'
+ webSocket = compute._WebSocket(self.client_sock, self.url)
+
+ self.client_sock.recv.assert_has_calls([mock.call(4096),
+ mock.call(4096)])
+ self.assertEqual(webSocket.response, expect_response)
+ self.assertEqual(webSocket.cached_stream, frame_part1)
+
+ self.client_sock.recv.reset_mock()
+
+ recv_version = webSocket.receive_frame()
+
+ self.client_sock.recv.assert_called_once_with(len(frame_part2))
+ self.assertEqual(recv_version, RFP_VERSION)
+ # cached_stream should be empty in the end.
+ self.assertEqual(webSocket.cached_stream, b'')
diff --git a/tempest/tests/common/test_configured_creds.py b/tempest/tests/common/test_configured_creds.py
deleted file mode 100644
index 96b75fd..0000000
--- a/tempest/tests/common/test_configured_creds.py
+++ /dev/null
@@ -1,130 +0,0 @@
-# Copyright 2015 Hewlett-Packard Development Company, L.P.
-#
-# 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_config import cfg
-from tempest_lib import auth
-from tempest_lib import exceptions as lib_exc
-from tempest_lib.services.identity.v2 import token_client as v2_client
-from tempest_lib.services.identity.v3 import token_client as v3_client
-
-from tempest.common import credentials_factory as common_creds
-from tempest.common import tempest_fixtures as fixtures
-from tempest import config
-from tempest.tests import base
-from tempest.tests import fake_config
-from tempest.tests import fake_identity
-
-
-class ConfiguredV2CredentialsTests(base.TestCase):
- attributes = {
- 'username': 'fake_username',
- 'password': 'fake_password',
- 'tenant_name': 'fake_tenant_name'
- }
-
- identity_response = fake_identity._fake_v2_response
- credentials_class = auth.KeystoneV2Credentials
- tokenclient_class = v2_client.TokenClient
- identity_version = 'v2'
-
- def setUp(self):
- super(ConfiguredV2CredentialsTests, self).setUp()
- self.useFixture(fake_config.ConfigFixture())
- self.stubs.Set(config, 'TempestConfigPrivate', fake_config.FakePrivate)
- self.stubs.Set(self.tokenclient_class, 'raw_request',
- self.identity_response)
-
- def _get_credentials(self, attributes=None):
- if attributes is None:
- attributes = self.attributes
- return self.credentials_class(**attributes)
-
- def _check(self, credentials, credentials_class, filled):
- # Check the right version of credentials has been returned
- self.assertIsInstance(credentials, credentials_class)
- # Check the id attributes are filled in
- attributes = [x for x in credentials.ATTRIBUTES if (
- '_id' in x and x != 'domain_id')]
- for attr in attributes:
- if filled:
- self.assertIsNotNone(getattr(credentials, attr))
- else:
- self.assertIsNone(getattr(credentials, attr))
-
- def _verify_credentials(self, credentials_class, filled=True,
- identity_version=None):
- for ctype in common_creds.CREDENTIAL_TYPES:
- if identity_version is None:
- creds = common_creds.get_configured_credentials(
- credential_type=ctype, fill_in=filled)
- else:
- creds = common_creds.get_configured_credentials(
- credential_type=ctype, fill_in=filled,
- identity_version=identity_version)
- self._check(creds, credentials_class, filled)
-
- def test_create(self):
- creds = self._get_credentials()
- self.assertEqual(self.attributes, creds._initial)
-
- def test_create_invalid_attr(self):
- self.assertRaises(lib_exc.InvalidCredentials,
- self._get_credentials,
- attributes=dict(invalid='fake'))
-
- def test_get_configured_credentials(self):
- self.useFixture(fixtures.LockFixture('auth_version'))
- self._verify_credentials(credentials_class=self.credentials_class)
-
- def test_get_configured_credentials_unfilled(self):
- self.useFixture(fixtures.LockFixture('auth_version'))
- self._verify_credentials(credentials_class=self.credentials_class,
- filled=False)
-
- def test_get_configured_credentials_version(self):
- # version specified and not loaded from config
- self.useFixture(fixtures.LockFixture('auth_version'))
- self._verify_credentials(credentials_class=self.credentials_class,
- identity_version=self.identity_version)
-
- def test_is_valid(self):
- creds = self._get_credentials()
- self.assertTrue(creds.is_valid())
-
-
-class ConfiguredV3CredentialsTests(ConfiguredV2CredentialsTests):
- attributes = {
- 'username': 'fake_username',
- 'password': 'fake_password',
- 'project_name': 'fake_project_name',
- 'user_domain_name': 'fake_domain_name'
- }
-
- credentials_class = auth.KeystoneV3Credentials
- identity_response = fake_identity._fake_v3_response
- tokenclient_class = v3_client.V3TokenClient
- identity_version = 'v3'
-
- def setUp(self):
- super(ConfiguredV3CredentialsTests, self).setUp()
- # Additional config items reset by cfg fixture after each test
- cfg.CONF.set_default('auth_version', 'v3', group='identity')
- # Identity group items
- for prefix in ['', 'alt_', 'admin_']:
- if prefix == 'admin_':
- group = 'auth'
- else:
- group = 'identity'
- cfg.CONF.set_default(prefix + 'domain_name', 'fake_domain_name',
- group=group)
diff --git a/tempest/tests/common/test_credentials.py b/tempest/tests/common/test_credentials.py
deleted file mode 100644
index 136ac02..0000000
--- a/tempest/tests/common/test_credentials.py
+++ /dev/null
@@ -1,36 +0,0 @@
-# Copyright 2015 Hewlett-Packard Development Company, L.P.
-#
-# 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 credentials_factory as credentials
-from tempest import config
-from tempest import exceptions
-from tempest.tests import base
-from tempest.tests import fake_config
-
-
-class TestLegacyCredentialsProvider(base.TestCase):
-
- fixed_params = {'identity_version': 'v2'}
-
- def setUp(self):
- super(TestLegacyCredentialsProvider, self).setUp()
- self.useFixture(fake_config.ConfigFixture())
- self.stubs.Set(config, 'TempestConfigPrivate', fake_config.FakePrivate)
-
- def test_get_creds_roles_legacy_invalid(self):
- test_accounts_class = credentials.LegacyCredentialProvider(
- **self.fixed_params)
- self.assertRaises(exceptions.InvalidConfiguration,
- test_accounts_class.get_creds_by_roles,
- ['fake_role'])
diff --git a/tempest/tests/common/test_credentials_factory.py b/tempest/tests/common/test_credentials_factory.py
new file mode 100644
index 0000000..7cf87f8
--- /dev/null
+++ b/tempest/tests/common/test_credentials_factory.py
@@ -0,0 +1,279 @@
+# Copyright 2017 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.
+
+import mock
+from oslo_config import cfg
+import testtools
+
+from tempest.common import credentials_factory as cf
+from tempest import config
+from tempest.lib.common import dynamic_creds
+from tempest.lib.common import preprov_creds
+from tempest.lib import exceptions
+from tempest.tests import base
+from tempest.tests import fake_config
+from tempest.tests.lib import fake_credentials
+
+
+class TestCredentialsFactory(base.TestCase):
+
+ def setUp(self):
+ super(TestCredentialsFactory, self).setUp()
+ self.useFixture(fake_config.ConfigFixture())
+ self.patchobject(config, 'TempestConfigPrivate',
+ fake_config.FakePrivate)
+
+ def test_get_dynamic_provider_params_creds_v2(self):
+ expected_uri = 'EXPECTED_V2_URI'
+ cfg.CONF.set_default('uri', expected_uri, group='identity')
+ admin_creds = fake_credentials.FakeCredentials()
+ params = cf.get_dynamic_provider_params('v2', admin_creds=admin_creds)
+ expected_params = dict(identity_uri=expected_uri,
+ admin_creds=admin_creds)
+ for key in expected_params:
+ self.assertIn(key, params)
+ self.assertEqual(expected_params[key], params[key])
+
+ def test_get_dynamic_provider_params_creds_v3(self):
+ expected_uri = 'EXPECTED_V3_URI'
+ cfg.CONF.set_default('uri_v3', expected_uri, group='identity')
+ admin_creds = fake_credentials.FakeCredentials()
+ params = cf.get_dynamic_provider_params('v3', admin_creds=admin_creds)
+ expected_params = dict(identity_uri=expected_uri,
+ admin_creds=admin_creds)
+ for key in expected_params:
+ self.assertIn(key, params)
+ self.assertEqual(expected_params[key], params[key])
+
+ def test_get_dynamic_provider_params_creds_vx(self):
+ admin_creds = fake_credentials.FakeCredentials()
+ invalid_version = 'invalid_version_x'
+ with testtools.ExpectedException(
+ exc_type=exceptions.InvalidIdentityVersion,
+ value_re='Invalid version ' + invalid_version):
+ cf.get_dynamic_provider_params(invalid_version,
+ admin_creds=admin_creds)
+
+ def test_get_dynamic_provider_params_no_creds(self):
+ expected_identity_version = 'v3'
+ with mock.patch.object(
+ cf, 'get_configured_admin_credentials') as admin_creds_mock:
+ cf.get_dynamic_provider_params(expected_identity_version)
+ admin_creds_mock.assert_called_once_with(
+ fill_in=True, identity_version=expected_identity_version)
+
+ def test_get_preprov_provider_params_creds_v2(self):
+ expected_uri = 'EXPECTED_V2_URI'
+ cfg.CONF.set_default('uri', expected_uri, group='identity')
+ params = cf.get_preprov_provider_params('v2')
+ self.assertIn('identity_uri', params)
+ self.assertEqual(expected_uri, params['identity_uri'])
+
+ def test_get_preprov_provider_params_creds_v3(self):
+ expected_uri = 'EXPECTED_V3_URI'
+ cfg.CONF.set_default('uri_v3', expected_uri, group='identity')
+ params = cf.get_preprov_provider_params('v3')
+ self.assertIn('identity_uri', params)
+ self.assertEqual(expected_uri, params['identity_uri'])
+
+ def test_get_preprov_provider_params_creds_vx(self):
+ invalid_version = 'invalid_version_x'
+ with testtools.ExpectedException(
+ exc_type=exceptions.InvalidIdentityVersion,
+ value_re='Invalid version ' + invalid_version):
+ cf.get_dynamic_provider_params(invalid_version)
+
+ @mock.patch.object(dynamic_creds, 'DynamicCredentialProvider')
+ @mock.patch.object(cf, 'get_dynamic_provider_params')
+ def test_get_credentials_provider_dynamic(
+ self, mock_dynamic_provider_params,
+ mock_dynamic_credentials_provider_class):
+ cfg.CONF.set_default('use_dynamic_credentials', True, group='auth')
+ expected_params = {'foo': 'bar'}
+ mock_dynamic_provider_params.return_value = expected_params
+ expected_name = 'my_name'
+ expected_network_resources = {'network': 'resources'}
+ expected_identity_version = 'identity_version'
+ cf.get_credentials_provider(
+ expected_name,
+ network_resources=expected_network_resources,
+ force_tenant_isolation=False,
+ identity_version=expected_identity_version)
+ mock_dynamic_provider_params.assert_called_once_with(
+ expected_identity_version)
+ mock_dynamic_credentials_provider_class.assert_called_once_with(
+ name=expected_name, network_resources=expected_network_resources,
+ **expected_params)
+
+ @mock.patch.object(preprov_creds, 'PreProvisionedCredentialProvider')
+ @mock.patch.object(cf, 'get_preprov_provider_params')
+ def test_get_credentials_provider_preprov(
+ self, mock_preprov_provider_params,
+ mock_preprov_credentials_provider_class):
+ cfg.CONF.set_default('use_dynamic_credentials', False, group='auth')
+ cfg.CONF.set_default('test_accounts_file', '/some/file', group='auth')
+ expected_params = {'foo': 'bar'}
+ mock_preprov_provider_params.return_value = expected_params
+ expected_name = 'my_name'
+ expected_identity_version = 'identity_version'
+ cf.get_credentials_provider(
+ expected_name,
+ force_tenant_isolation=False,
+ identity_version=expected_identity_version)
+ mock_preprov_provider_params.assert_called_once_with(
+ expected_identity_version)
+ mock_preprov_credentials_provider_class.assert_called_once_with(
+ name=expected_name, **expected_params)
+
+ def test_get_credentials_provider_preprov_no_file(self):
+ cfg.CONF.set_default('use_dynamic_credentials', False, group='auth')
+ cfg.CONF.set_default('test_accounts_file', None, group='auth')
+ with testtools.ExpectedException(
+ exc_type=exceptions.InvalidConfiguration):
+ cf.get_credentials_provider(
+ 'some_name',
+ force_tenant_isolation=False,
+ identity_version='some_version')
+
+ @mock.patch.object(dynamic_creds, 'DynamicCredentialProvider')
+ @mock.patch.object(cf, 'get_dynamic_provider_params')
+ def test_get_credentials_provider_force_dynamic(
+ self, mock_dynamic_provider_params,
+ mock_dynamic_credentials_provider_class):
+ cfg.CONF.set_default('use_dynamic_credentials', False, group='auth')
+ expected_params = {'foo': 'bar'}
+ mock_dynamic_provider_params.return_value = expected_params
+ expected_name = 'my_name'
+ expected_network_resources = {'network': 'resources'}
+ expected_identity_version = 'identity_version'
+ cf.get_credentials_provider(
+ expected_name,
+ network_resources=expected_network_resources,
+ force_tenant_isolation=True,
+ identity_version=expected_identity_version)
+ mock_dynamic_provider_params.assert_called_once_with(
+ expected_identity_version)
+ mock_dynamic_credentials_provider_class.assert_called_once_with(
+ name=expected_name, network_resources=expected_network_resources,
+ **expected_params)
+
+ @mock.patch.object(cf, 'get_credentials')
+ def test_get_configured_admin_credentials(self, mock_get_credentials):
+ cfg.CONF.set_default('auth_version', 'v3', 'identity')
+ all_params = [('admin_username', 'username', 'my_name'),
+ ('admin_password', 'password', 'secret'),
+ ('admin_project_name', 'project_name', 'my_pname'),
+ ('admin_domain_name', 'domain_name', 'my_dname')]
+ expected_result = 'my_admin_credentials'
+ mock_get_credentials.return_value = expected_result
+ for config_item, _, value in all_params:
+ cfg.CONF.set_default(config_item, value, 'auth')
+ # Build the expected params
+ expected_params = dict(
+ [(field, value) for _, field, value in all_params])
+ expected_params.update(config.service_client_config())
+ admin_creds = cf.get_configured_admin_credentials()
+ mock_get_credentials.assert_called_once_with(
+ fill_in=True, identity_version='v3', **expected_params)
+ self.assertEqual(expected_result, admin_creds)
+
+ @mock.patch.object(cf, 'get_credentials')
+ def test_get_configured_admin_credentials_not_fill_valid(
+ self, mock_get_credentials):
+ cfg.CONF.set_default('auth_version', 'v2', 'identity')
+ all_params = [('admin_username', 'username', 'my_name'),
+ ('admin_password', 'password', 'secret'),
+ ('admin_project_name', 'project_name', 'my_pname'),
+ ('admin_domain_name', 'domain_name', 'my_dname')]
+ expected_result = mock.Mock()
+ expected_result.is_valid.return_value = True
+ mock_get_credentials.return_value = expected_result
+ for config_item, _, value in all_params:
+ cfg.CONF.set_default(config_item, value, 'auth')
+ # Build the expected params
+ expected_params = dict(
+ [(field, value) for _, field, value in all_params])
+ expected_params.update(config.service_client_config())
+ admin_creds = cf.get_configured_admin_credentials(
+ fill_in=False, identity_version='v3')
+ mock_get_credentials.assert_called_once_with(
+ fill_in=False, identity_version='v3', **expected_params)
+ self.assertEqual(expected_result, admin_creds)
+ expected_result.is_valid.assert_called_once()
+
+ @mock.patch.object(cf, 'get_credentials')
+ def test_get_configured_admin_credentials_not_fill_not_valid(
+ self, mock_get_credentials):
+ cfg.CONF.set_default('auth_version', 'v2', 'identity')
+ expected_result = mock.Mock()
+ expected_result.is_valid.return_value = False
+ mock_get_credentials.return_value = expected_result
+ with testtools.ExpectedException(exceptions.InvalidConfiguration,
+ value_re='.*\n.*identity version v2'):
+ cf.get_configured_admin_credentials(fill_in=False)
+
+ @mock.patch('tempest.lib.auth.get_credentials')
+ def test_get_credentials_v2(self, mock_auth_get_credentials):
+ expected_uri = 'V2_URI'
+ expected_result = 'my_creds'
+ mock_auth_get_credentials.return_value = expected_result
+ cfg.CONF.set_default('uri', expected_uri, 'identity')
+ params = {'foo': 'bar'}
+ expected_params = params.copy()
+ expected_params.update(config.service_client_config())
+ result = cf.get_credentials(identity_version='v2', **params)
+ self.assertEqual(expected_result, result)
+ mock_auth_get_credentials.assert_called_once_with(
+ expected_uri, fill_in=True, identity_version='v2',
+ **expected_params)
+
+ @mock.patch('tempest.lib.auth.get_credentials')
+ def test_get_credentials_v3_no_domain(self, mock_auth_get_credentials):
+ expected_uri = 'V3_URI'
+ expected_result = 'my_creds'
+ expected_domain = 'my_domain'
+ mock_auth_get_credentials.return_value = expected_result
+ cfg.CONF.set_default('uri_v3', expected_uri, 'identity')
+ cfg.CONF.set_default('default_credentials_domain_name',
+ expected_domain, 'auth')
+ params = {'foo': 'bar'}
+ expected_params = params.copy()
+ expected_params['domain_name'] = expected_domain
+ expected_params.update(config.service_client_config())
+ result = cf.get_credentials(fill_in=False, identity_version='v3',
+ **params)
+ self.assertEqual(expected_result, result)
+ mock_auth_get_credentials.assert_called_once_with(
+ expected_uri, fill_in=False, identity_version='v3',
+ **expected_params)
+
+ @mock.patch('tempest.lib.auth.get_credentials')
+ def test_get_credentials_v3_domain(self, mock_auth_get_credentials):
+ expected_uri = 'V3_URI'
+ expected_result = 'my_creds'
+ expected_domain = 'my_domain'
+ mock_auth_get_credentials.return_value = expected_result
+ cfg.CONF.set_default('uri_v3', expected_uri, 'identity')
+ cfg.CONF.set_default('default_credentials_domain_name',
+ expected_domain, 'auth')
+ params = {'foo': 'bar', 'user_domain_name': expected_domain}
+ expected_params = params.copy()
+ expected_params.update(config.service_client_config())
+ result = cf.get_credentials(fill_in=False, identity_version='v3',
+ **params)
+ self.assertEqual(expected_result, result)
+ mock_auth_get_credentials.assert_called_once_with(
+ expected_uri, fill_in=False, identity_version='v3',
+ **expected_params)
diff --git a/tempest/tests/common/test_custom_matchers.py b/tempest/tests/common/test_custom_matchers.py
index 2656a47..1053d86 100644
--- a/tempest/tests/common/test_custom_matchers.py
+++ b/tempest/tests/common/test_custom_matchers.py
@@ -16,11 +16,47 @@
from tempest.common import custom_matchers
from tempest.tests import base
-from testtools.tests.matchers import helpers
+
+# Stolen from testtools/testtools/tests/matchers/helpers.py
+class TestMatchersInterface(object):
+
+ def test_matches_match(self):
+ matcher = self.matches_matcher
+ matches = self.matches_matches
+ mismatches = self.matches_mismatches
+ for candidate in matches:
+ self.assertIsNone(matcher.match(candidate))
+ for candidate in mismatches:
+ mismatch = matcher.match(candidate)
+ self.assertIsNotNone(mismatch)
+ self.assertIsNotNone(getattr(mismatch, 'describe', None))
+
+ def test__str__(self):
+ # [(expected, object to __str__)].
+ from testtools.matchers._doctest import DocTestMatches
+ examples = self.str_examples
+ for expected, matcher in examples:
+ self.assertThat(matcher, DocTestMatches(expected))
+
+ def test_describe_difference(self):
+ # [(expected, matchee, matcher), ...]
+ examples = self.describe_examples
+ for difference, matchee, matcher in examples:
+ mismatch = matcher.match(matchee)
+ self.assertEqual(difference, mismatch.describe())
+
+ def test_mismatch_details(self):
+ # The mismatch object must provide get_details, which must return a
+ # dictionary mapping names to Content objects.
+ examples = self.describe_examples
+ for difference, matchee, matcher in examples:
+ mismatch = matcher.match(matchee)
+ details = mismatch.get_details()
+ self.assertEqual(dict(details), details)
class TestMatchesDictExceptForKeys(base.TestCase,
- helpers.TestMatchersInterface):
+ TestMatchersInterface):
matches_matcher = custom_matchers.MatchesDictExceptForKeys(
{'a': 1, 'b': 2, 'c': 3, 'd': 4}, ['c', 'd'])
diff --git a/tempest/tests/common/test_dynamic_creds.py b/tempest/tests/common/test_dynamic_creds.py
deleted file mode 100644
index a55e556..0000000
--- a/tempest/tests/common/test_dynamic_creds.py
+++ /dev/null
@@ -1,588 +0,0 @@
-# Copyright 2014 IBM Corp.
-#
-# 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 mock
-from oslo_config import cfg
-from oslotest import mockpatch
-from tempest_lib.services.identity.v2 import token_client as json_token_client
-
-from tempest.common import credentials_factory as credentials
-from tempest.common import dynamic_creds
-from tempest.common import service_client
-from tempest import config
-from tempest import exceptions
-from tempest.services.identity.v2.json import identity_client as \
- json_iden_client
-from tempest.services.identity.v2.json import roles_client as \
- json_roles_client
-from tempest.services.identity.v2.json import tenants_client as \
- json_tenants_client
-from tempest.services.network.json import network_client as json_network_client
-from tempest.tests import base
-from tempest.tests import fake_config
-from tempest.tests import fake_http
-from tempest.tests import fake_identity
-
-
-class TestDynamicCredentialProvider(base.TestCase):
-
- fixed_params = {'name': 'test class',
- 'identity_version': 'v2',
- 'admin_role': 'admin'}
-
- def setUp(self):
- super(TestDynamicCredentialProvider, self).setUp()
- self.useFixture(fake_config.ConfigFixture())
- self.stubs.Set(config, 'TempestConfigPrivate', fake_config.FakePrivate)
- self.fake_http = fake_http.fake_httplib2(return_type=200)
- self.stubs.Set(json_token_client.TokenClient, 'raw_request',
- fake_identity._fake_v2_response)
- cfg.CONF.set_default('operator_role', 'FakeRole',
- group='object-storage')
- self._mock_list_ec2_credentials('fake_user_id', 'fake_tenant_id')
- self.fixed_params.update(
- admin_creds=self._get_fake_admin_creds())
-
- def test_tempest_client(self):
- creds = dynamic_creds.DynamicCredentialProvider(**self.fixed_params)
- self.assertTrue(isinstance(creds.identity_admin_client,
- json_iden_client.IdentityClient))
- self.assertTrue(isinstance(creds.network_admin_client,
- json_network_client.NetworkClient))
-
- def _get_fake_admin_creds(self):
- return credentials.get_credentials(
- fill_in=False,
- identity_version=self.fixed_params['identity_version'],
- username='fake_username', password='fake_password',
- tenant_name='fake_tenant')
-
- def _mock_user_create(self, id, name):
- user_fix = self.useFixture(mockpatch.PatchObject(
- json_iden_client.IdentityClient,
- 'create_user',
- return_value=(service_client.ResponseBody
- (200, {'user': {'id': id, 'name': name}}))))
- return user_fix
-
- def _mock_tenant_create(self, id, name):
- tenant_fix = self.useFixture(mockpatch.PatchObject(
- json_tenants_client.TenantsClient,
- 'create_tenant',
- return_value=(service_client.ResponseBody
- (200, {'tenant': {'id': id, 'name': name}}))))
- return tenant_fix
-
- def _mock_list_roles(self, id, name):
- roles_fix = self.useFixture(mockpatch.PatchObject(
- json_roles_client.RolesClient,
- 'list_roles',
- return_value=(service_client.ResponseBody
- (200,
- {'roles': [{'id': id, 'name': name},
- {'id': '1', 'name': 'FakeRole'},
- {'id': '2', 'name': 'Member'}]}))))
- return roles_fix
-
- def _mock_list_2_roles(self):
- roles_fix = self.useFixture(mockpatch.PatchObject(
- json_roles_client.RolesClient,
- 'list_roles',
- return_value=(service_client.ResponseBody
- (200,
- {'roles': [{'id': '1234', 'name': 'role1'},
- {'id': '1', 'name': 'FakeRole'},
- {'id': '12345', 'name': 'role2'}]}))))
- return roles_fix
-
- def _mock_assign_user_role(self):
- tenant_fix = self.useFixture(mockpatch.PatchObject(
- json_roles_client.RolesClient,
- 'assign_user_role',
- return_value=(service_client.ResponseBody
- (200, {}))))
- return tenant_fix
-
- def _mock_list_role(self):
- roles_fix = self.useFixture(mockpatch.PatchObject(
- json_roles_client.RolesClient,
- 'list_roles',
- return_value=(service_client.ResponseBody
- (200, {'roles': [{'id': '1',
- 'name': 'FakeRole'}]}))))
- return roles_fix
-
- def _mock_list_ec2_credentials(self, user_id, tenant_id):
- ec2_creds_fix = self.useFixture(mockpatch.PatchObject(
- json_iden_client.IdentityClient,
- 'list_user_ec2_credentials',
- return_value=(service_client.ResponseBody
- (200, {'credentials': [{
- 'access': 'fake_access',
- 'secret': 'fake_secret',
- 'tenant_id': tenant_id,
- 'user_id': user_id,
- 'trust_id': None}]}))))
- return ec2_creds_fix
-
- def _mock_network_create(self, iso_creds, id, name):
- net_fix = self.useFixture(mockpatch.PatchObject(
- iso_creds.networks_admin_client,
- 'create_network',
- return_value={'network': {'id': id, 'name': name}}))
- return net_fix
-
- def _mock_subnet_create(self, iso_creds, id, name):
- subnet_fix = self.useFixture(mockpatch.PatchObject(
- iso_creds.subnets_admin_client,
- 'create_subnet',
- return_value={'subnet': {'id': id, 'name': name}}))
- return subnet_fix
-
- def _mock_router_create(self, id, name):
- router_fix = self.useFixture(mockpatch.PatchObject(
- json_network_client.NetworkClient,
- 'create_router',
- return_value={'router': {'id': id, 'name': name}}))
- return router_fix
-
- @mock.patch('tempest_lib.common.rest_client.RestClient')
- def test_primary_creds(self, MockRestClient):
- cfg.CONF.set_default('neutron', False, 'service_available')
- creds = dynamic_creds.DynamicCredentialProvider(**self.fixed_params)
- self._mock_assign_user_role()
- self._mock_list_role()
- self._mock_tenant_create('1234', 'fake_prim_tenant')
- self._mock_user_create('1234', 'fake_prim_user')
- primary_creds = creds.get_primary_creds()
- self.assertEqual(primary_creds.username, 'fake_prim_user')
- self.assertEqual(primary_creds.tenant_name, 'fake_prim_tenant')
- # Verify IDs
- self.assertEqual(primary_creds.tenant_id, '1234')
- self.assertEqual(primary_creds.user_id, '1234')
-
- @mock.patch('tempest_lib.common.rest_client.RestClient')
- def test_admin_creds(self, MockRestClient):
- cfg.CONF.set_default('neutron', False, 'service_available')
- creds = dynamic_creds.DynamicCredentialProvider(**self.fixed_params)
- self._mock_list_roles('1234', 'admin')
- self._mock_user_create('1234', 'fake_admin_user')
- self._mock_tenant_create('1234', 'fake_admin_tenant')
-
- user_mock = mock.patch.object(json_roles_client.RolesClient,
- 'assign_user_role')
- user_mock.start()
- self.addCleanup(user_mock.stop)
- with mock.patch.object(json_roles_client.RolesClient,
- 'assign_user_role') as user_mock:
- admin_creds = creds.get_admin_creds()
- user_mock.assert_has_calls([
- mock.call('1234', '1234', '1234')])
- self.assertEqual(admin_creds.username, 'fake_admin_user')
- self.assertEqual(admin_creds.tenant_name, 'fake_admin_tenant')
- # Verify IDs
- self.assertEqual(admin_creds.tenant_id, '1234')
- self.assertEqual(admin_creds.user_id, '1234')
-
- @mock.patch('tempest_lib.common.rest_client.RestClient')
- def test_role_creds(self, MockRestClient):
- cfg.CONF.set_default('neutron', False, 'service_available')
- creds = dynamic_creds.DynamicCredentialProvider(**self.fixed_params)
- self._mock_list_2_roles()
- self._mock_user_create('1234', 'fake_role_user')
- self._mock_tenant_create('1234', 'fake_role_tenant')
-
- user_mock = mock.patch.object(json_roles_client.RolesClient,
- 'assign_user_role')
- user_mock.start()
- self.addCleanup(user_mock.stop)
- with mock.patch.object(json_roles_client.RolesClient,
- 'assign_user_role') as user_mock:
- role_creds = creds.get_creds_by_roles(
- roles=['role1', 'role2'])
- calls = user_mock.mock_calls
- # Assert that the role creation is called with the 2 specified roles
- self.assertEqual(len(calls), 2)
- args = map(lambda x: x[1], calls)
- args = list(args)
- self.assertIn(('1234', '1234', '1234'), args)
- self.assertIn(('1234', '1234', '12345'), args)
- self.assertEqual(role_creds.username, 'fake_role_user')
- self.assertEqual(role_creds.tenant_name, 'fake_role_tenant')
- # Verify IDs
- self.assertEqual(role_creds.tenant_id, '1234')
- self.assertEqual(role_creds.user_id, '1234')
-
- @mock.patch('tempest_lib.common.rest_client.RestClient')
- def test_all_cred_cleanup(self, MockRestClient):
- cfg.CONF.set_default('neutron', False, 'service_available')
- creds = dynamic_creds.DynamicCredentialProvider(**self.fixed_params)
- self._mock_assign_user_role()
- self._mock_list_role()
- self._mock_tenant_create('1234', 'fake_prim_tenant')
- self._mock_user_create('1234', 'fake_prim_user')
- creds.get_primary_creds()
- self._mock_tenant_create('12345', 'fake_alt_tenant')
- self._mock_user_create('12345', 'fake_alt_user')
- creds.get_alt_creds()
- self._mock_tenant_create('123456', 'fake_admin_tenant')
- self._mock_user_create('123456', 'fake_admin_user')
- self._mock_list_roles('123456', 'admin')
- creds.get_admin_creds()
- user_mock = self.patch(
- 'tempest.services.identity.v2.json.identity_client.'
- 'IdentityClient.delete_user')
- tenant_mock = self.patch(
- 'tempest.services.identity.v2.json.tenants_client.'
- 'TenantsClient.delete_tenant')
- creds.clear_creds()
- # Verify user delete calls
- calls = user_mock.mock_calls
- self.assertEqual(len(calls), 3)
- args = map(lambda x: x[1][0], calls)
- args = list(args)
- self.assertIn('1234', args)
- self.assertIn('12345', args)
- self.assertIn('123456', args)
- # Verify tenant delete calls
- calls = tenant_mock.mock_calls
- self.assertEqual(len(calls), 3)
- args = map(lambda x: x[1][0], calls)
- args = list(args)
- self.assertIn('1234', args)
- self.assertIn('12345', args)
- self.assertIn('123456', args)
-
- @mock.patch('tempest_lib.common.rest_client.RestClient')
- def test_alt_creds(self, MockRestClient):
- cfg.CONF.set_default('neutron', False, 'service_available')
- creds = dynamic_creds.DynamicCredentialProvider(**self.fixed_params)
- self._mock_assign_user_role()
- self._mock_list_role()
- self._mock_user_create('1234', 'fake_alt_user')
- self._mock_tenant_create('1234', 'fake_alt_tenant')
- alt_creds = creds.get_alt_creds()
- self.assertEqual(alt_creds.username, 'fake_alt_user')
- self.assertEqual(alt_creds.tenant_name, 'fake_alt_tenant')
- # Verify IDs
- self.assertEqual(alt_creds.tenant_id, '1234')
- self.assertEqual(alt_creds.user_id, '1234')
-
- @mock.patch('tempest_lib.common.rest_client.RestClient')
- def test_no_network_creation_with_config_set(self, MockRestClient):
- cfg.CONF.set_default('create_isolated_networks', False, group='auth')
- creds = dynamic_creds.DynamicCredentialProvider(**self.fixed_params)
- self._mock_assign_user_role()
- self._mock_list_role()
- self._mock_user_create('1234', 'fake_prim_user')
- self._mock_tenant_create('1234', 'fake_prim_tenant')
- net = mock.patch.object(creds.networks_admin_client,
- 'delete_network')
- net_mock = net.start()
- subnet = mock.patch.object(creds.subnets_admin_client,
- 'delete_subnet')
- subnet_mock = subnet.start()
- router = mock.patch.object(creds.network_admin_client,
- 'delete_router')
- router_mock = router.start()
-
- primary_creds = creds.get_primary_creds()
- self.assertEqual(net_mock.mock_calls, [])
- self.assertEqual(subnet_mock.mock_calls, [])
- self.assertEqual(router_mock.mock_calls, [])
- network = primary_creds.network
- subnet = primary_creds.subnet
- router = primary_creds.router
- self.assertIsNone(network)
- self.assertIsNone(subnet)
- self.assertIsNone(router)
-
- @mock.patch('tempest_lib.common.rest_client.RestClient')
- def test_network_creation(self, MockRestClient):
- creds = dynamic_creds.DynamicCredentialProvider(**self.fixed_params)
- self._mock_assign_user_role()
- self._mock_list_role()
- self._mock_user_create('1234', 'fake_prim_user')
- self._mock_tenant_create('1234', 'fake_prim_tenant')
- self._mock_network_create(creds, '1234', 'fake_net')
- self._mock_subnet_create(creds, '1234', 'fake_subnet')
- self._mock_router_create('1234', 'fake_router')
- router_interface_mock = self.patch(
- 'tempest.services.network.json.network_client.NetworkClient.'
- 'add_router_interface_with_subnet_id')
- primary_creds = creds.get_primary_creds()
- router_interface_mock.called_once_with('1234', '1234')
- network = primary_creds.network
- subnet = primary_creds.subnet
- router = primary_creds.router
- self.assertEqual(network['id'], '1234')
- self.assertEqual(network['name'], 'fake_net')
- self.assertEqual(subnet['id'], '1234')
- self.assertEqual(subnet['name'], 'fake_subnet')
- self.assertEqual(router['id'], '1234')
- self.assertEqual(router['name'], 'fake_router')
-
- @mock.patch('tempest_lib.common.rest_client.RestClient')
- def test_network_cleanup(self, MockRestClient):
- def side_effect(**args):
- return {"security_groups": [{"tenant_id": args['tenant_id'],
- "name": args['name'],
- "description": args['name'],
- "security_group_rules": [],
- "id": "sg-%s" % args['tenant_id']}]}
- creds = dynamic_creds.DynamicCredentialProvider(**self.fixed_params)
- # Create primary tenant and network
- self._mock_assign_user_role()
- self._mock_list_role()
- self._mock_user_create('1234', 'fake_prim_user')
- self._mock_tenant_create('1234', 'fake_prim_tenant')
- self._mock_network_create(creds, '1234', 'fake_net')
- self._mock_subnet_create(creds, '1234', 'fake_subnet')
- self._mock_router_create('1234', 'fake_router')
- router_interface_mock = self.patch(
- 'tempest.services.network.json.network_client.NetworkClient.'
- 'add_router_interface_with_subnet_id')
- creds.get_primary_creds()
- router_interface_mock.called_once_with('1234', '1234')
- router_interface_mock.reset_mock()
- # Create alternate tenant and network
- self._mock_user_create('12345', 'fake_alt_user')
- self._mock_tenant_create('12345', 'fake_alt_tenant')
- self._mock_network_create(creds, '12345', 'fake_alt_net')
- self._mock_subnet_create(creds, '12345', 'fake_alt_subnet')
- self._mock_router_create('12345', 'fake_alt_router')
- creds.get_alt_creds()
- router_interface_mock.called_once_with('12345', '12345')
- router_interface_mock.reset_mock()
- # Create admin tenant and networks
- self._mock_user_create('123456', 'fake_admin_user')
- self._mock_tenant_create('123456', 'fake_admin_tenant')
- self._mock_network_create(creds, '123456', 'fake_admin_net')
- self._mock_subnet_create(creds, '123456', 'fake_admin_subnet')
- self._mock_router_create('123456', 'fake_admin_router')
- self._mock_list_roles('123456', 'admin')
- creds.get_admin_creds()
- self.patch('tempest.services.identity.v2.json.identity_client.'
- 'IdentityClient.delete_user')
- self.patch('tempest.services.identity.v2.json.tenants_client.'
- 'TenantsClient.delete_tenant')
- net = mock.patch.object(creds.networks_admin_client,
- 'delete_network')
- net_mock = net.start()
- subnet = mock.patch.object(creds.subnets_admin_client,
- 'delete_subnet')
- subnet_mock = subnet.start()
- router = mock.patch.object(creds.network_admin_client,
- 'delete_router')
- router_mock = router.start()
- remove_router_interface_mock = self.patch(
- 'tempest.services.network.json.network_client.NetworkClient.'
- 'remove_router_interface_with_subnet_id')
- return_values = ({'status': 200}, {'ports': []})
- port_list_mock = mock.patch.object(creds.ports_admin_client,
- 'list_ports',
- return_value=return_values)
-
- port_list_mock.start()
- secgroup_list_mock = mock.patch.object(
- creds.security_groups_admin_client,
- 'list_security_groups',
- side_effect=side_effect)
- secgroup_list_mock.start()
-
- return_values = (fake_http.fake_httplib({}, status=204), {})
- remove_secgroup_mock = self.patch(
- 'tempest.services.network.json.security_groups_client.'
- 'SecurityGroupsClient.delete', return_value=return_values)
- creds.clear_creds()
- # Verify default security group delete
- calls = remove_secgroup_mock.mock_calls
- self.assertEqual(len(calls), 3)
- args = map(lambda x: x[1][0], calls)
- args = list(args)
- self.assertIn('v2.0/security-groups/sg-1234', args)
- self.assertIn('v2.0/security-groups/sg-12345', args)
- self.assertIn('v2.0/security-groups/sg-123456', args)
- # Verify remove router interface calls
- calls = remove_router_interface_mock.mock_calls
- self.assertEqual(len(calls), 3)
- args = map(lambda x: x[1], calls)
- args = list(args)
- self.assertIn(('1234', '1234'), args)
- self.assertIn(('12345', '12345'), args)
- self.assertIn(('123456', '123456'), args)
- # Verify network delete calls
- calls = net_mock.mock_calls
- self.assertEqual(len(calls), 3)
- args = map(lambda x: x[1][0], calls)
- args = list(args)
- self.assertIn('1234', args)
- self.assertIn('12345', args)
- self.assertIn('123456', args)
- # Verify subnet delete calls
- calls = subnet_mock.mock_calls
- self.assertEqual(len(calls), 3)
- args = map(lambda x: x[1][0], calls)
- args = list(args)
- self.assertIn('1234', args)
- self.assertIn('12345', args)
- self.assertIn('123456', args)
- # Verify router delete calls
- calls = router_mock.mock_calls
- self.assertEqual(len(calls), 3)
- args = map(lambda x: x[1][0], calls)
- args = list(args)
- self.assertIn('1234', args)
- self.assertIn('12345', args)
- self.assertIn('123456', args)
-
- @mock.patch('tempest_lib.common.rest_client.RestClient')
- def test_network_alt_creation(self, MockRestClient):
- creds = dynamic_creds.DynamicCredentialProvider(**self.fixed_params)
- self._mock_assign_user_role()
- self._mock_list_role()
- self._mock_user_create('1234', 'fake_alt_user')
- self._mock_tenant_create('1234', 'fake_alt_tenant')
- self._mock_network_create(creds, '1234', 'fake_alt_net')
- self._mock_subnet_create(creds, '1234', 'fake_alt_subnet')
- self._mock_router_create('1234', 'fake_alt_router')
- router_interface_mock = self.patch(
- 'tempest.services.network.json.network_client.NetworkClient.'
- 'add_router_interface_with_subnet_id')
- alt_creds = creds.get_alt_creds()
- router_interface_mock.called_once_with('1234', '1234')
- network = alt_creds.network
- subnet = alt_creds.subnet
- router = alt_creds.router
- self.assertEqual(network['id'], '1234')
- self.assertEqual(network['name'], 'fake_alt_net')
- self.assertEqual(subnet['id'], '1234')
- self.assertEqual(subnet['name'], 'fake_alt_subnet')
- self.assertEqual(router['id'], '1234')
- self.assertEqual(router['name'], 'fake_alt_router')
-
- @mock.patch('tempest_lib.common.rest_client.RestClient')
- def test_network_admin_creation(self, MockRestClient):
- creds = dynamic_creds.DynamicCredentialProvider(**self.fixed_params)
- self._mock_assign_user_role()
- self._mock_user_create('1234', 'fake_admin_user')
- self._mock_tenant_create('1234', 'fake_admin_tenant')
- self._mock_network_create(creds, '1234', 'fake_admin_net')
- self._mock_subnet_create(creds, '1234', 'fake_admin_subnet')
- self._mock_router_create('1234', 'fake_admin_router')
- router_interface_mock = self.patch(
- 'tempest.services.network.json.network_client.NetworkClient.'
- 'add_router_interface_with_subnet_id')
- self._mock_list_roles('123456', 'admin')
- admin_creds = creds.get_admin_creds()
- router_interface_mock.called_once_with('1234', '1234')
- network = admin_creds.network
- subnet = admin_creds.subnet
- router = admin_creds.router
- self.assertEqual(network['id'], '1234')
- self.assertEqual(network['name'], 'fake_admin_net')
- self.assertEqual(subnet['id'], '1234')
- self.assertEqual(subnet['name'], 'fake_admin_subnet')
- self.assertEqual(router['id'], '1234')
- self.assertEqual(router['name'], 'fake_admin_router')
-
- @mock.patch('tempest_lib.common.rest_client.RestClient')
- def test_no_network_resources(self, MockRestClient):
- net_dict = {
- 'network': False,
- 'router': False,
- 'subnet': False,
- 'dhcp': False,
- }
- creds = dynamic_creds.DynamicCredentialProvider(
- network_resources=net_dict,
- **self.fixed_params)
- self._mock_assign_user_role()
- self._mock_list_role()
- self._mock_user_create('1234', 'fake_prim_user')
- self._mock_tenant_create('1234', 'fake_prim_tenant')
- net = mock.patch.object(creds.networks_admin_client,
- 'delete_network')
- net_mock = net.start()
- subnet = mock.patch.object(creds.subnets_admin_client,
- 'delete_subnet')
- subnet_mock = subnet.start()
- router = mock.patch.object(creds.network_admin_client,
- 'delete_router')
- router_mock = router.start()
-
- primary_creds = creds.get_primary_creds()
- self.assertEqual(net_mock.mock_calls, [])
- self.assertEqual(subnet_mock.mock_calls, [])
- self.assertEqual(router_mock.mock_calls, [])
- network = primary_creds.network
- subnet = primary_creds.subnet
- router = primary_creds.router
- self.assertIsNone(network)
- self.assertIsNone(subnet)
- self.assertIsNone(router)
-
- @mock.patch('tempest_lib.common.rest_client.RestClient')
- def test_router_without_network(self, MockRestClient):
- net_dict = {
- 'network': False,
- 'router': True,
- 'subnet': False,
- 'dhcp': False,
- }
- creds = dynamic_creds.DynamicCredentialProvider(
- network_resources=net_dict,
- **self.fixed_params)
- self._mock_assign_user_role()
- self._mock_list_role()
- self._mock_user_create('1234', 'fake_prim_user')
- self._mock_tenant_create('1234', 'fake_prim_tenant')
- self.assertRaises(exceptions.InvalidConfiguration,
- creds.get_primary_creds)
-
- @mock.patch('tempest_lib.common.rest_client.RestClient')
- def test_subnet_without_network(self, MockRestClient):
- net_dict = {
- 'network': False,
- 'router': False,
- 'subnet': True,
- 'dhcp': False,
- }
- creds = dynamic_creds.DynamicCredentialProvider(
- network_resources=net_dict,
- **self.fixed_params)
- self._mock_assign_user_role()
- self._mock_list_role()
- self._mock_user_create('1234', 'fake_prim_user')
- self._mock_tenant_create('1234', 'fake_prim_tenant')
- self.assertRaises(exceptions.InvalidConfiguration,
- creds.get_primary_creds)
-
- @mock.patch('tempest_lib.common.rest_client.RestClient')
- def test_dhcp_without_subnet(self, MockRestClient):
- net_dict = {
- 'network': False,
- 'router': False,
- 'subnet': False,
- 'dhcp': True,
- }
- creds = dynamic_creds.DynamicCredentialProvider(
- network_resources=net_dict,
- **self.fixed_params)
- self._mock_assign_user_role()
- self._mock_list_role()
- self._mock_user_create('1234', 'fake_prim_user')
- self._mock_tenant_create('1234', 'fake_prim_tenant')
- self.assertRaises(exceptions.InvalidConfiguration,
- creds.get_primary_creds)
diff --git a/tempest/tests/common/test_image.py b/tempest/tests/common/test_image.py
new file mode 100644
index 0000000..240df4d
--- /dev/null
+++ b/tempest/tests/common/test_image.py
@@ -0,0 +1,61 @@
+# Copyright 2016 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.common import image
+from tempest.lib.common import rest_client
+from tempest.tests import base
+
+
+class TestImage(base.TestCase):
+
+ def test_get_image_meta_from_headers(self):
+ resp = {
+ 'x-image-meta-id': 'ea30c926-0629-4400-bb6e-f8a8da6a4e56',
+ 'x-image-meta-owner': '8f421f9470e645b1b10f5d2db7804924',
+ 'x-image-meta-status': 'queued',
+ 'x-image-meta-name': 'New Http Image'
+ }
+ respbody = rest_client.ResponseBody(resp)
+ observed = image.get_image_meta_from_headers(respbody)
+
+ expected = {
+ 'properties': {},
+ 'id': 'ea30c926-0629-4400-bb6e-f8a8da6a4e56',
+ 'owner': '8f421f9470e645b1b10f5d2db7804924',
+ 'status': 'queued',
+ 'name': 'New Http Image'
+ }
+ self.assertEqual(expected, observed)
+
+ def test_image_meta_to_headers(self):
+ observed = image.image_meta_to_headers(
+ name='test',
+ container_format='wrong',
+ disk_format='vhd',
+ copy_from='http://localhost/images/10',
+ properties={'foo': 'bar'},
+ api={'abc': 'def'},
+ purge_props=True)
+
+ expected = {
+ 'x-image-meta-name': 'test',
+ 'x-image-meta-container_format': 'wrong',
+ 'x-image-meta-disk_format': 'vhd',
+ 'x-glance-api-copy-from': 'http://localhost/images/10',
+ 'x-image-meta-property-foo': 'bar',
+ 'x-glance-api-property-abc': 'def',
+ 'x-glance-registry-purge-props': True
+ }
+ self.assertEqual(expected, observed)
diff --git a/tempest/tests/common/test_preprov_creds.py b/tempest/tests/common/test_preprov_creds.py
deleted file mode 100644
index fd7df16..0000000
--- a/tempest/tests/common/test_preprov_creds.py
+++ /dev/null
@@ -1,334 +0,0 @@
-# Copyright 2014 Hewlett-Packard Development Company, L.P.
-#
-# 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 hashlib
-import os
-
-import mock
-from oslo_concurrency.fixture import lockutils as lockutils_fixtures
-from oslo_config import cfg
-from oslotest import mockpatch
-import shutil
-import six
-from tempest_lib import auth
-from tempest_lib import exceptions as lib_exc
-from tempest_lib.services.identity.v2 import token_client
-
-from tempest.common import cred_provider
-from tempest.common import preprov_creds
-from tempest import config
-from tempest.tests import base
-from tempest.tests import fake_config
-from tempest.tests import fake_http
-from tempest.tests import fake_identity
-
-
-class TestPreProvisionedCredentials(base.TestCase):
-
- fixed_params = {'name': 'test class',
- 'identity_version': 'v2',
- 'test_accounts_file': 'fake_accounts_file',
- 'accounts_lock_dir': 'fake_locks_dir',
- 'admin_role': 'admin',
- 'object_storage_operator_role': 'operator',
- 'object_storage_reseller_admin_role': 'reseller'}
-
- def setUp(self):
- super(TestPreProvisionedCredentials, self).setUp()
- self.useFixture(fake_config.ConfigFixture())
- self.stubs.Set(config, 'TempestConfigPrivate', fake_config.FakePrivate)
- self.fake_http = fake_http.fake_httplib2(return_type=200)
- self.stubs.Set(token_client.TokenClient, 'raw_request',
- fake_identity._fake_v2_response)
- self.useFixture(lockutils_fixtures.ExternalLockFixture())
- self.test_accounts = [
- {'username': 'test_user1', 'tenant_name': 'test_tenant1',
- 'password': 'p'},
- {'username': 'test_user2', 'tenant_name': 'test_tenant2',
- 'password': 'p'},
- {'username': 'test_user3', 'tenant_name': 'test_tenant3',
- 'password': 'p'},
- {'username': 'test_user4', 'tenant_name': 'test_tenant4',
- 'password': 'p'},
- {'username': 'test_user5', 'tenant_name': 'test_tenant5',
- 'password': 'p'},
- {'username': 'test_user6', 'tenant_name': 'test_tenant6',
- 'password': 'p', 'roles': ['role1', 'role2']},
- {'username': 'test_user7', 'tenant_name': 'test_tenant7',
- 'password': 'p', 'roles': ['role2', 'role3']},
- {'username': 'test_user8', 'tenant_name': 'test_tenant8',
- 'password': 'p', 'roles': ['role4', 'role1']},
- {'username': 'test_user9', 'tenant_name': 'test_tenant9',
- 'password': 'p', 'roles': ['role1', 'role2', 'role3', 'role4']},
- {'username': 'test_user10', 'tenant_name': 'test_tenant10',
- 'password': 'p', 'roles': ['role1', 'role2', 'role3', 'role4']},
- {'username': 'test_user11', 'tenant_name': 'test_tenant11',
- 'password': 'p', 'roles': [cfg.CONF.identity.admin_role]},
- {'username': 'test_user12', 'tenant_name': 'test_tenant12',
- 'password': 'p', 'roles': [cfg.CONF.identity.admin_role]},
- ]
- self.accounts_mock = self.useFixture(mockpatch.Patch(
- 'tempest.common.preprov_creds.read_accounts_yaml',
- return_value=self.test_accounts))
- self.useFixture(mockpatch.Patch('os.path.isfile', return_value=True))
-
- def tearDown(self):
- super(TestPreProvisionedCredentials, self).tearDown()
- shutil.rmtree(self.fixed_params['accounts_lock_dir'],
- ignore_errors=True)
-
- def _get_hash_list(self, accounts_list):
- hash_list = []
- for account in accounts_list:
- hash = hashlib.md5()
- hash.update(six.text_type(account).encode('utf-8'))
- temp_hash = hash.hexdigest()
- hash_list.append(temp_hash)
- return hash_list
-
- def test_get_hash(self):
- self.stubs.Set(token_client.TokenClient, 'raw_request',
- fake_identity._fake_v2_response)
- test_account_class = preprov_creds.PreProvisionedCredentialProvider(
- **self.fixed_params)
- hash_list = self._get_hash_list(self.test_accounts)
- test_cred_dict = self.test_accounts[3]
- test_creds = auth.get_credentials(fake_identity.FAKE_AUTH_URL,
- **test_cred_dict)
- results = test_account_class.get_hash(test_creds)
- self.assertEqual(hash_list[3], results)
-
- def test_get_hash_dict(self):
- test_account_class = preprov_creds.PreProvisionedCredentialProvider(
- **self.fixed_params)
- hash_dict = test_account_class.get_hash_dict(
- self.test_accounts, self.fixed_params['admin_role'])
- hash_list = self._get_hash_list(self.test_accounts)
- for hash in hash_list:
- self.assertIn(hash, hash_dict['creds'].keys())
- self.assertIn(hash_dict['creds'][hash], self.test_accounts)
-
- def test_create_hash_file_previous_file(self):
- # Emulate the lock existing on the filesystem
- self.useFixture(mockpatch.Patch('os.path.isfile', return_value=True))
- with mock.patch('six.moves.builtins.open', mock.mock_open(),
- create=True):
- test_account_class = (
- preprov_creds.PreProvisionedCredentialProvider(
- **self.fixed_params))
- res = test_account_class._create_hash_file('12345')
- self.assertFalse(res, "_create_hash_file should return False if the "
- "pseudo-lock file already exists")
-
- def test_create_hash_file_no_previous_file(self):
- # Emulate the lock not existing on the filesystem
- self.useFixture(mockpatch.Patch('os.path.isfile', return_value=False))
- with mock.patch('six.moves.builtins.open', mock.mock_open(),
- create=True):
- test_account_class = (
- preprov_creds.PreProvisionedCredentialProvider(
- **self.fixed_params))
- res = test_account_class._create_hash_file('12345')
- self.assertTrue(res, "_create_hash_file should return True if the "
- "pseudo-lock doesn't already exist")
-
- @mock.patch('oslo_concurrency.lockutils.lock')
- def test_get_free_hash_no_previous_accounts(self, lock_mock):
- # Emulate no pre-existing lock
- self.useFixture(mockpatch.Patch('os.path.isdir', return_value=False))
- hash_list = self._get_hash_list(self.test_accounts)
- mkdir_mock = self.useFixture(mockpatch.Patch('os.mkdir'))
- self.useFixture(mockpatch.Patch('os.path.isfile', return_value=False))
- test_account_class = preprov_creds.PreProvisionedCredentialProvider(
- **self.fixed_params)
- with mock.patch('six.moves.builtins.open', mock.mock_open(),
- create=True) as open_mock:
- test_account_class._get_free_hash(hash_list)
- lock_path = os.path.join(self.fixed_params['accounts_lock_dir'],
- hash_list[0])
- open_mock.assert_called_once_with(lock_path, 'w')
- mkdir_path = os.path.join(self.fixed_params['accounts_lock_dir'])
- mkdir_mock.mock.assert_called_once_with(mkdir_path)
-
- @mock.patch('oslo_concurrency.lockutils.lock')
- def test_get_free_hash_no_free_accounts(self, lock_mock):
- hash_list = self._get_hash_list(self.test_accounts)
- # Emulate pre-existing lock dir
- self.useFixture(mockpatch.Patch('os.path.isdir', return_value=True))
- # Emulate all lcoks in list are in use
- self.useFixture(mockpatch.Patch('os.path.isfile', return_value=True))
- test_account_class = preprov_creds.PreProvisionedCredentialProvider(
- **self.fixed_params)
- with mock.patch('six.moves.builtins.open', mock.mock_open(),
- create=True):
- self.assertRaises(lib_exc.InvalidCredentials,
- test_account_class._get_free_hash, hash_list)
-
- @mock.patch('oslo_concurrency.lockutils.lock')
- def test_get_free_hash_some_in_use_accounts(self, lock_mock):
- # Emulate no pre-existing lock
- self.useFixture(mockpatch.Patch('os.path.isdir', return_value=True))
- hash_list = self._get_hash_list(self.test_accounts)
- test_account_class = preprov_creds.PreProvisionedCredentialProvider(
- **self.fixed_params)
-
- def _fake_is_file(path):
- # Fake isfile() to return that the path exists unless a specific
- # hash is in the path
- if hash_list[3] in path:
- return False
- return True
-
- self.stubs.Set(os.path, 'isfile', _fake_is_file)
- with mock.patch('six.moves.builtins.open', mock.mock_open(),
- create=True) as open_mock:
- test_account_class._get_free_hash(hash_list)
- lock_path = os.path.join(self.fixed_params['accounts_lock_dir'],
- hash_list[3])
- open_mock.assert_has_calls([mock.call(lock_path, 'w')])
-
- @mock.patch('oslo_concurrency.lockutils.lock')
- def test_remove_hash_last_account(self, lock_mock):
- hash_list = self._get_hash_list(self.test_accounts)
- # Pretend the pseudo-lock is there
- self.useFixture(mockpatch.Patch('os.path.isfile', return_value=True))
- # Pretend the lock dir is empty
- self.useFixture(mockpatch.Patch('os.listdir', return_value=[]))
- test_account_class = preprov_creds.PreProvisionedCredentialProvider(
- **self.fixed_params)
- remove_mock = self.useFixture(mockpatch.Patch('os.remove'))
- rmdir_mock = self.useFixture(mockpatch.Patch('os.rmdir'))
- test_account_class.remove_hash(hash_list[2])
- hash_path = os.path.join(self.fixed_params['accounts_lock_dir'],
- hash_list[2])
- lock_path = self.fixed_params['accounts_lock_dir']
- remove_mock.mock.assert_called_once_with(hash_path)
- rmdir_mock.mock.assert_called_once_with(lock_path)
-
- @mock.patch('oslo_concurrency.lockutils.lock')
- def test_remove_hash_not_last_account(self, lock_mock):
- hash_list = self._get_hash_list(self.test_accounts)
- # Pretend the pseudo-lock is there
- self.useFixture(mockpatch.Patch('os.path.isfile', return_value=True))
- # Pretend the lock dir is empty
- self.useFixture(mockpatch.Patch('os.listdir', return_value=[
- hash_list[1], hash_list[4]]))
- test_account_class = preprov_creds.PreProvisionedCredentialProvider(
- **self.fixed_params)
- remove_mock = self.useFixture(mockpatch.Patch('os.remove'))
- rmdir_mock = self.useFixture(mockpatch.Patch('os.rmdir'))
- test_account_class.remove_hash(hash_list[2])
- hash_path = os.path.join(self.fixed_params['accounts_lock_dir'],
- hash_list[2])
- remove_mock.mock.assert_called_once_with(hash_path)
- rmdir_mock.mock.assert_not_called()
-
- def test_is_multi_user(self):
- test_accounts_class = preprov_creds.PreProvisionedCredentialProvider(
- **self.fixed_params)
- self.assertTrue(test_accounts_class.is_multi_user())
-
- def test_is_not_multi_user(self):
- self.test_accounts = [self.test_accounts[0]]
- self.useFixture(mockpatch.Patch(
- 'tempest.common.preprov_creds.read_accounts_yaml',
- return_value=self.test_accounts))
- test_accounts_class = preprov_creds.PreProvisionedCredentialProvider(
- **self.fixed_params)
- self.assertFalse(test_accounts_class.is_multi_user())
-
- def test__get_creds_by_roles_one_role(self):
- self.useFixture(mockpatch.Patch(
- 'tempest.common.preprov_creds.read_accounts_yaml',
- return_value=self.test_accounts))
- test_accounts_class = preprov_creds.PreProvisionedCredentialProvider(
- **self.fixed_params)
- hashes = test_accounts_class.hash_dict['roles']['role4']
- temp_hash = hashes[0]
- get_free_hash_mock = self.useFixture(mockpatch.PatchObject(
- test_accounts_class, '_get_free_hash', return_value=temp_hash))
- # Test a single role returns all matching roles
- test_accounts_class._get_creds(roles=['role4'])
- calls = get_free_hash_mock.mock.mock_calls
- self.assertEqual(len(calls), 1)
- args = calls[0][1][0]
- for i in hashes:
- self.assertIn(i, args)
-
- def test__get_creds_by_roles_list_role(self):
- self.useFixture(mockpatch.Patch(
- 'tempest.common.preprov_creds.read_accounts_yaml',
- return_value=self.test_accounts))
- test_accounts_class = preprov_creds.PreProvisionedCredentialProvider(
- **self.fixed_params)
- hashes = test_accounts_class.hash_dict['roles']['role4']
- hashes2 = test_accounts_class.hash_dict['roles']['role2']
- hashes = list(set(hashes) & set(hashes2))
- temp_hash = hashes[0]
- get_free_hash_mock = self.useFixture(mockpatch.PatchObject(
- test_accounts_class, '_get_free_hash', return_value=temp_hash))
- # Test an intersection of multiple roles
- test_accounts_class._get_creds(roles=['role2', 'role4'])
- calls = get_free_hash_mock.mock.mock_calls
- self.assertEqual(len(calls), 1)
- args = calls[0][1][0]
- for i in hashes:
- self.assertIn(i, args)
-
- def test__get_creds_by_roles_no_admin(self):
- self.useFixture(mockpatch.Patch(
- 'tempest.common.preprov_creds.read_accounts_yaml',
- return_value=self.test_accounts))
- test_accounts_class = preprov_creds.PreProvisionedCredentialProvider(
- **self.fixed_params)
- hashes = list(test_accounts_class.hash_dict['creds'].keys())
- admin_hashes = test_accounts_class.hash_dict['roles'][
- cfg.CONF.identity.admin_role]
- temp_hash = hashes[0]
- get_free_hash_mock = self.useFixture(mockpatch.PatchObject(
- test_accounts_class, '_get_free_hash', return_value=temp_hash))
- # Test an intersection of multiple roles
- test_accounts_class._get_creds()
- calls = get_free_hash_mock.mock.mock_calls
- self.assertEqual(len(calls), 1)
- args = calls[0][1][0]
- self.assertEqual(len(args), 10)
- for i in admin_hashes:
- self.assertNotIn(i, args)
-
- def test_networks_returned_with_creds(self):
- test_accounts = [
- {'username': 'test_user13', 'tenant_name': 'test_tenant13',
- 'password': 'p', 'resources': {'network': 'network-1'}},
- {'username': 'test_user14', 'tenant_name': 'test_tenant14',
- 'password': 'p', 'roles': ['role-7', 'role-11'],
- 'resources': {'network': 'network-2'}}]
- self.useFixture(mockpatch.Patch(
- 'tempest.common.preprov_creds.read_accounts_yaml',
- return_value=test_accounts))
- test_accounts_class = preprov_creds.PreProvisionedCredentialProvider(
- **self.fixed_params)
- with mock.patch('tempest_lib.services.compute.networks_client.'
- 'NetworksClient.list_networks',
- return_value={'networks': [{'name': 'network-2',
- 'id': 'fake-id',
- 'label': 'network-2'}]}):
- creds = test_accounts_class.get_creds_by_roles(['role-7'])
- self.assertTrue(isinstance(creds, cred_provider.TestResources))
- network = creds.network
- self.assertIsNotNone(network)
- self.assertIn('name', network)
- self.assertIn('id', network)
- self.assertEqual('fake-id', network['id'])
- self.assertEqual('network-2', network['name'])
diff --git a/tempest/tests/common/test_service_clients.py b/tempest/tests/common/test_service_clients.py
deleted file mode 100644
index f248957..0000000
--- a/tempest/tests/common/test_service_clients.py
+++ /dev/null
@@ -1,144 +0,0 @@
-# Copyright 2015 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.
-
-import mock
-import random
-import six
-
-from tempest.services.baremetal.v1.json import baremetal_client
-from tempest.services.data_processing.v1_1 import data_processing_client
-from tempest.services.database.json import flavors_client as db_flavor_client
-from tempest.services.database.json import versions_client as db_version_client
-from tempest.services.identity.v2.json import identity_client as \
- identity_v2_identity_client
-from tempest.services.identity.v3.json import credentials_client
-from tempest.services.identity.v3.json import endpoints_client
-from tempest.services.identity.v3.json import identity_client as \
- identity_v3_identity_client
-from tempest.services.identity.v3.json import policies_client
-from tempest.services.identity.v3.json import regions_client
-from tempest.services.identity.v3.json import services_client
-from tempest.services.image.v1.json import images_client
-from tempest.services.image.v2.json import images_client as images_v2_client
-from tempest.services.messaging.json import messaging_client
-from tempest.services.network.json import network_client
-from tempest.services.object_storage import account_client
-from tempest.services.object_storage import container_client
-from tempest.services.object_storage import object_client
-from tempest.services.orchestration.json import orchestration_client
-from tempest.services.telemetry.json import alarming_client
-from tempest.services.telemetry.json import telemetry_client
-from tempest.services.volume.v1.json.admin import hosts_client \
- as volume_hosts_client
-from tempest.services.volume.v1.json.admin import quotas_client \
- as volume_quotas_client
-from tempest.services.volume.v1.json.admin import services_client \
- as volume_services_client
-from tempest.services.volume.v1.json.admin import types_client \
- as volume_types_client
-from tempest.services.volume.v1.json import availability_zone_client \
- as volume_az_client
-from tempest.services.volume.v1.json import backups_client
-from tempest.services.volume.v1.json import extensions_client \
- as volume_extensions_client
-from tempest.services.volume.v1.json import qos_client
-from tempest.services.volume.v1.json import snapshots_client
-from tempest.services.volume.v1.json import volumes_client
-from tempest.services.volume.v2.json.admin import hosts_client \
- as volume_v2_hosts_client
-from tempest.services.volume.v2.json.admin import quotas_client \
- as volume_v2_quotas_client
-from tempest.services.volume.v2.json.admin import services_client \
- as volume_v2_services_client
-from tempest.services.volume.v2.json.admin import types_client \
- as volume_v2_types_client
-from tempest.services.volume.v2.json import availability_zone_client \
- as volume_v2_az_client
-from tempest.services.volume.v2.json import backups_client \
- as volume_v2_backups_client
-from tempest.services.volume.v2.json import extensions_client \
- as volume_v2_extensions_client
-from tempest.services.volume.v2.json import qos_client as volume_v2_qos_client
-from tempest.services.volume.v2.json import snapshots_client \
- as volume_v2_snapshots_client
-from tempest.services.volume.v2.json import volumes_client as \
- volume_v2_volumes_client
-from tempest.tests import base
-
-
-class TestServiceClient(base.TestCase):
-
- @mock.patch('tempest_lib.common.rest_client.RestClient.__init__')
- def test_service_client_creations_with_specified_args(self, mock_init):
- test_clients = [
- baremetal_client.BaremetalClient,
- data_processing_client.DataProcessingClient,
- db_flavor_client.DatabaseFlavorsClient,
- db_version_client.DatabaseVersionsClient,
- messaging_client.MessagingClient,
- network_client.NetworkClient,
- account_client.AccountClient,
- container_client.ContainerClient,
- object_client.ObjectClient,
- orchestration_client.OrchestrationClient,
- telemetry_client.TelemetryClient,
- alarming_client.AlarmingClient,
- qos_client.QosSpecsClient,
- volume_hosts_client.HostsClient,
- volume_quotas_client.QuotasClient,
- volume_services_client.ServicesClient,
- volume_types_client.TypesClient,
- volume_az_client.AvailabilityZoneClient,
- backups_client.BackupsClient,
- volume_extensions_client.ExtensionsClient,
- snapshots_client.SnapshotsClient,
- volumes_client.VolumesClient,
- volume_v2_hosts_client.HostsClient,
- volume_v2_quotas_client.QuotasClient,
- volume_v2_services_client.ServicesClient,
- volume_v2_types_client.TypesClient,
- volume_v2_az_client.AvailabilityZoneClient,
- volume_v2_backups_client.BackupsClient,
- volume_v2_extensions_client.ExtensionsClient,
- volume_v2_qos_client.QosSpecsClient,
- volume_v2_snapshots_client.SnapshotsClient,
- volume_v2_volumes_client.VolumesClient,
- identity_v2_identity_client.IdentityClient,
- credentials_client.CredentialsClient,
- endpoints_client.EndPointClient,
- identity_v3_identity_client.IdentityV3Client,
- policies_client.PoliciesClient,
- regions_client.RegionsClient,
- services_client.ServicesClient,
- images_client.ImagesClient,
- images_v2_client.ImagesClientV2
- ]
-
- for client in test_clients:
- fake_string = six.text_type(random.randint(1, 0x7fffffff))
- auth = 'auth' + fake_string
- service = 'service' + fake_string
- region = 'region' + fake_string
- params = {
- 'endpoint_type': 'URL' + fake_string,
- 'build_interval': random.randint(1, 100),
- 'build_timeout': random.randint(1, 100),
- 'disable_ssl_certificate_validation':
- True if random.randint(0, 1) else False,
- 'ca_certs': None,
- 'trace_requests': 'foo' + fake_string
- }
- client(auth, service, region, **params)
- mock_init.assert_called_once_with(auth, service, region, **params)
- mock_init.reset_mock()
diff --git a/tempest/tests/common/test_waiters.py b/tempest/tests/common/test_waiters.py
index c7cc638..938d226 100644
--- a/tempest/tests/common/test_waiters.py
+++ b/tempest/tests/common/test_waiters.py
@@ -18,8 +18,10 @@
from tempest.common import waiters
from tempest import exceptions
-from tempest.services.volume.base import base_volumes_client
+from tempest.lib import exceptions as lib_exc
+from tempest.lib.services.volume.v2 import volumes_client
from tempest.tests import base
+import tempest.tests.utils as utils
class TestImageWaiters(base.TestCase):
@@ -35,11 +37,14 @@
waiters.wait_for_image_status(self.client, 'fake_image_id', 'active')
end_time = int(time.time())
# Ensure waiter returns before build_timeout
- self.assertTrue((end_time - start_time) < 10)
+ self.assertLess((end_time - start_time), 10)
def test_wait_for_image_status_timeout(self):
+ time_mock = self.patch('time.time')
+ time_mock.side_effect = utils.generate_timeout_series(1)
+
self.client.show_image.return_value = ({'status': 'saving'})
- self.assertRaises(exceptions.TimeoutException,
+ self.assertRaises(lib_exc.TimeoutException,
waiters.wait_for_image_status,
self.client, 'fake_image_id', 'active')
@@ -53,7 +58,8 @@
def test_wait_for_volume_status_error_restoring(self, mock_sleep):
# Tests that the wait method raises VolumeRestoreErrorException if
# the volume status is 'error_restoring'.
- client = mock.Mock(spec=base_volumes_client.BaseVolumesClient,
+ client = mock.Mock(spec=volumes_client.VolumesClient,
+ resource_type="volume",
build_interval=1)
volume1 = {'volume': {'status': 'restoring-backup'}}
volume2 = {'volume': {'status': 'error_restoring'}}
@@ -61,8 +67,84 @@
client.show_volume = mock_show
volume_id = '7532b91e-aa0a-4e06-b3e5-20c0c5ee1caa'
self.assertRaises(exceptions.VolumeRestoreErrorException,
- waiters.wait_for_volume_status,
+ waiters.wait_for_volume_resource_status,
client, volume_id, 'available')
mock_show.assert_has_calls([mock.call(volume_id),
mock.call(volume_id)])
mock_sleep.assert_called_once_with(1)
+
+
+class TestInterfaceWaiters(base.TestCase):
+
+ build_timeout = 1.
+ build_interval = 1
+ port_down = {'interfaceAttachment': {'port_state': 'DOWN'}}
+ port_active = {'interfaceAttachment': {'port_state': 'ACTIVE'}}
+
+ def mock_client(self, **kwargs):
+ return mock.MagicMock(
+ build_timeout=self.build_timeout,
+ build_interval=self.build_interval,
+ **kwargs)
+
+ def test_wait_for_interface_status(self):
+ show_interface = mock.Mock(
+ side_effect=[self.port_down, self.port_active])
+ client = self.mock_client(show_interface=show_interface)
+ self.patch('time.time', return_value=0.)
+ sleep = self.patch('time.sleep')
+
+ result = waiters.wait_for_interface_status(
+ client, 'server_id', 'port_id', 'ACTIVE')
+
+ self.assertIs(self.port_active['interfaceAttachment'], result)
+ show_interface.assert_has_calls([mock.call('server_id', 'port_id'),
+ mock.call('server_id', 'port_id')])
+ sleep.assert_called_once_with(client.build_interval)
+
+ def test_wait_for_interface_status_timeout(self):
+ show_interface = mock.MagicMock(return_value=self.port_down)
+ client = self.mock_client(show_interface=show_interface)
+ self.patch('time.time', side_effect=[0., client.build_timeout + 1.])
+ sleep = self.patch('time.sleep')
+
+ self.assertRaises(lib_exc.TimeoutException,
+ waiters.wait_for_interface_status,
+ client, 'server_id', 'port_id', 'ACTIVE')
+
+ show_interface.assert_has_calls([mock.call('server_id', 'port_id'),
+ mock.call('server_id', 'port_id')])
+ sleep.assert_called_once_with(client.build_interval)
+
+ one_interface = {'interfaceAttachments': [{'port_id': 'port_one'}]}
+ two_interfaces = {'interfaceAttachments': [{'port_id': 'port_one'},
+ {'port_id': 'port_two'}]}
+
+ def test_wait_for_interface_detach(self):
+ list_interfaces = mock.MagicMock(
+ side_effect=[self.two_interfaces, self.one_interface])
+ client = self.mock_client(list_interfaces=list_interfaces)
+ self.patch('time.time', return_value=0.)
+ sleep = self.patch('time.sleep')
+
+ result = waiters.wait_for_interface_detach(
+ client, 'server_id', 'port_two')
+
+ self.assertIs(self.one_interface['interfaceAttachments'], result)
+ list_interfaces.assert_has_calls([mock.call('server_id'),
+ mock.call('server_id')])
+ sleep.assert_called_once_with(client.build_interval)
+
+ def test_wait_for_interface_detach_timeout(self):
+ list_interfaces = mock.MagicMock(return_value=self.one_interface)
+ client = self.mock_client(list_interfaces=list_interfaces)
+ self.patch('time.time', side_effect=[0., client.build_timeout + 1.])
+ sleep = self.patch('time.sleep')
+
+ self.assertRaises(lib_exc.TimeoutException,
+ waiters.wait_for_interface_detach,
+ client, 'server_id', 'port_one')
+
+ list_interfaces.assert_has_calls([mock.call('server_id'),
+ mock.call('server_id')])
+ sleep.assert_called_once_with(client.build_interval)
diff --git a/tempest/tests/common/utils/linux/test_remote_client.py b/tempest/tests/common/utils/linux/test_remote_client.py
index 9c2b99e..739357b 100644
--- a/tempest/tests/common/utils/linux/test_remote_client.py
+++ b/tempest/tests/common/utils/linux/test_remote_client.py
@@ -14,42 +14,57 @@
import time
+import fixtures
from oslo_config import cfg
-from oslotest import mockpatch
from tempest.common.utils.linux import remote_client
from tempest import config
+from tempest.lib import exceptions as lib_exc
from tempest.tests import base
from tempest.tests import fake_config
+SERVER = {
+ 'id': 'server_uuid',
+ 'name': 'fake_server',
+ 'status': 'ACTIVE'
+}
+
+BROKEN_SERVER = {
+ 'id': 'broken_server_uuid',
+ 'name': 'broken_server',
+ 'status': 'ERROR'
+}
+
+
+class FakeServersClient(object):
+
+ CONSOLE_OUTPUT = "Console output for %s"
+
+ def get_console_output(self, server_id):
+ status = 'ERROR'
+ for s in SERVER, BROKEN_SERVER:
+ if s['id'] == server_id:
+ status = s['status']
+ if status == 'ERROR':
+ raise lib_exc.BadRequest('Server in ERROR state')
+ else:
+ return dict(output=self.CONSOLE_OUTPUT % server_id)
+
+
class TestRemoteClient(base.TestCase):
def setUp(self):
super(TestRemoteClient, self).setUp()
self.useFixture(fake_config.ConfigFixture())
- self.stubs.Set(config, 'TempestConfigPrivate', fake_config.FakePrivate)
+ self.patchobject(config, 'TempestConfigPrivate',
+ fake_config.FakePrivate)
cfg.CONF.set_default('ip_version_for_ssh', 4, group='validation')
cfg.CONF.set_default('network_for_ssh', 'public', group='validation')
cfg.CONF.set_default('connect_timeout', 1, group='validation')
self.conn = remote_client.RemoteClient('127.0.0.1', 'user', 'pass')
- self.ssh_mock = self.useFixture(mockpatch.PatchObject(self.conn,
- 'ssh_client'))
-
- def test_hostname_equals_servername_for_expected_names(self):
- self.ssh_mock.mock.exec_command.return_value = 'fake_hostname'
- self.assertTrue(self.conn.hostname_equals_servername('fake_hostname'))
-
- def test_hostname_equals_servername_for_unexpected_names(self):
- self.ssh_mock.mock.exec_command.return_value = 'fake_hostname'
- self.assertFalse(
- self.conn.hostname_equals_servername('unexpected_hostname'))
-
- def test_get_ram_size(self):
- free_output = "Mem: 48294 45738 2555 0" \
- "402 40346"
- self.ssh_mock.mock.exec_command.return_value = free_output
- self.assertEqual(self.conn.get_ram_size_in_mb(), '48294')
+ self.ssh_mock = self.useFixture(fixtures.MockPatchObject(self.conn,
+ 'ssh_client'))
def test_write_to_console_regular_str(self):
self.conn.write_to_console('test')
@@ -76,24 +91,26 @@
cmd = "set -eu -o pipefail; PATH=$PATH:/sbin; " + cmd
self.ssh_mock.mock.exec_command.assert_called_with(cmd)
- def test_get_number_of_vcpus(self):
- self.ssh_mock.mock.exec_command.return_value = '16'
- self.assertEqual(self.conn.get_number_of_vcpus(), 16)
- self._assert_exec_called_with('grep -c ^processor /proc/cpuinfo')
+ def test_get_disks(self):
+ output_lsblk = """\
+NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
+sda 8:0 0 128035676160 0 disk
+sdb 8:16 0 1000204886016 0 disk
+sr0 11:0 1 1073741312 0 rom"""
+ result = """\
+NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
+sda 8:0 0 128035676160 0 disk
+sdb 8:16 0 1000204886016 0 disk"""
- def test_get_partitions(self):
- proc_partitions = """major minor #blocks name
-
-8 0 1048576 vda"""
- self.ssh_mock.mock.exec_command.return_value = proc_partitions
- self.assertEqual(self.conn.get_partitions(), proc_partitions)
- self._assert_exec_called_with('cat /proc/partitions')
+ self.ssh_mock.mock.exec_command.return_value = output_lsblk
+ self.assertEqual(self.conn.get_disks(), result)
+ self._assert_exec_called_with('lsblk -lb --nodeps')
def test_get_boot_time(self):
booted_at = 10000
uptime_sec = 5000.02
self.ssh_mock.mock.exec_command.return_value = uptime_sec
- self.useFixture(mockpatch.PatchObject(
+ self.useFixture(fixtures.MockPatchObject(
time, 'time', return_value=booted_at + uptime_sec))
self.assertEqual(self.conn.get_boot_time(),
time.localtime(booted_at))
@@ -121,36 +138,77 @@
self._assert_exec_called_with(
"ip addr | awk '/ether/ {print $2}'")
- def test_get_ip_list(self):
- ips = """1: lo: <LOOPBACK,UP,LOWER_UP> mtu 16436 qdisc noqueue
- link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
- inet 127.0.0.1/8 scope host lo
- inet6 ::1/128 scope host
- valid_lft forever preferred_lft forever
-2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast qlen 1000
- link/ether fa:16:3e:6e:26:3b brd ff:ff:ff:ff:ff:ff
- inet 10.0.0.4/24 brd 10.0.0.255 scope global eth0
- inet6 fd55:faaf:e1ab:3d9:f816:3eff:fe6e:263b/64 scope global dynamic
- valid_lft 2591936sec preferred_lft 604736sec
- inet6 fe80::f816:3eff:fe6e:263b/64 scope link
- valid_lft forever preferred_lft forever"""
- self.ssh_mock.mock.exec_command.return_value = ips
- self.assertEqual(self.conn.get_ip_list(), ips)
- self._assert_exec_called_with('ip address')
- def test_assign_static_ip(self):
- self.ssh_mock.mock.exec_command.return_value = ''
- ip = '10.0.0.2'
- nic = 'eth0'
- self.assertEqual(self.conn.assign_static_ip(nic, ip), '')
- self._assert_exec_called_with(
- "sudo ip addr add %s/%s dev %s" % (ip, '28', nic))
+class TestRemoteClientWithServer(base.TestCase):
- def test_set_nic_state(self):
- nic = 'eth0'
- self.conn.set_nic_state(nic)
- self._assert_exec_called_with(
- 'sudo ip link set %s up' % nic)
- self.conn.set_nic_state(nic, "down")
- self._assert_exec_called_with(
- 'sudo ip link set %s down' % nic)
+ server = SERVER
+
+ def setUp(self):
+ super(TestRemoteClientWithServer, self).setUp()
+ self.useFixture(fake_config.ConfigFixture())
+ self.patchobject(config, 'TempestConfigPrivate',
+ fake_config.FakePrivate)
+ cfg.CONF.set_default('ip_version_for_ssh', 4, group='validation')
+ cfg.CONF.set_default('network_for_ssh', 'public',
+ group='validation')
+ cfg.CONF.set_default('connect_timeout', 1, group='validation')
+ cfg.CONF.set_default('console_output', True,
+ group='compute-feature-enabled')
+
+ self.conn = remote_client.RemoteClient(
+ '127.0.0.1', 'user', 'pass',
+ server=self.server, servers_client=FakeServersClient())
+ self.useFixture(fixtures.MockPatch(
+ 'tempest.lib.common.ssh.Client._get_ssh_connection',
+ side_effect=lib_exc.SSHTimeout(host='127.0.0.1',
+ user='user',
+ password='pass')))
+ self.log = self.useFixture(fixtures.FakeLogger(
+ name='tempest.lib.common.utils.linux.remote_client',
+ level='DEBUG'))
+
+ def test_validate_debug_ssh_console(self):
+ self.assertRaises(lib_exc.SSHTimeout,
+ self.conn.validate_authentication)
+ msg = 'Caller: %s. Timeout trying to ssh to server %s' % (
+ 'TestRemoteClientWithServer:test_validate_debug_ssh_console',
+ self.server)
+ self.assertIn(msg, self.log.output)
+ self.assertIn('Console output for', self.log.output)
+
+ def test_exec_command_debug_ssh_console(self):
+ self.assertRaises(lib_exc.SSHTimeout,
+ self.conn.exec_command, 'fake command')
+ self.assertIn('fake command', self.log.output)
+ msg = 'Caller: %s. Timeout trying to ssh to server %s' % (
+ 'TestRemoteClientWithServer:test_exec_command_debug_ssh_console',
+ self.server)
+ self.assertIn(msg, self.log.output)
+ self.assertIn('Console output for', self.log.output)
+
+
+class TestRemoteClientWithBrokenServer(TestRemoteClientWithServer):
+
+ server = BROKEN_SERVER
+
+ def test_validate_debug_ssh_console(self):
+ self.assertRaises(lib_exc.SSHTimeout,
+ self.conn.validate_authentication)
+ msg = 'Caller: %s. Timeout trying to ssh to server %s' % (
+ 'TestRemoteClientWithBrokenServer:test_validate_debug_ssh_console',
+ self.server)
+ self.assertIn(msg, self.log.output)
+ msg = 'Could not get console_log for server %s' % self.server['id']
+ self.assertIn(msg, self.log.output)
+
+ def test_exec_command_debug_ssh_console(self):
+ self.assertRaises(lib_exc.SSHTimeout,
+ self.conn.exec_command, 'fake command')
+ self.assertIn('fake command', self.log.output)
+ caller = ":".join(['TestRemoteClientWithBrokenServer',
+ 'test_exec_command_debug_ssh_console'])
+ msg = 'Caller: %s. Timeout trying to ssh to server %s' % (
+ caller, self.server)
+ self.assertIn(msg, self.log.output)
+ msg = 'Could not get console_log for server %s' % self.server['id']
+ self.assertIn(msg, self.log.output)
diff --git a/tempest/tests/common/utils/test_file_utils.py b/tempest/tests/common/utils/test_file_utils.py
deleted file mode 100644
index 937aefa..0000000
--- a/tempest/tests/common/utils/test_file_utils.py
+++ /dev/null
@@ -1,32 +0,0 @@
-# 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.
-
-import mock
-
-from tempest.common.utils import file_utils
-from tempest.tests import base
-
-
-class TestFileUtils(base.TestCase):
-
- def test_have_effective_read_path(self):
- with mock.patch('six.moves.builtins.open', mock.mock_open(),
- create=True):
- result = file_utils.have_effective_read_access('fake_path')
- self.assertTrue(result)
-
- def test_not_effective_read_path(self):
- result = file_utils.have_effective_read_access('fake_path')
- self.assertFalse(result)
diff --git a/tempest/tests/common/utils/test_net_utils.py b/tempest/tests/common/utils/test_net_utils.py
new file mode 100644
index 0000000..83c6bcc
--- /dev/null
+++ b/tempest/tests/common/utils/test_net_utils.py
@@ -0,0 +1,33 @@
+# 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 mock
+
+from tempest.common.utils import net_utils
+from tempest.lib import exceptions as lib_exc
+from tempest.tests import base
+
+
+class TestGetPingPayloadSize(base.TestCase):
+
+ def test_ipv4(self):
+ self.assertEqual(1422, net_utils.get_ping_payload_size(1450, 4))
+
+ def test_ipv6(self):
+ self.assertEqual(1406, net_utils.get_ping_payload_size(1450, 6))
+
+ def test_too_low_mtu(self):
+ self.assertRaises(
+ lib_exc.BadRequest, net_utils.get_ping_payload_size, 10, 4)
+
+ def test_None(self):
+ self.assertIsNone(net_utils.get_ping_payload_size(None, mock.Mock()))
diff --git a/tempest/tests/fake_auth_provider.py b/tempest/tests/fake_auth_provider.py
deleted file mode 100644
index bc68d26..0000000
--- a/tempest/tests/fake_auth_provider.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# Copyright 2014 Hewlett-Packard Development Company, L.P.
-# 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.
-
-
-class FakeAuthProvider(object):
-
- def auth_request(self, method, url, headers=None, body=None, filters=None):
- return url, headers, body
diff --git a/tempest/tests/fake_config.py b/tempest/tests/fake_config.py
index ca8bc3e..4a2fff4 100644
--- a/tempest/tests/fake_config.py
+++ b/tempest/tests/fake_config.py
@@ -24,6 +24,7 @@
class ConfigFixture(conf_fixture.Config):
def __init__(self):
+ cfg.CONF([], default_config_files=[])
config.register_opts()
super(ConfigFixture, self).__init__()
@@ -38,27 +39,69 @@
self.conf.set_default('uri_v3', 'http://fake_uri_v3.com/auth',
group='identity')
self.conf.set_default('neutron', True, group='service_available')
- self.conf.set_default('heat', True, group='service_available')
- if not os.path.exists(str(os.environ.get('OS_TEST_LOCK_PATH'))):
- os.mkdir(str(os.environ.get('OS_TEST_LOCK_PATH')))
+ lock_path = str(os.environ.get('OS_TEST_LOCK_PATH',
+ os.environ.get('TMPDIR', '/tmp')))
+ if not os.path.exists(lock_path):
+ os.mkdir(lock_path)
lockutils.set_defaults(
- lock_path=str(os.environ.get('OS_TEST_LOCK_PATH')),
+ lock_path=lock_path,
)
self.conf.set_default('auth_version', 'v2', group='identity')
- for config_option in ['username', 'password', 'tenant_name']:
+ for config_option in ['username', 'password', 'project_name']:
# Identity group items
- for prefix in ['', 'alt_', 'admin_']:
- if prefix == 'admin_':
- group = 'auth'
- else:
- group = 'identity'
- self.conf.set_default(prefix + config_option,
- 'fake_' + config_option,
- group=group)
+ self.conf.set_default('admin_' + config_option,
+ 'fake_' + config_option,
+ group='auth')
class FakePrivate(config.TempestConfigPrivate):
def __init__(self, parse_conf=True, config_path=None):
- cfg.CONF([], default_config_files=[])
self._set_attrs()
- self.lock_path = cfg.CONF.lock_path
+ self.lock_path = cfg.CONF.oslo_concurrency.lock_path
+
+fake_service1_group = cfg.OptGroup(name='fake-service1', title='Fake service1')
+
+FakeService1Group = [
+ cfg.StrOpt('catalog_type', default='fake-service1'),
+ cfg.StrOpt('endpoint_type', default='faketype'),
+ cfg.StrOpt('region', default='fake_region'),
+ cfg.IntOpt('build_timeout', default=99),
+ cfg.IntOpt('build_interval', default=9)]
+
+fake_service2_group = cfg.OptGroup(name='fake-service2', title='Fake service2')
+
+FakeService2Group = [
+ cfg.StrOpt('catalog_type', default='fake-service2'),
+ cfg.StrOpt('endpoint_type', default='faketype')]
+
+
+class ServiceClientsConfigFixture(conf_fixture.Config):
+
+ def __init__(self):
+ cfg.CONF([], default_config_files=[])
+ config._opts.append((fake_service1_group, FakeService1Group))
+ config._opts.append((fake_service2_group, FakeService2Group))
+ config.register_opts()
+ super(ServiceClientsConfigFixture, self).__init__()
+
+ def setUp(self):
+ super(ServiceClientsConfigFixture, self).setUp()
+ # Debug default values
+ self.conf.set_default('trace_requests', 'fake_module', 'debug')
+ # Identity default values
+ self.conf.set_default('disable_ssl_certificate_validation', True,
+ group='identity')
+ self.conf.set_default('ca_certificates_file', '/fake/certificates',
+ group='identity')
+ self.conf.set_default('region', 'fake_region', 'identity')
+ # Compute default values
+ self.conf.set_default('build_interval', 88, group='compute')
+ self.conf.set_default('build_timeout', 8, group='compute')
+
+
+class ServiceClientsFakePrivate(config.TempestConfigPrivate):
+ def __init__(self, parse_conf=True, config_path=None):
+ self._set_attrs()
+ self.fake_service1 = cfg.CONF['fake-service1']
+ self.fake_service2 = cfg.CONF['fake-service2']
+ self.lock_path = cfg.CONF.oslo_concurrency.lock_path
diff --git a/tempest/tests/fake_http.py b/tempest/tests/fake_http.py
deleted file mode 100644
index d714055..0000000
--- a/tempest/tests/fake_http.py
+++ /dev/null
@@ -1,74 +0,0 @@
-# Copyright 2013 IBM Corp.
-#
-# 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
-
-import httplib2
-
-
-class fake_httplib2(object):
-
- def __init__(self, return_type=None, *args, **kwargs):
- self.return_type = return_type
-
- def request(self, uri, method="GET", body=None, headers=None,
- redirections=5, connection_type=None):
- if not self.return_type:
- fake_headers = httplib2.Response(headers)
- return_obj = {
- 'uri': uri,
- 'method': method,
- 'body': body,
- 'headers': headers
- }
- return (fake_headers, return_obj)
- elif isinstance(self.return_type, int):
- body = "fake_body"
- header_info = {
- 'content-type': 'text/plain',
- 'status': str(self.return_type),
- 'content-length': len(body)
- }
- resp_header = httplib2.Response(header_info)
- return (resp_header, body)
- else:
- msg = "unsupported return type %s" % self.return_type
- raise TypeError(msg)
-
-
-class fake_httplib(object):
- def __init__(self, headers, body=None,
- version=1.0, status=200, reason="Ok"):
- """Initialization of fake httplib
-
- :param headers: dict representing HTTP response headers
- :param body: file-like object
- :param version: HTTP Version
- :param status: Response status code
- :param reason: Status code related message.
- """
- self.body = body
- self.status = status
- self.reason = reason
- self.version = version
- self.headers = headers
-
- def getheaders(self):
- return copy.deepcopy(self.headers).items()
-
- def getheader(self, key, default):
- return self.headers.get(key, default)
-
- def read(self, amt):
- return self.body.read(amt)
diff --git a/tempest/tests/fake_identity.py b/tempest/tests/fake_identity.py
deleted file mode 100644
index d0de927..0000000
--- a/tempest/tests/fake_identity.py
+++ /dev/null
@@ -1,163 +0,0 @@
-# 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.
-
-import httplib2
-from oslo_serialization import jsonutils as json
-
-FAKE_AUTH_URL = 'http://fake_uri.com/auth'
-
-TOKEN = "fake_token"
-ALT_TOKEN = "alt_fake_token"
-
-# Fake Identity v2 constants
-COMPUTE_ENDPOINTS_V2 = {
- "endpoints": [
- {
- "adminURL": "http://fake_url/v2/first_endpoint/admin",
- "region": "NoMatchRegion",
- "internalURL": "http://fake_url/v2/first_endpoint/internal",
- "publicURL": "http://fake_url/v2/first_endpoint/public"
- },
- {
- "adminURL": "http://fake_url/v2/second_endpoint/admin",
- "region": "FakeRegion",
- "internalURL": "http://fake_url/v2/second_endpoint/internal",
- "publicURL": "http://fake_url/v2/second_endpoint/public"
- },
- ],
- "type": "compute",
- "name": "nova"
-}
-
-CATALOG_V2 = [COMPUTE_ENDPOINTS_V2, ]
-
-ALT_IDENTITY_V2_RESPONSE = {
- "access": {
- "token": {
- "expires": "2020-01-01T00:00:10Z",
- "id": ALT_TOKEN,
- "tenant": {
- "id": "fake_tenant_id"
- },
- },
- "user": {
- "id": "fake_user_id",
- },
- "serviceCatalog": CATALOG_V2,
- },
-}
-
-IDENTITY_V2_RESPONSE = {
- "access": {
- "token": {
- "expires": "2020-01-01T00:00:10Z",
- "id": TOKEN,
- "tenant": {
- "id": "fake_tenant_id"
- },
- },
- "user": {
- "id": "fake_user_id",
- },
- "serviceCatalog": CATALOG_V2,
- },
-}
-
-# Fake Identity V3 constants
-COMPUTE_ENDPOINTS_V3 = {
- "endpoints": [
- {
- "id": "first_compute_fake_service",
- "interface": "public",
- "region": "NoMatchRegion",
- "url": "http://fake_url/v3/first_endpoint/api"
- },
- {
- "id": "second_fake_service",
- "interface": "public",
- "region": "FakeRegion",
- "url": "http://fake_url/v3/second_endpoint/api"
- },
- {
- "id": "third_fake_service",
- "interface": "admin",
- "region": "MiddleEarthRegion",
- "url": "http://fake_url/v3/third_endpoint/api"
- }
-
- ],
- "type": "compute",
- "id": "fake_compute_endpoint"
-}
-
-CATALOG_V3 = [COMPUTE_ENDPOINTS_V3, ]
-
-IDENTITY_V3_RESPONSE = {
- "token": {
- "methods": [
- "token",
- "password"
- ],
- "expires_at": "2020-01-01T00:00:10.000123Z",
- "project": {
- "domain": {
- "id": "fake_domain_id",
- "name": "fake"
- },
- "id": "project_id",
- "name": "project_name"
- },
- "user": {
- "domain": {
- "id": "fake_domain_id",
- "name": "domain_name"
- },
- "id": "fake_user_id",
- "name": "username"
- },
- "issued_at": "2013-05-29T16:55:21.468960Z",
- "catalog": CATALOG_V3
- }
-}
-
-ALT_IDENTITY_V3 = IDENTITY_V3_RESPONSE
-
-
-def _fake_v3_response(self, uri, method="GET", body=None, headers=None,
- redirections=5, connection_type=None):
- fake_headers = {
- "status": "201",
- "x-subject-token": TOKEN
- }
- return (httplib2.Response(fake_headers),
- json.dumps(IDENTITY_V3_RESPONSE))
-
-
-def _fake_v2_response(self, uri, method="GET", body=None, headers=None,
- redirections=5, connection_type=None):
- return (httplib2.Response({"status": "200"}),
- json.dumps(IDENTITY_V2_RESPONSE))
-
-
-def _fake_auth_failure_response():
- # the response body isn't really used in this case, but lets send it anyway
- # to have a safe check in some future change on the rest client.
- body = {
- "unauthorized": {
- "message": "Unauthorized",
- "code": "401"
- }
- }
- return httplib2.Response({"status": "401"}), json.dumps(body)
diff --git a/tempest/tests/fake_tempest_plugin.py b/tempest/tests/fake_tempest_plugin.py
index f718d0b..56aae1e 100644
--- a/tempest/tests/fake_tempest_plugin.py
+++ b/tempest/tests/fake_tempest_plugin.py
@@ -18,6 +18,7 @@
class FakePlugin(plugins.TempestPlugin):
expected_load_test = ["my/test/path", "/home/dir"]
+ expected_service_clients = [{'foo': 'bar'}]
def load_tests(self):
return self.expected_load_test
@@ -28,6 +29,9 @@
def get_opt_lists(self):
return []
+ def get_service_clients(self):
+ return self.expected_service_clients
+
class FakeStevedoreObj(object):
obj = FakePlugin()
@@ -38,3 +42,26 @@
def __init__(self, name='Test1'):
self._name = name
+
+
+class FakePluginNoServiceClients(plugins.TempestPlugin):
+
+ def load_tests(self):
+ return []
+
+ def register_opts(self, conf):
+ return
+
+ def get_opt_lists(self):
+ return []
+
+
+class FakeStevedoreObjNoServiceClients(object):
+ obj = FakePluginNoServiceClients()
+
+ @property
+ def name(self):
+ return self._name
+
+ def __init__(self, name='Test2'):
+ self._name = name
diff --git a/tempest/tests/files/setup.cfg b/tempest/tests/files/setup.cfg
index f6f9f73..bd68708 100644
--- a/tempest/tests/files/setup.cfg
+++ b/tempest/tests/files/setup.cfg
@@ -4,7 +4,7 @@
summary = Fake Project for testing wrapper scripts
author = OpenStack
author-email = openstack-dev@lists.openstack.org
-home-page = http://www.openstack.org/
+home-page = https://docs.openstack.org/tempest/latest/
classifier =
Intended Audience :: Information Technology
Intended Audience :: System Administrators
diff --git a/tempest/tests/files/testr-conf b/tempest/tests/files/testr-conf
index d5ad083..63b3c44 100644
--- a/tempest/tests/files/testr-conf
+++ b/tempest/tests/files/testr-conf
@@ -1,5 +1,3 @@
[DEFAULT]
-test_command=${PYTHON:-python} -m subunit.run discover -t ./ ./tests $LISTOPT $IDOPTION
-test_id_option=--load-list $IDFILE
-test_list_option=--list
+test_path=./tests
group_regex=([^\.]*\.)*
diff --git a/tempest/tests/stress/__init__.py b/tempest/tests/lib/__init__.py
similarity index 100%
copy from tempest/tests/stress/__init__.py
copy to tempest/tests/lib/__init__.py
diff --git a/tempest/tests/negative/__init__.py b/tempest/tests/lib/cli/__init__.py
similarity index 100%
rename from tempest/tests/negative/__init__.py
rename to tempest/tests/lib/cli/__init__.py
diff --git a/tempest/tests/lib/cli/test_command_failed.py b/tempest/tests/lib/cli/test_command_failed.py
new file mode 100644
index 0000000..388028a
--- /dev/null
+++ b/tempest/tests/lib/cli/test_command_failed.py
@@ -0,0 +1,30 @@
+# 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 import exceptions
+from tempest.tests import base
+
+
+class TestOutputParser(base.TestCase):
+
+ def test_command_failed_exception(self):
+ returncode = 1
+ cmd = "foo"
+ stdout = "output"
+ stderr = "error"
+ try:
+ raise exceptions.CommandFailed(returncode, cmd, stdout, stderr)
+ except exceptions.CommandFailed as e:
+ self.assertIn(str(returncode), str(e))
+ self.assertIn(cmd, str(e))
+ self.assertIn(stdout, str(e))
+ self.assertIn(stderr, str(e))
diff --git a/tempest/tests/lib/cli/test_execute.py b/tempest/tests/lib/cli/test_execute.py
new file mode 100644
index 0000000..c069af5
--- /dev/null
+++ b/tempest/tests/lib/cli/test_execute.py
@@ -0,0 +1,151 @@
+#
+# 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 subprocess
+
+import mock
+
+from tempest.lib.cli import base as cli_base
+from tempest.lib import exceptions
+from tempest.tests import base
+
+
+class TestExecute(base.TestCase):
+
+ @mock.patch('subprocess.Popen', autospec=True)
+ def test_execute_success(self, mock_popen):
+ mock_popen.return_value.returncode = 0
+ mock_popen.return_value.communicate.return_value = (
+ "__init__.py", "")
+ result = cli_base.execute("/bin/ls", action="tempest",
+ flags="-l -a")
+ args, kwargs = mock_popen.call_args
+ # Check merge_stderr == False
+ self.assertEqual(subprocess.PIPE, kwargs['stderr'])
+ # Check action and flags are passed
+ args = args[0]
+ # We just tests that all pieces are passed through, we cannot make
+ # assumptions about the order
+ self.assertIn("/bin/ls", args)
+ self.assertIn("-l", args)
+ self.assertIn("-a", args)
+ self.assertIn("tempest", args)
+ # The result is mocked - checking that the mock was invoked correctly
+ self.assertIsInstance(result, str)
+ self.assertIn("__init__.py", result)
+
+ @mock.patch('subprocess.Popen', autospec=True)
+ def test_execute_failure(self, mock_popen):
+ mock_popen.return_value.returncode = 1
+ mock_popen.return_value.communicate.return_value = (
+ "No such option --foobar", "")
+ result = cli_base.execute("/bin/ls", action="tempest.lib",
+ flags="--foobar", merge_stderr=True,
+ fail_ok=True)
+ args, kwargs = mock_popen.call_args
+ # Check the merge_stderr
+ self.assertEqual(subprocess.STDOUT, kwargs['stderr'])
+ # Check action and flags are passed
+ args = args[0]
+ # We just tests that all pieces are passed through, we cannot make
+ # assumptions about the order
+ self.assertIn("/bin/ls", args)
+ self.assertIn("--foobar", args)
+ self.assertIn("tempest.lib", args)
+ # The result is mocked - checking that the mock was invoked correctly
+ self.assertIsInstance(result, str)
+ self.assertIn("--foobar", result)
+
+ @mock.patch('subprocess.Popen', autospec=True)
+ def test_execute_failure_raise_exception(self, mock_popen):
+ mock_popen.return_value.returncode = 1
+ mock_popen.return_value.communicate.return_value = (
+ "No such option --foobar", "")
+ self.assertRaises(exceptions.CommandFailed, cli_base.execute,
+ "/bin/ls", action="tempest", flags="--foobar",
+ merge_stderr=True)
+
+ def test_execute_with_prefix(self):
+ result = cli_base.execute("env", action="",
+ prefix="env NEW_VAR=1")
+ self.assertIsInstance(result, str)
+ self.assertIn("NEW_VAR=1", result)
+
+
+class TestCLIClient(base.TestCase):
+
+ @mock.patch.object(cli_base, 'execute')
+ def test_execute_with_prefix(self, mock_execute):
+ cli = cli_base.CLIClient(prefix='env LAC_ALL=C')
+ cli.glance('action')
+ self.assertEqual(mock_execute.call_count, 1)
+ self.assertEqual(mock_execute.call_args[1],
+ {'prefix': 'env LAC_ALL=C'})
+
+ @mock.patch.object(cli_base, 'execute')
+ def test_execute_with_domain_name(self, mock_execute):
+ cli = cli_base.CLIClient(
+ user_domain_name='default',
+ project_domain_name='default'
+ )
+ cli.glance('action')
+ self.assertEqual(mock_execute.call_count, 1)
+ self.assertIn('--os-user-domain-name default',
+ mock_execute.call_args[0][2])
+ self.assertIn('--os-project-domain-name default',
+ mock_execute.call_args[0][2])
+ self.assertNotIn('--os-user-domain-id',
+ mock_execute.call_args[0][2])
+ self.assertNotIn('--os-project-domain-id',
+ mock_execute.call_args[0][2])
+
+ @mock.patch.object(cli_base, 'execute')
+ def test_execute_with_domain_id(self, mock_execute):
+ cli = cli_base.CLIClient(
+ user_domain_id='default',
+ project_domain_id='default'
+ )
+ cli.glance('action')
+ self.assertEqual(mock_execute.call_count, 1)
+ self.assertIn('--os-user-domain-id default',
+ mock_execute.call_args[0][2])
+ self.assertIn('--os-project-domain-id default',
+ mock_execute.call_args[0][2])
+ self.assertNotIn('--os-user-domain-name',
+ mock_execute.call_args[0][2])
+ self.assertNotIn('--os-project-domain-name',
+ mock_execute.call_args[0][2])
+
+ @mock.patch.object(cli_base, 'execute')
+ def test_execute_with_default_api_version(self, mock_execute):
+ cli = cli_base.CLIClient()
+ cli.openstack('action')
+ self.assertEqual(mock_execute.call_count, 1)
+ self.assertNotIn('--os-identity-api-version ',
+ mock_execute.call_args[0][2])
+
+ @mock.patch.object(cli_base, 'execute')
+ def test_execute_with_empty_api_version(self, mock_execute):
+ cli = cli_base.CLIClient(identity_api_version='')
+ cli.openstack('action')
+ self.assertEqual(mock_execute.call_count, 1)
+ self.assertNotIn('--os-identity-api-version ',
+ mock_execute.call_args[0][2])
+
+ @mock.patch.object(cli_base, 'execute')
+ def test_execute_with_explicit_api_version(self, mock_execute):
+ cli = cli_base.CLIClient(identity_api_version='0.0')
+ cli.openstack('action')
+ self.assertEqual(mock_execute.call_count, 1)
+ self.assertIn('--os-identity-api-version 0.0 ',
+ mock_execute.call_args[0][2])
diff --git a/tempest/tests/lib/cli/test_output_parser.py b/tempest/tests/lib/cli/test_output_parser.py
new file mode 100644
index 0000000..d88dfc3
--- /dev/null
+++ b/tempest/tests/lib/cli/test_output_parser.py
@@ -0,0 +1,177 @@
+# Copyright 2014 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.cli import output_parser
+from tempest.lib import exceptions
+from tempest.tests import base
+
+
+class TestOutputParser(base.TestCase):
+ OUTPUT_LINES = """
++----+------+---------+
+| ID | Name | Status |
++----+------+---------+
+| 11 | foo | BUILD |
+| 21 | bar | ERROR |
+| 31 | bee | None |
++----+------+---------+
+"""
+ OUTPUT_LINES2 = """
++----+-------+---------+
+| ID | Name2 | Status2 |
++----+-------+---------+
+| 41 | aaa | SSSSS |
+| 51 | bbb | TTTTT |
+| 61 | ccc | AAAAA |
++----+-------+---------+
+"""
+
+ EXPECTED_TABLE = {'headers': ['ID', 'Name', 'Status'],
+ 'values': [['11', 'foo', 'BUILD'],
+ ['21', 'bar', 'ERROR'],
+ ['31', 'bee', 'None']]}
+ EXPECTED_TABLE2 = {'headers': ['ID', 'Name2', 'Status2'],
+ 'values': [['41', 'aaa', 'SSSSS'],
+ ['51', 'bbb', 'TTTTT'],
+ ['61', 'ccc', 'AAAAA']]}
+
+ def test_table_with_normal_values(self):
+ actual = output_parser.table(self.OUTPUT_LINES)
+ self.assertIsInstance(actual, dict)
+ self.assertEqual(self.EXPECTED_TABLE, actual)
+
+ def test_table_with_list(self):
+ output_lines = self.OUTPUT_LINES.split('\n')
+ actual = output_parser.table(output_lines)
+ self.assertIsInstance(actual, dict)
+ self.assertEqual(self.EXPECTED_TABLE, actual)
+
+ def test_table_with_invalid_line(self):
+ output_lines = self.OUTPUT_LINES + "aaaa"
+ actual = output_parser.table(output_lines)
+ self.assertIsInstance(actual, dict)
+ self.assertEqual(self.EXPECTED_TABLE, actual)
+
+ def test_tables_with_normal_values(self):
+ output_lines = ('test' + self.OUTPUT_LINES +
+ 'test2' + self.OUTPUT_LINES2)
+ expected = [{'headers': self.EXPECTED_TABLE['headers'],
+ 'label': 'test',
+ 'values': self.EXPECTED_TABLE['values']},
+ {'headers': self.EXPECTED_TABLE2['headers'],
+ 'label': 'test2',
+ 'values': self.EXPECTED_TABLE2['values']}]
+ actual = output_parser.tables(output_lines)
+ self.assertIsInstance(actual, list)
+ self.assertEqual(expected, actual)
+
+ def test_tables_with_invalid_values(self):
+ output_lines = ('test' + self.OUTPUT_LINES +
+ 'test2' + self.OUTPUT_LINES2 + '\n')
+ expected = [{'headers': self.EXPECTED_TABLE['headers'],
+ 'label': 'test',
+ 'values': self.EXPECTED_TABLE['values']},
+ {'headers': self.EXPECTED_TABLE2['headers'],
+ 'label': 'test2',
+ 'values': self.EXPECTED_TABLE2['values']}]
+ actual = output_parser.tables(output_lines)
+ self.assertIsInstance(actual, list)
+ self.assertEqual(expected, actual)
+
+ def test_tables_with_invalid_line(self):
+ output_lines = ('test' + self.OUTPUT_LINES +
+ 'test2' + self.OUTPUT_LINES2 +
+ '+----+-------+---------+')
+ expected = [{'headers': self.EXPECTED_TABLE['headers'],
+ 'label': 'test',
+ 'values': self.EXPECTED_TABLE['values']},
+ {'headers': self.EXPECTED_TABLE2['headers'],
+ 'label': 'test2',
+ 'values': self.EXPECTED_TABLE2['values']}]
+
+ actual = output_parser.tables(output_lines)
+ self.assertIsInstance(actual, list)
+ self.assertEqual(expected, actual)
+
+ LISTING_OUTPUT = """
++----+
+| ID |
++----+
+| 11 |
+| 21 |
+| 31 |
++----+
+"""
+
+ def test_listing(self):
+ expected = [{'ID': '11'}, {'ID': '21'}, {'ID': '31'}]
+ actual = output_parser.listing(self.LISTING_OUTPUT)
+ self.assertIsInstance(actual, list)
+ self.assertEqual(expected, actual)
+
+ def test_details_multiple_with_invalid_line(self):
+ self.assertRaises(exceptions.InvalidStructure,
+ output_parser.details_multiple,
+ self.OUTPUT_LINES)
+
+ DETAILS_LINES1 = """First Table
++----------+--------+
+| Property | Value |
++----------+--------+
+| foo | BUILD |
+| bar | ERROR |
+| bee | None |
++----------+--------+
+"""
+ DETAILS_LINES2 = """Second Table
++----------+--------+
+| Property | Value |
++----------+--------+
+| aaa | VVVVV |
+| bbb | WWWWW |
+| ccc | XXXXX |
++----------+--------+
+"""
+
+ def test_details_with_normal_line_label_false(self):
+ expected = {'foo': 'BUILD', 'bar': 'ERROR', 'bee': 'None'}
+ actual = output_parser.details(self.DETAILS_LINES1)
+ self.assertEqual(expected, actual)
+
+ def test_details_with_normal_line_label_true(self):
+ expected = {'__label': 'First Table',
+ 'foo': 'BUILD', 'bar': 'ERROR', 'bee': 'None'}
+ actual = output_parser.details(self.DETAILS_LINES1, with_label=True)
+ self.assertEqual(expected, actual)
+
+ def test_details_multiple_with_normal_line_label_false(self):
+ expected = [{'foo': 'BUILD', 'bar': 'ERROR', 'bee': 'None'},
+ {'aaa': 'VVVVV', 'bbb': 'WWWWW', 'ccc': 'XXXXX'}]
+ actual = output_parser.details_multiple(self.DETAILS_LINES1 +
+ self.DETAILS_LINES2)
+ self.assertIsInstance(actual, list)
+ self.assertEqual(expected, actual)
+
+ def test_details_multiple_with_normal_line_label_true(self):
+ expected = [{'__label': 'First Table',
+ 'foo': 'BUILD', 'bar': 'ERROR', 'bee': 'None'},
+ {'__label': 'Second Table',
+ 'aaa': 'VVVVV', 'bbb': 'WWWWW', 'ccc': 'XXXXX'}]
+ actual = output_parser.details_multiple(self.DETAILS_LINES1 +
+ self.DETAILS_LINES2,
+ with_label=True)
+ self.assertIsInstance(actual, list)
+ self.assertEqual(expected, actual)
diff --git a/tempest/tests/negative/__init__.py b/tempest/tests/lib/common/__init__.py
similarity index 100%
copy from tempest/tests/negative/__init__.py
copy to tempest/tests/lib/common/__init__.py
diff --git a/tempest/tests/lib/common/test_api_version_request.py b/tempest/tests/lib/common/test_api_version_request.py
new file mode 100644
index 0000000..58e7040
--- /dev/null
+++ b/tempest/tests/lib/common/test_api_version_request.py
@@ -0,0 +1,146 @@
+# Copyright 2014 IBM Corp.
+#
+# 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.common import api_version_request
+from tempest.lib import exceptions
+from tempest.tests import base
+
+
+class APIVersionRequestTests(base.TestCase):
+ def test_valid_version_strings(self):
+ def _test_string(version, exp_major, exp_minor):
+ v = api_version_request.APIVersionRequest(version)
+ self.assertEqual(v.ver_major, exp_major)
+ self.assertEqual(v.ver_minor, exp_minor)
+
+ _test_string("1.1", 1, 1)
+ _test_string("2.10", 2, 10)
+ _test_string("5.234", 5, 234)
+ _test_string("12.5", 12, 5)
+ _test_string("2.0", 2, 0)
+ _test_string("2.200", 2, 200)
+
+ def test_null_version(self):
+ v = api_version_request.APIVersionRequest()
+ self.assertTrue(v.is_null())
+
+ def test_invalid_version_strings(self):
+ self.assertRaises(exceptions.InvalidAPIVersionString,
+ api_version_request.APIVersionRequest, "2")
+
+ self.assertRaises(exceptions.InvalidAPIVersionString,
+ api_version_request.APIVersionRequest, "200")
+
+ self.assertRaises(exceptions.InvalidAPIVersionString,
+ api_version_request.APIVersionRequest, "2.1.4")
+
+ self.assertRaises(exceptions.InvalidAPIVersionString,
+ api_version_request.APIVersionRequest, "200.23.66.3")
+
+ self.assertRaises(exceptions.InvalidAPIVersionString,
+ api_version_request.APIVersionRequest, "5 .3")
+
+ self.assertRaises(exceptions.InvalidAPIVersionString,
+ api_version_request.APIVersionRequest, "5. 3")
+
+ self.assertRaises(exceptions.InvalidAPIVersionString,
+ api_version_request.APIVersionRequest, "5.03")
+
+ self.assertRaises(exceptions.InvalidAPIVersionString,
+ api_version_request.APIVersionRequest, "02.1")
+
+ self.assertRaises(exceptions.InvalidAPIVersionString,
+ api_version_request.APIVersionRequest, "2.001")
+
+ self.assertRaises(exceptions.InvalidAPIVersionString,
+ api_version_request.APIVersionRequest, "")
+
+ self.assertRaises(exceptions.InvalidAPIVersionString,
+ api_version_request.APIVersionRequest, " 2.1")
+
+ self.assertRaises(exceptions.InvalidAPIVersionString,
+ api_version_request.APIVersionRequest, "2.1 ")
+
+ def test_version_comparisons(self):
+ vers2_0 = api_version_request.APIVersionRequest("2.0")
+ vers2_5 = api_version_request.APIVersionRequest("2.5")
+ vers5_23 = api_version_request.APIVersionRequest("5.23")
+ v_null = api_version_request.APIVersionRequest()
+ v_latest = api_version_request.APIVersionRequest('latest')
+
+ self.assertTrue(v_null < vers2_5)
+ self.assertTrue(vers2_0 < vers2_5)
+ self.assertTrue(vers2_0 <= vers2_5)
+ self.assertTrue(vers2_0 <= vers2_0)
+ self.assertTrue(vers2_5 > v_null)
+ self.assertTrue(vers5_23 > vers2_5)
+ self.assertTrue(vers2_0 >= vers2_0)
+ self.assertTrue(vers5_23 >= vers2_5)
+ self.assertTrue(vers2_0 != vers2_5)
+ self.assertTrue(vers2_0 == vers2_0)
+ self.assertTrue(vers2_0 != v_null)
+ self.assertTrue(v_null == v_null)
+ self.assertTrue(vers2_0 <= v_latest)
+ self.assertTrue(vers2_0 != v_latest)
+ self.assertTrue(v_latest == v_latest)
+ self.assertRaises(TypeError, vers2_0.__lt__, "2.1")
+
+ def test_version_matches(self):
+ vers2_0 = api_version_request.APIVersionRequest("2.0")
+ vers2_5 = api_version_request.APIVersionRequest("2.5")
+ vers2_45 = api_version_request.APIVersionRequest("2.45")
+ vers3_3 = api_version_request.APIVersionRequest("3.3")
+ vers3_23 = api_version_request.APIVersionRequest("3.23")
+ vers4_0 = api_version_request.APIVersionRequest("4.0")
+ v_null = api_version_request.APIVersionRequest()
+ v_latest = api_version_request.APIVersionRequest('latest')
+
+ def _check_version_matches(version, version1, version2, check=True):
+ if check:
+ msg = "Version %s does not matches with [%s - %s] range"
+ self.assertTrue(version.matches(version1, version2),
+ msg % (version.get_string(),
+ version1.get_string(),
+ version2.get_string()))
+ else:
+ msg = "Version %s matches with [%s - %s] range"
+ self.assertFalse(version.matches(version1, version2),
+ msg % (version.get_string(),
+ version1.get_string(),
+ version2.get_string()))
+
+ _check_version_matches(vers2_5, vers2_0, vers2_45)
+ _check_version_matches(vers2_5, vers2_0, v_null)
+ _check_version_matches(vers2_0, vers2_0, vers2_5)
+ _check_version_matches(vers3_3, vers2_5, vers3_3)
+ _check_version_matches(vers3_3, v_null, vers3_3)
+ _check_version_matches(vers3_3, v_null, vers4_0)
+ _check_version_matches(vers2_0, vers2_5, vers2_45, False)
+ _check_version_matches(vers3_23, vers2_5, vers3_3, False)
+ _check_version_matches(vers2_5, vers2_45, vers2_0, False)
+ _check_version_matches(vers2_5, vers2_0, v_latest)
+ _check_version_matches(v_latest, v_latest, v_latest)
+ _check_version_matches(vers2_5, v_latest, v_latest, False)
+ _check_version_matches(v_latest, vers2_0, vers4_0, False)
+
+ self.assertRaises(ValueError, v_null.matches, vers2_0, vers2_45)
+
+ def test_get_string(self):
+ vers_string = ["3.23", "latest"]
+ for ver in vers_string:
+ ver_obj = api_version_request.APIVersionRequest(ver)
+ self.assertEqual(ver, ver_obj.get_string())
+
+ self.assertIsNotNone(
+ api_version_request.APIVersionRequest().get_string)
diff --git a/tempest/tests/lib/common/test_api_version_utils.py b/tempest/tests/lib/common/test_api_version_utils.py
new file mode 100644
index 0000000..b99e8d4
--- /dev/null
+++ b/tempest/tests/lib/common/test_api_version_utils.py
@@ -0,0 +1,197 @@
+# Copyright 2015 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.
+
+import six
+import testtools
+
+from tempest.lib.common import api_version_utils
+from tempest.lib import exceptions
+from tempest.tests import base
+
+
+class TestVersionSkipLogic(base.TestCase):
+
+ def _test_version(self, test_min_version, test_max_version,
+ cfg_min_version, cfg_max_version, expected_skip=False):
+ try:
+ api_version_utils.check_skip_with_microversion(test_min_version,
+ test_max_version,
+ cfg_min_version,
+ cfg_max_version)
+ except testtools.TestCase.skipException as e:
+ if not expected_skip:
+ raise testtools.TestCase.failureException(six.text_type(e))
+
+ def test_version_min_in_range(self):
+ self._test_version('2.2', '2.10', '2.1', '2.7')
+
+ def test_version_max_in_range(self):
+ self._test_version('2.1', '2.3', '2.2', '2.7')
+
+ def test_version_cfg_in_range(self):
+ self._test_version('2.2', '2.9', '2.3', '2.7')
+
+ def test_version_equal(self):
+ self._test_version('2.2', '2.2', '2.2', '2.2')
+
+ def test_version_below_cfg_min(self):
+ self._test_version('2.2', '2.4', '2.5', '2.7', expected_skip=True)
+
+ def test_version_above_cfg_max(self):
+ self._test_version('2.8', '2.9', '2.3', '2.7', expected_skip=True)
+
+ def test_version_min_greater_than_max(self):
+ self.assertRaises(exceptions.InvalidAPIVersionRange,
+ self._test_version, '2.8', '2.7', '2.3', '2.7')
+
+ def test_cfg_version_min_greater_than_max(self):
+ self.assertRaises(exceptions.InvalidAPIVersionRange,
+ self._test_version, '2.2', '2.7', '2.9', '2.7')
+
+
+class TestSelectRequestMicroversion(base.TestCase):
+
+ def _test_request_version(self, test_min_version,
+ cfg_min_version, expected_version):
+ selected_version = api_version_utils.select_request_microversion(
+ test_min_version, cfg_min_version)
+ self.assertEqual(expected_version, selected_version)
+
+ def test_cfg_min_version_greater(self):
+ self._test_request_version('2.1', '2.3', expected_version='2.3')
+
+ def test_class_min_version_greater(self):
+ self._test_request_version('2.5', '2.3', expected_version='2.5')
+
+ def test_cfg_min_version_none(self):
+ self._test_request_version('2.5', None, expected_version='2.5')
+
+ def test_class_min_version_none(self):
+ self._test_request_version(None, '2.3', expected_version='2.3')
+
+ def test_both_min_version_none(self):
+ self._test_request_version(None, None, expected_version=None)
+
+ def test_both_min_version_equal(self):
+ self._test_request_version('2.3', '2.3', expected_version='2.3')
+
+
+class TestMicroversionHeaderMatches(base.TestCase):
+
+ def test_header_matches(self):
+ microversion_header_name = 'x-openstack-xyz-api-version'
+ request_microversion = '2.1'
+ test_response = {microversion_header_name: request_microversion}
+ api_version_utils.assert_version_header_matches_request(
+ microversion_header_name, request_microversion, test_response)
+
+ def test_header_does_not_match(self):
+ microversion_header_name = 'x-openstack-xyz-api-version'
+ request_microversion = '2.1'
+ test_response = {microversion_header_name: '2.2'}
+ self.assertRaises(
+ exceptions.InvalidHTTPResponseHeader,
+ api_version_utils.assert_version_header_matches_request,
+ microversion_header_name, request_microversion, test_response)
+
+ def test_header_not_present(self):
+ microversion_header_name = 'x-openstack-xyz-api-version'
+ request_microversion = '2.1'
+ test_response = {}
+ self.assertRaises(
+ exceptions.InvalidHTTPResponseHeader,
+ api_version_utils.assert_version_header_matches_request,
+ microversion_header_name, request_microversion, test_response)
+
+ def test_compare_versions_less_than(self):
+ microversion_header_name = 'x-openstack-xyz-api-version'
+ request_microversion = '2.2'
+ test_response = {microversion_header_name: '2.1'}
+ self.assertFalse(
+ api_version_utils.compare_version_header_to_response(
+ microversion_header_name, request_microversion, test_response,
+ "lt"))
+
+ def test_compare_versions_less_than_equal(self):
+ microversion_header_name = 'x-openstack-xyz-api-version'
+ request_microversion = '2.2'
+ test_response = {microversion_header_name: '2.1'}
+ self.assertFalse(
+ api_version_utils.compare_version_header_to_response(
+ microversion_header_name, request_microversion, test_response,
+ "le"))
+
+ def test_compare_versions_greater_than_equal(self):
+ microversion_header_name = 'x-openstack-xyz-api-version'
+ request_microversion = '2.1'
+ test_response = {microversion_header_name: '2.2'}
+ self.assertFalse(
+ api_version_utils.compare_version_header_to_response(
+ microversion_header_name, request_microversion, test_response,
+ "ge"))
+
+ def test_compare_versions_greater_than(self):
+ microversion_header_name = 'x-openstack-xyz-api-version'
+ request_microversion = '2.1'
+ test_response = {microversion_header_name: '2.2'}
+ self.assertFalse(
+ api_version_utils.compare_version_header_to_response(
+ microversion_header_name, request_microversion, test_response,
+ "gt"))
+
+ def test_compare_versions_equal(self):
+ microversion_header_name = 'x-openstack-xyz-api-version'
+ request_microversion = '2.11'
+ test_response = {microversion_header_name: '2.1'}
+ self.assertFalse(
+ api_version_utils.compare_version_header_to_response(
+ microversion_header_name, request_microversion, test_response,
+ "eq"))
+
+ def test_compare_versions_not_equal(self):
+ microversion_header_name = 'x-openstack-xyz-api-version'
+ request_microversion = '2.1'
+ test_response = {microversion_header_name: '2.1'}
+ self.assertFalse(
+ api_version_utils.compare_version_header_to_response(
+ microversion_header_name, request_microversion, test_response,
+ "ne"))
+
+ def test_compare_versions_with_name_in_microversion(self):
+ microversion_header_name = 'x-openstack-xyz-api-version'
+ request_microversion = 'volume 3.1'
+ test_response = {microversion_header_name: 'volume 3.1'}
+ self.assertTrue(
+ api_version_utils.compare_version_header_to_response(
+ microversion_header_name, request_microversion, test_response,
+ "eq"))
+
+ def test_compare_versions_invalid_operation(self):
+ microversion_header_name = 'x-openstack-xyz-api-version'
+ request_microversion = '2.1'
+ test_response = {microversion_header_name: '2.1'}
+ self.assertRaises(
+ exceptions.InvalidParam,
+ api_version_utils.compare_version_header_to_response,
+ microversion_header_name, request_microversion, test_response,
+ "foo")
+
+ def test_compare_versions_header_not_present(self):
+ microversion_header_name = 'x-openstack-xyz-api-version'
+ request_microversion = '2.1'
+ test_response = {}
+ self.assertFalse(
+ api_version_utils.compare_version_header_to_response(
+ microversion_header_name, request_microversion, test_response,
+ "eq"))
diff --git a/tempest/tests/lib/common/test_cred_client.py b/tempest/tests/lib/common/test_cred_client.py
new file mode 100644
index 0000000..3dff16f
--- /dev/null
+++ b/tempest/tests/lib/common/test_cred_client.py
@@ -0,0 +1,77 @@
+# Copyright 2016 Hewlett Packard Enterprise Development LP
+# 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 mock
+
+from tempest.lib.common import cred_client
+from tempest.tests import base
+
+
+class TestCredClientV2(base.TestCase):
+ def setUp(self):
+ super(TestCredClientV2, self).setUp()
+ self.identity_client = mock.MagicMock()
+ self.projects_client = mock.MagicMock()
+ self.users_client = mock.MagicMock()
+ self.roles_client = mock.MagicMock()
+ self.creds_client = cred_client.V2CredsClient(self.identity_client,
+ self.projects_client,
+ self.users_client,
+ self.roles_client)
+
+ def test_create_project(self):
+ self.projects_client.create_tenant.return_value = {
+ 'tenant': 'a_tenant'
+ }
+ res = self.creds_client.create_project('fake_name', 'desc')
+ self.assertEqual('a_tenant', res)
+ self.projects_client.create_tenant.assert_called_once_with(
+ name='fake_name', description='desc')
+
+ def test_delete_project(self):
+ self.creds_client.delete_project('fake_id')
+ self.projects_client.delete_tenant.assert_called_once_with(
+ 'fake_id')
+
+
+class TestCredClientV3(base.TestCase):
+ def setUp(self):
+ super(TestCredClientV3, self).setUp()
+ self.identity_client = mock.MagicMock()
+ self.projects_client = mock.MagicMock()
+ self.users_client = mock.MagicMock()
+ self.roles_client = mock.MagicMock()
+ self.domains_client = mock.MagicMock()
+ self.domains_client.list_domains.return_value = {
+ 'domains': [{'id': 'fake_domain_id'}]
+ }
+ self.creds_client = cred_client.V3CredsClient(self.identity_client,
+ self.projects_client,
+ self.users_client,
+ self.roles_client,
+ self.domains_client,
+ 'fake_domain')
+
+ def test_create_project(self):
+ self.projects_client.create_project.return_value = {
+ 'project': 'a_tenant'
+ }
+ res = self.creds_client.create_project('fake_name', 'desc')
+ self.assertEqual('a_tenant', res)
+ self.projects_client.create_project.assert_called_once_with(
+ name='fake_name', description='desc', domain_id='fake_domain_id')
+
+ def test_delete_project(self):
+ self.creds_client.delete_project('fake_id')
+ self.projects_client.delete_project.assert_called_once_with(
+ 'fake_id')
diff --git a/tempest/tests/lib/common/test_dynamic_creds.py b/tempest/tests/lib/common/test_dynamic_creds.py
new file mode 100644
index 0000000..ebcf5d1
--- /dev/null
+++ b/tempest/tests/lib/common/test_dynamic_creds.py
@@ -0,0 +1,669 @@
+# Copyright 2014 IBM Corp.
+#
+# 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 fixtures
+import mock
+from oslo_config import cfg
+
+from tempest.common import credentials_factory as credentials
+from tempest import config
+from tempest.lib.common import dynamic_creds
+from tempest.lib.common import rest_client
+from tempest.lib import exceptions as lib_exc
+from tempest.lib.services.identity.v2 import identity_client as v2_iden_client
+from tempest.lib.services.identity.v2 import roles_client as v2_roles_client
+from tempest.lib.services.identity.v2 import tenants_client as \
+ v2_tenants_client
+from tempest.lib.services.identity.v2 import token_client as v2_token_client
+from tempest.lib.services.identity.v2 import users_client as v2_users_client
+from tempest.lib.services.identity.v3 import domains_client
+from tempest.lib.services.identity.v3 import identity_client as v3_iden_client
+from tempest.lib.services.identity.v3 import projects_client as \
+ v3_projects_client
+from tempest.lib.services.identity.v3 import roles_client as v3_roles_client
+from tempest.lib.services.identity.v3 import token_client as v3_token_client
+from tempest.lib.services.identity.v3 import users_client as \
+ v3_users_client
+from tempest.lib.services.network import routers_client
+from tempest.tests import base
+from tempest.tests import fake_config
+from tempest.tests.lib import fake_http
+from tempest.tests.lib import fake_identity
+from tempest.tests.lib.services import registry_fixture
+
+
+class TestDynamicCredentialProvider(base.TestCase):
+
+ fixed_params = {'name': 'test class',
+ 'identity_version': 'v2',
+ 'admin_role': 'admin',
+ 'identity_uri': 'fake_uri'}
+
+ token_client = v2_token_client
+ iden_client = v2_iden_client
+ roles_client = v2_roles_client
+ tenants_client = v2_tenants_client
+ users_client = v2_users_client
+ token_client_class = token_client.TokenClient
+ fake_response = fake_identity._fake_v2_response
+ tenants_client_class = tenants_client.TenantsClient
+ delete_tenant = 'delete_tenant'
+
+ def setUp(self):
+ super(TestDynamicCredentialProvider, self).setUp()
+ self.useFixture(fake_config.ConfigFixture())
+ self.useFixture(registry_fixture.RegistryFixture())
+ self.patchobject(config, 'TempestConfigPrivate',
+ fake_config.FakePrivate)
+ self.patchobject(self.token_client_class, 'raw_request',
+ self.fake_response)
+ cfg.CONF.set_default('operator_role', 'FakeRole',
+ group='object-storage')
+ self._mock_list_ec2_credentials('fake_user_id', 'fake_tenant_id')
+ self.fixed_params.update(
+ admin_creds=self._get_fake_admin_creds())
+
+ def test_tempest_client(self):
+ creds = dynamic_creds.DynamicCredentialProvider(**self.fixed_params)
+ self.assertIsInstance(creds.identity_admin_client,
+ self.iden_client.IdentityClient)
+
+ def _get_fake_admin_creds(self):
+ return credentials.get_credentials(
+ fill_in=False,
+ identity_version=self.fixed_params['identity_version'],
+ username='fake_username', password='fake_password',
+ tenant_name='fake_tenant')
+
+ def _mock_user_create(self, id, name):
+ user_fix = self.useFixture(fixtures.MockPatchObject(
+ self.users_client.UsersClient,
+ 'create_user',
+ return_value=(rest_client.ResponseBody
+ (200, {'user': {'id': id, 'name': name}}))))
+ return user_fix
+
+ def _mock_tenant_create(self, id, name):
+ tenant_fix = self.useFixture(fixtures.MockPatchObject(
+ self.tenants_client.TenantsClient,
+ 'create_tenant',
+ return_value=(rest_client.ResponseBody
+ (200, {'tenant': {'id': id, 'name': name}}))))
+ return tenant_fix
+
+ def _mock_list_roles(self, id, name):
+ roles_fix = self.useFixture(fixtures.MockPatchObject(
+ self.roles_client.RolesClient,
+ 'list_roles',
+ return_value=(rest_client.ResponseBody
+ (200,
+ {'roles': [{'id': id, 'name': name},
+ {'id': '1', 'name': 'FakeRole'},
+ {'id': '2', 'name': 'Member'}]}))))
+ return roles_fix
+
+ def _mock_list_2_roles(self):
+ roles_fix = self.useFixture(fixtures.MockPatchObject(
+ self.roles_client.RolesClient,
+ 'list_roles',
+ return_value=(rest_client.ResponseBody
+ (200,
+ {'roles': [{'id': '1234', 'name': 'role1'},
+ {'id': '1', 'name': 'FakeRole'},
+ {'id': '12345', 'name': 'role2'}]}))))
+ return roles_fix
+
+ def _mock_assign_user_role(self):
+ tenant_fix = self.useFixture(fixtures.MockPatchObject(
+ self.roles_client.RolesClient,
+ 'create_user_role_on_project',
+ return_value=(rest_client.ResponseBody
+ (200, {}))))
+ return tenant_fix
+
+ def _mock_list_role(self):
+ roles_fix = self.useFixture(fixtures.MockPatchObject(
+ self.roles_client.RolesClient,
+ 'list_roles',
+ return_value=(rest_client.ResponseBody
+ (200, {'roles': [
+ {'id': '1', 'name': 'FakeRole'},
+ {'id': '2', 'name': 'Member'}]}))))
+ return roles_fix
+
+ def _mock_list_ec2_credentials(self, user_id, tenant_id):
+ ec2_creds_fix = self.useFixture(fixtures.MockPatchObject(
+ self.users_client.UsersClient,
+ 'list_user_ec2_credentials',
+ return_value=(rest_client.ResponseBody
+ (200, {'credentials': [{
+ 'access': 'fake_access',
+ 'secret': 'fake_secret',
+ 'tenant_id': tenant_id,
+ 'user_id': user_id,
+ 'trust_id': None}]}))))
+ return ec2_creds_fix
+
+ def _mock_network_create(self, iso_creds, id, name):
+ net_fix = self.useFixture(fixtures.MockPatchObject(
+ iso_creds.networks_admin_client,
+ 'create_network',
+ return_value={'network': {'id': id, 'name': name}}))
+ return net_fix
+
+ def _mock_subnet_create(self, iso_creds, id, name):
+ subnet_fix = self.useFixture(fixtures.MockPatchObject(
+ iso_creds.subnets_admin_client,
+ 'create_subnet',
+ return_value={'subnet': {'id': id, 'name': name}}))
+ return subnet_fix
+
+ def _mock_router_create(self, id, name):
+ router_fix = self.useFixture(fixtures.MockPatchObject(
+ routers_client.RoutersClient,
+ 'create_router',
+ return_value={'router': {'id': id, 'name': name}}))
+ return router_fix
+
+ @mock.patch('tempest.lib.common.rest_client.RestClient')
+ def test_primary_creds(self, MockRestClient):
+ creds = dynamic_creds.DynamicCredentialProvider(**self.fixed_params)
+ self._mock_assign_user_role()
+ self._mock_list_role()
+ self._mock_tenant_create('1234', 'fake_prim_tenant')
+ self._mock_user_create('1234', 'fake_prim_user')
+ primary_creds = creds.get_primary_creds()
+ self.assertEqual(primary_creds.username, 'fake_prim_user')
+ self.assertEqual(primary_creds.tenant_name, 'fake_prim_tenant')
+ # Verify IDs
+ self.assertEqual(primary_creds.tenant_id, '1234')
+ self.assertEqual(primary_creds.user_id, '1234')
+
+ @mock.patch('tempest.lib.common.rest_client.RestClient')
+ def test_admin_creds(self, MockRestClient):
+ creds = dynamic_creds.DynamicCredentialProvider(**self.fixed_params)
+ self._mock_list_roles('1234', 'admin')
+ self._mock_user_create('1234', 'fake_admin_user')
+ self._mock_tenant_create('1234', 'fake_admin_tenant')
+
+ user_mock = mock.patch.object(self.roles_client.RolesClient,
+ 'create_user_role_on_project')
+ user_mock.start()
+ self.addCleanup(user_mock.stop)
+ with mock.patch.object(self.roles_client.RolesClient,
+ 'create_user_role_on_project') as user_mock:
+ admin_creds = creds.get_admin_creds()
+ user_mock.assert_has_calls([
+ mock.call('1234', '1234', '1234')])
+ self.assertEqual(admin_creds.username, 'fake_admin_user')
+ self.assertEqual(admin_creds.tenant_name, 'fake_admin_tenant')
+ # Verify IDs
+ self.assertEqual(admin_creds.tenant_id, '1234')
+ self.assertEqual(admin_creds.user_id, '1234')
+
+ @mock.patch('tempest.lib.common.rest_client.RestClient')
+ def test_role_creds(self, MockRestClient):
+ creds = dynamic_creds.DynamicCredentialProvider(**self.fixed_params)
+ self._mock_list_2_roles()
+ self._mock_user_create('1234', 'fake_role_user')
+ self._mock_tenant_create('1234', 'fake_role_tenant')
+
+ user_mock = mock.patch.object(self.roles_client.RolesClient,
+ 'create_user_role_on_project')
+ user_mock.start()
+ self.addCleanup(user_mock.stop)
+ with mock.patch.object(self.roles_client.RolesClient,
+ 'create_user_role_on_project') as user_mock:
+ role_creds = creds.get_creds_by_roles(
+ roles=['role1', 'role2'])
+ calls = user_mock.mock_calls
+ # Assert that the role creation is called with the 2 specified roles
+ self.assertEqual(len(calls), 2)
+ args = map(lambda x: x[1], calls)
+ args = list(args)
+ self.assertIn(('1234', '1234', '1234'), args)
+ self.assertIn(('1234', '1234', '12345'), args)
+ self.assertEqual(role_creds.username, 'fake_role_user')
+ self.assertEqual(role_creds.tenant_name, 'fake_role_tenant')
+ # Verify IDs
+ self.assertEqual(role_creds.tenant_id, '1234')
+ self.assertEqual(role_creds.user_id, '1234')
+
+ @mock.patch('tempest.lib.common.rest_client.RestClient')
+ def test_all_cred_cleanup(self, MockRestClient):
+ creds = dynamic_creds.DynamicCredentialProvider(**self.fixed_params)
+ self._mock_assign_user_role()
+ self._mock_list_role()
+ self._mock_tenant_create('1234', 'fake_prim_tenant')
+ self._mock_user_create('1234', 'fake_prim_user')
+ creds.get_primary_creds()
+ self._mock_tenant_create('12345', 'fake_alt_tenant')
+ self._mock_user_create('12345', 'fake_alt_user')
+ creds.get_alt_creds()
+ self._mock_tenant_create('123456', 'fake_admin_tenant')
+ self._mock_user_create('123456', 'fake_admin_user')
+ self._mock_list_roles('123456', 'admin')
+ creds.get_admin_creds()
+ user_mock = self.patchobject(self.users_client.UsersClient,
+ 'delete_user')
+ tenant_mock = self.patchobject(self.tenants_client_class,
+ self.delete_tenant)
+ creds.clear_creds()
+ # Verify user delete calls
+ calls = user_mock.mock_calls
+ self.assertEqual(len(calls), 3)
+ args = map(lambda x: x[1][0], calls)
+ args = list(args)
+ self.assertIn('1234', args)
+ self.assertIn('12345', args)
+ self.assertIn('123456', args)
+ # Verify tenant delete calls
+ calls = tenant_mock.mock_calls
+ self.assertEqual(len(calls), 3)
+ args = map(lambda x: x[1][0], calls)
+ args = list(args)
+ self.assertIn('1234', args)
+ self.assertIn('12345', args)
+ self.assertIn('123456', args)
+
+ @mock.patch('tempest.lib.common.rest_client.RestClient')
+ def test_alt_creds(self, MockRestClient):
+ creds = dynamic_creds.DynamicCredentialProvider(**self.fixed_params)
+ self._mock_assign_user_role()
+ self._mock_list_role()
+ self._mock_user_create('1234', 'fake_alt_user')
+ self._mock_tenant_create('1234', 'fake_alt_tenant')
+ alt_creds = creds.get_alt_creds()
+ self.assertEqual(alt_creds.username, 'fake_alt_user')
+ self.assertEqual(alt_creds.tenant_name, 'fake_alt_tenant')
+ # Verify IDs
+ self.assertEqual(alt_creds.tenant_id, '1234')
+ self.assertEqual(alt_creds.user_id, '1234')
+
+ @mock.patch('tempest.lib.common.rest_client.RestClient')
+ def test_no_network_creation_with_config_set(self, MockRestClient):
+ creds = dynamic_creds.DynamicCredentialProvider(
+ neutron_available=True, create_networks=False,
+ project_network_cidr='10.100.0.0/16', project_network_mask_bits=28,
+ **self.fixed_params)
+ self._mock_assign_user_role()
+ self._mock_list_role()
+ self._mock_user_create('1234', 'fake_prim_user')
+ self._mock_tenant_create('1234', 'fake_prim_tenant')
+ net = mock.patch.object(creds.networks_admin_client,
+ 'delete_network')
+ net_mock = net.start()
+ subnet = mock.patch.object(creds.subnets_admin_client,
+ 'delete_subnet')
+ subnet_mock = subnet.start()
+ router = mock.patch.object(creds.routers_admin_client,
+ 'delete_router')
+ router_mock = router.start()
+
+ primary_creds = creds.get_primary_creds()
+ self.assertEqual(net_mock.mock_calls, [])
+ self.assertEqual(subnet_mock.mock_calls, [])
+ self.assertEqual(router_mock.mock_calls, [])
+ network = primary_creds.network
+ subnet = primary_creds.subnet
+ router = primary_creds.router
+ self.assertIsNone(network)
+ self.assertIsNone(subnet)
+ self.assertIsNone(router)
+
+ @mock.patch('tempest.lib.common.rest_client.RestClient')
+ def test_network_creation(self, MockRestClient):
+ creds = dynamic_creds.DynamicCredentialProvider(
+ neutron_available=True,
+ project_network_cidr='10.100.0.0/16', project_network_mask_bits=28,
+ **self.fixed_params)
+ self._mock_assign_user_role()
+ self._mock_list_role()
+ self._mock_user_create('1234', 'fake_prim_user')
+ self._mock_tenant_create('1234', 'fake_prim_tenant')
+ self._mock_network_create(creds, '1234', 'fake_net')
+ self._mock_subnet_create(creds, '1234', 'fake_subnet')
+ self._mock_router_create('1234', 'fake_router')
+ router_interface_mock = self.patch(
+ 'tempest.lib.services.network.routers_client.RoutersClient.'
+ 'add_router_interface')
+ primary_creds = creds.get_primary_creds()
+ router_interface_mock.assert_called_once_with('1234', subnet_id='1234')
+ network = primary_creds.network
+ subnet = primary_creds.subnet
+ router = primary_creds.router
+ self.assertEqual(network['id'], '1234')
+ self.assertEqual(network['name'], 'fake_net')
+ self.assertEqual(subnet['id'], '1234')
+ self.assertEqual(subnet['name'], 'fake_subnet')
+ self.assertEqual(router['id'], '1234')
+ self.assertEqual(router['name'], 'fake_router')
+
+ @mock.patch('tempest.lib.common.rest_client.RestClient')
+ def test_network_cleanup(self, MockRestClient):
+ def side_effect(**args):
+ return {"security_groups": [{"tenant_id": args['tenant_id'],
+ "name": args['name'],
+ "description": args['name'],
+ "security_group_rules": [],
+ "id": "sg-%s" % args['tenant_id']}]}
+ creds = dynamic_creds.DynamicCredentialProvider(
+ neutron_available=True,
+ project_network_cidr='10.100.0.0/16', project_network_mask_bits=28,
+ **self.fixed_params)
+ # Create primary tenant and network
+ self._mock_assign_user_role()
+ self._mock_list_role()
+ self._mock_user_create('1234', 'fake_prim_user')
+ self._mock_tenant_create('1234', 'fake_prim_tenant')
+ self._mock_network_create(creds, '1234', 'fake_net')
+ self._mock_subnet_create(creds, '1234', 'fake_subnet')
+ self._mock_router_create('1234', 'fake_router')
+ router_interface_mock = self.patch(
+ 'tempest.lib.services.network.routers_client.RoutersClient.'
+ 'add_router_interface')
+ creds.get_primary_creds()
+ router_interface_mock.assert_called_once_with('1234', subnet_id='1234')
+ router_interface_mock.reset_mock()
+ # Create alternate tenant and network
+ self._mock_user_create('12345', 'fake_alt_user')
+ self._mock_tenant_create('12345', 'fake_alt_tenant')
+ self._mock_network_create(creds, '12345', 'fake_alt_net')
+ self._mock_subnet_create(creds, '12345', 'fake_alt_subnet')
+ self._mock_router_create('12345', 'fake_alt_router')
+ creds.get_alt_creds()
+ router_interface_mock.assert_called_once_with('12345',
+ subnet_id='12345')
+ router_interface_mock.reset_mock()
+ # Create admin tenant and networks
+ self._mock_user_create('123456', 'fake_admin_user')
+ self._mock_tenant_create('123456', 'fake_admin_tenant')
+ self._mock_network_create(creds, '123456', 'fake_admin_net')
+ self._mock_subnet_create(creds, '123456', 'fake_admin_subnet')
+ self._mock_router_create('123456', 'fake_admin_router')
+ self._mock_list_roles('123456', 'admin')
+ creds.get_admin_creds()
+ self.patchobject(self.users_client.UsersClient, 'delete_user')
+ self.patchobject(self.tenants_client_class, self.delete_tenant)
+ net = mock.patch.object(creds.networks_admin_client, 'delete_network')
+ net_mock = net.start()
+ subnet = mock.patch.object(creds.subnets_admin_client, 'delete_subnet')
+ subnet_mock = subnet.start()
+ router = mock.patch.object(creds.routers_admin_client, 'delete_router')
+ router_mock = router.start()
+ remove_router_interface_mock = self.patch(
+ 'tempest.lib.services.network.routers_client.RoutersClient.'
+ 'remove_router_interface')
+ return_values = ({'status': 200}, {'ports': []})
+ port_list_mock = mock.patch.object(creds.ports_admin_client,
+ 'list_ports',
+ return_value=return_values)
+
+ port_list_mock.start()
+ secgroup_list_mock = mock.patch.object(
+ creds.security_groups_admin_client,
+ 'list_security_groups',
+ side_effect=side_effect)
+ secgroup_list_mock.start()
+
+ return_values = fake_http.fake_http_response({}, status=204), ''
+ remove_secgroup_mock = self.patch(
+ 'tempest.lib.services.network.security_groups_client.'
+ 'SecurityGroupsClient.delete', return_value=return_values)
+ creds.clear_creds()
+ # Verify default security group delete
+ calls = remove_secgroup_mock.mock_calls
+ self.assertEqual(len(calls), 3)
+ args = map(lambda x: x[1][0], calls)
+ args = list(args)
+ self.assertIn('v2.0/security-groups/sg-1234', args)
+ self.assertIn('v2.0/security-groups/sg-12345', args)
+ self.assertIn('v2.0/security-groups/sg-123456', args)
+ # Verify remove router interface calls
+ calls = remove_router_interface_mock.mock_calls
+ self.assertEqual(len(calls), 3)
+ args = map(lambda x: (x[1][0], x[2]), calls)
+ args = list(args)
+ self.assertIn(('1234', {'subnet_id': '1234'}), args)
+ self.assertIn(('12345', {'subnet_id': '12345'}), args)
+ self.assertIn(('123456', {'subnet_id': '123456'}), args)
+ # Verify network delete calls
+ calls = net_mock.mock_calls
+ self.assertEqual(len(calls), 3)
+ args = map(lambda x: x[1][0], calls)
+ args = list(args)
+ self.assertIn('1234', args)
+ self.assertIn('12345', args)
+ self.assertIn('123456', args)
+ # Verify subnet delete calls
+ calls = subnet_mock.mock_calls
+ self.assertEqual(len(calls), 3)
+ args = map(lambda x: x[1][0], calls)
+ args = list(args)
+ self.assertIn('1234', args)
+ self.assertIn('12345', args)
+ self.assertIn('123456', args)
+ # Verify router delete calls
+ calls = router_mock.mock_calls
+ self.assertEqual(len(calls), 3)
+ args = map(lambda x: x[1][0], calls)
+ args = list(args)
+ self.assertIn('1234', args)
+ self.assertIn('12345', args)
+ self.assertIn('123456', args)
+
+ @mock.patch('tempest.lib.common.rest_client.RestClient')
+ def test_network_alt_creation(self, MockRestClient):
+ creds = dynamic_creds.DynamicCredentialProvider(
+ neutron_available=True,
+ project_network_cidr='10.100.0.0/16', project_network_mask_bits=28,
+ **self.fixed_params)
+ self._mock_assign_user_role()
+ self._mock_list_role()
+ self._mock_user_create('1234', 'fake_alt_user')
+ self._mock_tenant_create('1234', 'fake_alt_tenant')
+ self._mock_network_create(creds, '1234', 'fake_alt_net')
+ self._mock_subnet_create(creds, '1234', 'fake_alt_subnet')
+ self._mock_router_create('1234', 'fake_alt_router')
+ router_interface_mock = self.patch(
+ 'tempest.lib.services.network.routers_client.RoutersClient.'
+ 'add_router_interface')
+ alt_creds = creds.get_alt_creds()
+ router_interface_mock.assert_called_once_with('1234', subnet_id='1234')
+ network = alt_creds.network
+ subnet = alt_creds.subnet
+ router = alt_creds.router
+ self.assertEqual(network['id'], '1234')
+ self.assertEqual(network['name'], 'fake_alt_net')
+ self.assertEqual(subnet['id'], '1234')
+ self.assertEqual(subnet['name'], 'fake_alt_subnet')
+ self.assertEqual(router['id'], '1234')
+ self.assertEqual(router['name'], 'fake_alt_router')
+
+ @mock.patch('tempest.lib.common.rest_client.RestClient')
+ def test_network_admin_creation(self, MockRestClient):
+ creds = dynamic_creds.DynamicCredentialProvider(
+ neutron_available=True,
+ project_network_cidr='10.100.0.0/16', project_network_mask_bits=28,
+ **self.fixed_params)
+ self._mock_assign_user_role()
+ self._mock_user_create('1234', 'fake_admin_user')
+ self._mock_tenant_create('1234', 'fake_admin_tenant')
+ self._mock_network_create(creds, '1234', 'fake_admin_net')
+ self._mock_subnet_create(creds, '1234', 'fake_admin_subnet')
+ self._mock_router_create('1234', 'fake_admin_router')
+ router_interface_mock = self.patch(
+ 'tempest.lib.services.network.routers_client.RoutersClient.'
+ 'add_router_interface')
+ self._mock_list_roles('123456', 'admin')
+ admin_creds = creds.get_admin_creds()
+ router_interface_mock.assert_called_once_with('1234', subnet_id='1234')
+ network = admin_creds.network
+ subnet = admin_creds.subnet
+ router = admin_creds.router
+ self.assertEqual(network['id'], '1234')
+ self.assertEqual(network['name'], 'fake_admin_net')
+ self.assertEqual(subnet['id'], '1234')
+ self.assertEqual(subnet['name'], 'fake_admin_subnet')
+ self.assertEqual(router['id'], '1234')
+ self.assertEqual(router['name'], 'fake_admin_router')
+
+ @mock.patch('tempest.lib.common.rest_client.RestClient')
+ def test_no_network_resources(self, MockRestClient):
+ net_dict = {
+ 'network': False,
+ 'router': False,
+ 'subnet': False,
+ 'dhcp': False,
+ }
+ creds = dynamic_creds.DynamicCredentialProvider(
+ neutron_available=True,
+ project_network_cidr='10.100.0.0/16', project_network_mask_bits=28,
+ network_resources=net_dict,
+ **self.fixed_params)
+ self._mock_assign_user_role()
+ self._mock_list_role()
+ self._mock_user_create('1234', 'fake_prim_user')
+ self._mock_tenant_create('1234', 'fake_prim_tenant')
+ net = mock.patch.object(creds.networks_admin_client,
+ 'delete_network')
+ net_mock = net.start()
+ subnet = mock.patch.object(creds.subnets_admin_client,
+ 'delete_subnet')
+ subnet_mock = subnet.start()
+ router = mock.patch.object(creds.routers_admin_client,
+ 'delete_router')
+ router_mock = router.start()
+
+ primary_creds = creds.get_primary_creds()
+ self.assertEqual(net_mock.mock_calls, [])
+ self.assertEqual(subnet_mock.mock_calls, [])
+ self.assertEqual(router_mock.mock_calls, [])
+ network = primary_creds.network
+ subnet = primary_creds.subnet
+ router = primary_creds.router
+ self.assertIsNone(network)
+ self.assertIsNone(subnet)
+ self.assertIsNone(router)
+
+ @mock.patch('tempest.lib.common.rest_client.RestClient')
+ def test_router_without_network(self, MockRestClient):
+ net_dict = {
+ 'network': False,
+ 'router': True,
+ 'subnet': False,
+ 'dhcp': False,
+ }
+ creds = dynamic_creds.DynamicCredentialProvider(
+ neutron_available=True,
+ project_network_cidr='10.100.0.0/16', project_network_mask_bits=28,
+ network_resources=net_dict,
+ **self.fixed_params)
+ self._mock_assign_user_role()
+ self._mock_list_role()
+ self._mock_user_create('1234', 'fake_prim_user')
+ self._mock_tenant_create('1234', 'fake_prim_tenant')
+ self.assertRaises(lib_exc.InvalidConfiguration,
+ creds.get_primary_creds)
+
+ @mock.patch('tempest.lib.common.rest_client.RestClient')
+ def test_subnet_without_network(self, MockRestClient):
+ net_dict = {
+ 'network': False,
+ 'router': False,
+ 'subnet': True,
+ 'dhcp': False,
+ }
+ creds = dynamic_creds.DynamicCredentialProvider(
+ neutron_available=True,
+ project_network_cidr='10.100.0.0/16', project_network_mask_bits=28,
+ network_resources=net_dict,
+ **self.fixed_params)
+ self._mock_assign_user_role()
+ self._mock_list_role()
+ self._mock_user_create('1234', 'fake_prim_user')
+ self._mock_tenant_create('1234', 'fake_prim_tenant')
+ self.assertRaises(lib_exc.InvalidConfiguration,
+ creds.get_primary_creds)
+
+ @mock.patch('tempest.lib.common.rest_client.RestClient')
+ def test_dhcp_without_subnet(self, MockRestClient):
+ net_dict = {
+ 'network': False,
+ 'router': False,
+ 'subnet': False,
+ 'dhcp': True,
+ }
+ creds = dynamic_creds.DynamicCredentialProvider(
+ neutron_available=True,
+ project_network_cidr='10.100.0.0/16', project_network_mask_bits=28,
+ network_resources=net_dict,
+ **self.fixed_params)
+ self._mock_assign_user_role()
+ self._mock_list_role()
+ self._mock_user_create('1234', 'fake_prim_user')
+ self._mock_tenant_create('1234', 'fake_prim_tenant')
+ self.assertRaises(lib_exc.InvalidConfiguration,
+ creds.get_primary_creds)
+
+
+class TestDynamicCredentialProviderV3(TestDynamicCredentialProvider):
+
+ fixed_params = {'name': 'test class',
+ 'identity_version': 'v3',
+ 'admin_role': 'admin',
+ 'identity_uri': 'fake_uri'}
+
+ token_client = v3_token_client
+ iden_client = v3_iden_client
+ roles_client = v3_roles_client
+ tenants_client = v3_projects_client
+ users_client = v3_users_client
+ token_client_class = token_client.V3TokenClient
+ fake_response = fake_identity._fake_v3_response
+ tenants_client_class = tenants_client.ProjectsClient
+ delete_tenant = 'delete_project'
+
+ def setUp(self):
+ super(TestDynamicCredentialProviderV3, self).setUp()
+ self.useFixture(fake_config.ConfigFixture())
+ self.useFixture(fixtures.MockPatchObject(
+ domains_client.DomainsClient, 'list_domains',
+ return_value=dict(domains=[dict(id='default',
+ name='Default')])))
+ self.patchobject(self.roles_client.RolesClient,
+ 'create_user_role_on_domain')
+
+ def _mock_list_ec2_credentials(self, user_id, tenant_id):
+ pass
+
+ def _mock_tenant_create(self, id, name):
+ project_fix = self.useFixture(fixtures.MockPatchObject(
+ self.tenants_client.ProjectsClient,
+ 'create_project',
+ return_value=(rest_client.ResponseBody
+ (200, {'project': {'id': id, 'name': name}}))))
+ return project_fix
+
+ @mock.patch('tempest.lib.common.rest_client.RestClient')
+ def test_member_role_creation_with_duplicate(self, rest_client_mock):
+ creds = dynamic_creds.DynamicCredentialProvider(**self.fixed_params)
+ creds.creds_client = mock.MagicMock()
+ creds.creds_client.create_user_role.side_effect = lib_exc.Conflict
+ with mock.patch('tempest.lib.common.dynamic_creds.LOG') as log_mock:
+ creds._create_creds()
+ log_mock.warning.assert_called_once_with(
+ "Member role already exists, ignoring conflict.")
+ creds.creds_client.assign_user_role.assert_called_once_with(
+ mock.ANY, mock.ANY, 'Member')
diff --git a/tempest/tests/lib/common/test_http.py b/tempest/tests/lib/common/test_http.py
new file mode 100644
index 0000000..02436e0
--- /dev/null
+++ b/tempest/tests/lib/common/test_http.py
@@ -0,0 +1,169 @@
+# 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 urllib3
+
+from tempest.lib.common import http
+from tempest.tests import base
+
+
+CERT_NONE = 'CERT_NONE'
+CERT_REQUIRED = 'CERT_REQUIRED'
+CERT_LOCATION = '/etc/ssl/certs/ca-certificates.crt'
+PROXY_URL = 'http://myproxy:3128'
+REQUEST_URL = 'http://10.0.0.107:5000/v2.0'
+REQUEST_METHOD = 'GET'
+
+
+class TestClosingHttp(base.TestCase):
+
+ def closing_http(self, **kwargs):
+ return http.ClosingHttp(**kwargs)
+
+ def test_closing_http(self):
+ connection = self.closing_http()
+
+ self.assertNotIn('cert_reqs', connection.connection_pool_kw)
+ self.assertNotIn('ca_certs', connection.connection_pool_kw)
+ self.assertNotIn('timeout', connection.connection_pool_kw)
+
+ def test_closing_http_with_ca_certs(self):
+ connection = self.closing_http(ca_certs=CERT_LOCATION)
+
+ self.assertEqual(CERT_REQUIRED,
+ connection.connection_pool_kw['cert_reqs'])
+ self.assertEqual(CERT_LOCATION,
+ connection.connection_pool_kw['ca_certs'])
+
+ def test_closing_http_with_dscv(self):
+ connection = self.closing_http(
+ disable_ssl_certificate_validation=True)
+
+ self.assertEqual(CERT_NONE,
+ connection.connection_pool_kw['cert_reqs'])
+ self.assertNotIn('ca_certs',
+ connection.connection_pool_kw)
+
+ def test_closing_http_with_ca_certs_and_dscv(self):
+ connection = self.closing_http(
+ disable_ssl_certificate_validation=True,
+ ca_certs=CERT_LOCATION)
+
+ self.assertEqual(CERT_NONE,
+ connection.connection_pool_kw['cert_reqs'])
+ self.assertNotIn('ca_certs',
+ connection.connection_pool_kw)
+
+ def test_closing_http_with_timeout(self):
+ timeout = 30
+ connection = self.closing_http(timeout=timeout)
+ self.assertEqual(timeout,
+ connection.connection_pool_kw['timeout'])
+
+ def test_request(self):
+ # Given
+ connection = self.closing_http()
+ http_response = urllib3.HTTPResponse()
+ request = self.patch('urllib3.PoolManager.request',
+ return_value=http_response)
+ retry = self.patch('urllib3.util.Retry')
+
+ # When
+ response, data = connection.request(
+ method=REQUEST_METHOD,
+ url=REQUEST_URL)
+
+ # Then
+ request.assert_called_once_with(
+ REQUEST_METHOD,
+ REQUEST_URL,
+ headers={'connection': 'close'},
+ retries=retry(raise_on_redirect=False, redirect=5))
+ self.assertEqual(
+ {'content-location': REQUEST_URL,
+ 'status': str(http_response.status)},
+ response)
+ self.assertEqual(http_response.status, response.status)
+ self.assertEqual(http_response.reason, response.reason)
+ self.assertEqual(http_response.version, response.version)
+ self.assertEqual(http_response.data, data)
+
+ def test_request_with_fields(self):
+ # Given
+ connection = self.closing_http()
+ http_response = urllib3.HTTPResponse()
+ request = self.patch('urllib3.PoolManager.request',
+ return_value=http_response)
+ retry = self.patch('urllib3.util.Retry')
+ fields = object()
+
+ # When
+ connection.request(
+ method=REQUEST_METHOD,
+ url=REQUEST_URL,
+ fields=fields)
+
+ # Then
+ request.assert_called_once_with(
+ REQUEST_METHOD,
+ REQUEST_URL,
+ fields=fields,
+ headers=dict(connection='close'),
+ retries=retry(raise_on_redirect=False, redirect=5))
+
+ def test_request_with_headers(self):
+ # Given
+ connection = self.closing_http()
+ headers = {'Xtra Key': 'Xtra Value'}
+ http_response = urllib3.HTTPResponse(headers=headers)
+ request = self.patch('urllib3.PoolManager.request',
+ return_value=http_response)
+ retry = self.patch('urllib3.util.Retry')
+
+ # When
+ response, _ = connection.request(
+ method=REQUEST_METHOD,
+ url=REQUEST_URL,
+ headers=headers)
+
+ # Then
+ request.assert_called_once_with(
+ REQUEST_METHOD,
+ REQUEST_URL,
+ headers=dict(headers, connection='close'),
+ retries=retry(raise_on_redirect=False, redirect=5))
+ self.assertEqual(
+ {'content-location': REQUEST_URL,
+ 'status': str(http_response.status),
+ 'xtra key': 'Xtra Value'},
+ response)
+
+
+class TestClosingProxyHttp(TestClosingHttp):
+
+ def closing_http(self, proxy_url=PROXY_URL, **kwargs):
+ connection = http.ClosingProxyHttp(proxy_url=proxy_url, **kwargs)
+ self.assertHasProxy(connection, proxy_url)
+ return connection
+
+ def test_class_without_proxy_url(self):
+ self.assertRaises(ValueError, http.ClosingProxyHttp, None)
+
+ def assertHasProxy(self, connection, proxy_url):
+ self.assertIsInstance(connection, http.ClosingProxyHttp)
+ proxy = connection.proxy
+ self.assertEqual(proxy_url,
+ '%s://%s:%i' % (proxy.scheme,
+ proxy.host,
+ proxy.port))
diff --git a/tempest/tests/lib/common/test_jsonschema_validator.py b/tempest/tests/lib/common/test_jsonschema_validator.py
new file mode 100644
index 0000000..8694f3d
--- /dev/null
+++ b/tempest/tests/lib/common/test_jsonschema_validator.py
@@ -0,0 +1,83 @@
+# Copyright 2016 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
+from tempest.lib.common import rest_client
+from tempest.lib import exceptions
+from tempest.tests import base
+from tempest.tests.lib import fake_http
+
+
+class TestJSONSchemaDateTimeFormat(base.TestCase):
+ date_time_schema = [
+ {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'date-time': parameter_types.date_time
+ }
+ }
+ },
+ {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'date-time': parameter_types.date_time_or_null
+ }
+ }
+ }
+ ]
+
+ def test_valid_date_time_format(self):
+ valid_instances = ['2016-10-02T10:00:00-05:00',
+ '2016-10-02T10:00:00+09:00',
+ '2016-10-02T15:00:00Z',
+ '2016-10-02T15:00:00.05Z']
+ resp = fake_http.fake_http_response('', status=200)
+ for instance in valid_instances:
+ body = {'date-time': instance}
+ for schema in self.date_time_schema:
+ rest_client.RestClient.validate_response(schema, resp, body)
+
+ def test_invalid_date_time_format(self):
+ invalid_instances = ['2016-10-02 T10:00:00-05:00',
+ '2016-10-02T 15:00:00',
+ '2016-10-02T15:00:00.05 Z',
+ '2016-10-02:15:00:00.05Z',
+ 'T15:00:00.05Z',
+ '2016:10:02T15:00:00',
+ '2016-10-02T15-00-00',
+ '2016-10-02T15.05Z',
+ '09MAR2015 11:15',
+ '13 Oct 2015 05:55:36 GMT',
+ '']
+ resp = fake_http.fake_http_response('', status=200)
+ for instance in invalid_instances:
+ body = {'date-time': instance}
+ for schema in self.date_time_schema:
+ self.assertRaises(exceptions.InvalidHTTPResponseBody,
+ rest_client.RestClient.validate_response,
+ schema, resp, body)
+
+ def test_date_time_or_null_format(self):
+ instance = None
+ resp = fake_http.fake_http_response('', status=200)
+ body = {'date-time': instance}
+ rest_client.RestClient.validate_response(self.date_time_schema[1],
+ resp, body)
+ self.assertRaises(exceptions.InvalidHTTPResponseBody,
+ rest_client.RestClient.validate_response,
+ self.date_time_schema[0], resp, body)
diff --git a/tempest/tests/lib/common/test_preprov_creds.py b/tempest/tests/lib/common/test_preprov_creds.py
new file mode 100644
index 0000000..25df2a7
--- /dev/null
+++ b/tempest/tests/lib/common/test_preprov_creds.py
@@ -0,0 +1,487 @@
+# Copyright 2014 Hewlett-Packard Development Company, L.P.
+#
+# 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 hashlib
+import os
+import shutil
+
+import mock
+import six
+import testtools
+
+import fixtures
+from oslo_concurrency.fixture import lockutils as lockutils_fixtures
+from oslo_config import cfg
+
+from tempest import config
+from tempest.lib import auth
+from tempest.lib.common import cred_provider
+from tempest.lib.common import preprov_creds
+from tempest.lib import exceptions as lib_exc
+from tempest.tests import base
+from tempest.tests import fake_config
+from tempest.tests.lib import fake_identity
+from tempest.tests.lib.services import registry_fixture
+
+
+class TestPreProvisionedCredentials(base.TestCase):
+
+ fixed_params = {'name': 'test class',
+ 'identity_version': 'v2',
+ 'identity_uri': 'fake_uri',
+ 'test_accounts_file': 'fake_accounts_file',
+ 'accounts_lock_dir': 'fake_locks_dir',
+ 'admin_role': 'admin',
+ 'object_storage_operator_role': 'operator',
+ 'object_storage_reseller_admin_role': 'reseller'}
+
+ identity_response = fake_identity._fake_v2_response
+ token_client = ('tempest.lib.services.identity.v2.token_client'
+ '.TokenClient.raw_request')
+
+ @classmethod
+ def _fake_accounts(cls, admin_role):
+ return [
+ {'username': 'test_user1', 'tenant_name': 'test_tenant1',
+ 'password': 'p'},
+ {'username': 'test_user2', 'project_name': 'test_tenant2',
+ 'password': 'p'},
+ {'username': 'test_user3', 'tenant_name': 'test_tenant3',
+ 'password': 'p'},
+ {'username': 'test_user4', 'project_name': 'test_tenant4',
+ 'password': 'p'},
+ {'username': 'test_user5', 'tenant_name': 'test_tenant5',
+ 'password': 'p'},
+ {'username': 'test_user6', 'project_name': 'test_tenant6',
+ 'password': 'p', 'roles': ['role1', 'role2']},
+ {'username': 'test_user7', 'tenant_name': 'test_tenant7',
+ 'password': 'p', 'roles': ['role2', 'role3']},
+ {'username': 'test_user8', 'project_name': 'test_tenant8',
+ 'password': 'p', 'roles': ['role4', 'role1']},
+ {'username': 'test_user9', 'tenant_name': 'test_tenant9',
+ 'password': 'p', 'roles': ['role1', 'role2', 'role3', 'role4']},
+ {'username': 'test_user10', 'project_name': 'test_tenant10',
+ 'password': 'p', 'roles': ['role1', 'role2', 'role3', 'role4']},
+ {'username': 'test_admin1', 'tenant_name': 'test_tenant11',
+ 'password': 'p', 'roles': [admin_role]},
+ {'username': 'test_admin2', 'project_name': 'test_tenant12',
+ 'password': 'p', 'roles': [admin_role]},
+ {'username': 'test_admin3', 'project_name': 'test_tenant13',
+ 'password': 'p', 'types': ['admin']}]
+
+ def setUp(self):
+ super(TestPreProvisionedCredentials, self).setUp()
+ self.useFixture(fake_config.ConfigFixture())
+ self.patchobject(config, 'TempestConfigPrivate',
+ fake_config.FakePrivate)
+ self.patch(self.token_client, side_effect=self.identity_response)
+ self.useFixture(lockutils_fixtures.ExternalLockFixture())
+ self.test_accounts = self._fake_accounts(cfg.CONF.identity.admin_role)
+ self.accounts_mock = self.useFixture(fixtures.MockPatch(
+ 'tempest.lib.common.preprov_creds.read_accounts_yaml',
+ return_value=self.test_accounts))
+ self.useFixture(fixtures.MockPatch(
+ 'os.path.isfile', return_value=True))
+ # Make sure we leave the registry clean
+ self.useFixture(registry_fixture.RegistryFixture())
+
+ def tearDown(self):
+ super(TestPreProvisionedCredentials, self).tearDown()
+ shutil.rmtree(self.fixed_params['accounts_lock_dir'],
+ ignore_errors=True)
+
+ def _get_hash_list(self, accounts_list):
+ hash_list = []
+ hash_fields = (
+ preprov_creds.PreProvisionedCredentialProvider.HASH_CRED_FIELDS)
+ for account in accounts_list:
+ hash = hashlib.md5()
+ account_for_hash = dict((k, v) for (k, v) in account.items()
+ if k in hash_fields)
+ hash.update(six.text_type(account_for_hash).encode('utf-8'))
+ temp_hash = hash.hexdigest()
+ hash_list.append(temp_hash)
+ return hash_list
+
+ def test_get_hash(self):
+ # Test with all accounts to make sure we try all combinations
+ # and hide no race conditions
+ hash_index = 0
+ for test_cred_dict in self.test_accounts:
+ test_account_class = (
+ preprov_creds.PreProvisionedCredentialProvider(
+ **self.fixed_params))
+ hash_list = self._get_hash_list(self.test_accounts)
+ test_creds = auth.get_credentials(
+ fake_identity.FAKE_AUTH_URL,
+ identity_version=self.fixed_params['identity_version'],
+ **test_cred_dict)
+ results = test_account_class.get_hash(test_creds)
+ self.assertEqual(hash_list[hash_index], results)
+ hash_index += 1
+
+ def test_get_hash_dict(self):
+ test_account_class = preprov_creds.PreProvisionedCredentialProvider(
+ **self.fixed_params)
+ hash_dict = test_account_class.get_hash_dict(
+ self.test_accounts, self.fixed_params['admin_role'])
+ hash_list = self._get_hash_list(self.test_accounts)
+ for hash in hash_list:
+ self.assertIn(hash, hash_dict['creds'].keys())
+ self.assertIn(hash_dict['creds'][hash], self.test_accounts)
+
+ def test_create_hash_file_previous_file(self):
+ # Emulate the lock existing on the filesystem
+ self.useFixture(fixtures.MockPatch(
+ 'os.path.isfile', return_value=True))
+ with mock.patch('six.moves.builtins.open', mock.mock_open(),
+ create=True):
+ test_account_class = (
+ preprov_creds.PreProvisionedCredentialProvider(
+ **self.fixed_params))
+ res = test_account_class._create_hash_file('12345')
+ self.assertFalse(res, "_create_hash_file should return False if the "
+ "pseudo-lock file already exists")
+
+ def test_create_hash_file_no_previous_file(self):
+ # Emulate the lock not existing on the filesystem
+ self.useFixture(fixtures.MockPatch(
+ 'os.path.isfile', return_value=False))
+ with mock.patch('six.moves.builtins.open', mock.mock_open(),
+ create=True):
+ test_account_class = (
+ preprov_creds.PreProvisionedCredentialProvider(
+ **self.fixed_params))
+ res = test_account_class._create_hash_file('12345')
+ self.assertTrue(res, "_create_hash_file should return True if the "
+ "pseudo-lock doesn't already exist")
+
+ @mock.patch('oslo_concurrency.lockutils.lock')
+ def test_get_free_hash_no_previous_accounts(self, lock_mock):
+ # Emulate no pre-existing lock
+ self.useFixture(fixtures.MockPatch(
+ 'os.path.isdir', return_value=False))
+ hash_list = self._get_hash_list(self.test_accounts)
+ mkdir_mock = self.useFixture(fixtures.MockPatch('os.mkdir'))
+ self.useFixture(fixtures.MockPatch(
+ 'os.path.isfile', return_value=False))
+ test_account_class = preprov_creds.PreProvisionedCredentialProvider(
+ **self.fixed_params)
+ with mock.patch('six.moves.builtins.open', mock.mock_open(),
+ create=True) as open_mock:
+ test_account_class._get_free_hash(hash_list)
+ lock_path = os.path.join(self.fixed_params['accounts_lock_dir'],
+ hash_list[0])
+ open_mock.assert_called_once_with(lock_path, 'w')
+ mkdir_path = os.path.join(self.fixed_params['accounts_lock_dir'])
+ mkdir_mock.mock.assert_called_once_with(mkdir_path)
+
+ @mock.patch('oslo_concurrency.lockutils.lock')
+ def test_get_free_hash_no_free_accounts(self, lock_mock):
+ hash_list = self._get_hash_list(self.test_accounts)
+ # Emulate pre-existing lock dir
+ self.useFixture(fixtures.MockPatch('os.path.isdir', return_value=True))
+ # Emulate all locks in list are in use
+ self.useFixture(fixtures.MockPatch(
+ 'os.path.isfile', return_value=True))
+ test_account_class = preprov_creds.PreProvisionedCredentialProvider(
+ **self.fixed_params)
+ with mock.patch('six.moves.builtins.open', mock.mock_open(),
+ create=True):
+ self.assertRaises(lib_exc.InvalidCredentials,
+ test_account_class._get_free_hash, hash_list)
+
+ @mock.patch('oslo_concurrency.lockutils.lock')
+ def test_get_free_hash_some_in_use_accounts(self, lock_mock):
+ # Emulate no pre-existing lock
+ self.useFixture(fixtures.MockPatch('os.path.isdir', return_value=True))
+ hash_list = self._get_hash_list(self.test_accounts)
+ test_account_class = preprov_creds.PreProvisionedCredentialProvider(
+ **self.fixed_params)
+
+ def _fake_is_file(path):
+ # Fake isfile() to return that the path exists unless a specific
+ # hash is in the path
+ if hash_list[3] in path:
+ return False
+ return True
+
+ self.patchobject(os.path, 'isfile', _fake_is_file)
+ with mock.patch('six.moves.builtins.open', mock.mock_open(),
+ create=True) as open_mock:
+ test_account_class._get_free_hash(hash_list)
+ lock_path = os.path.join(self.fixed_params['accounts_lock_dir'],
+ hash_list[3])
+ open_mock.assert_has_calls([mock.call(lock_path, 'w')])
+
+ @mock.patch('oslo_concurrency.lockutils.lock')
+ def test_remove_hash_last_account(self, lock_mock):
+ hash_list = self._get_hash_list(self.test_accounts)
+ # Pretend the pseudo-lock is there
+ self.useFixture(
+ fixtures.MockPatch('os.path.isfile', return_value=True))
+ # Pretend the lock dir is empty
+ self.useFixture(fixtures.MockPatch('os.listdir', return_value=[]))
+ test_account_class = preprov_creds.PreProvisionedCredentialProvider(
+ **self.fixed_params)
+ remove_mock = self.useFixture(fixtures.MockPatch('os.remove'))
+ rmdir_mock = self.useFixture(fixtures.MockPatch('os.rmdir'))
+ test_account_class.remove_hash(hash_list[2])
+ hash_path = os.path.join(self.fixed_params['accounts_lock_dir'],
+ hash_list[2])
+ lock_path = self.fixed_params['accounts_lock_dir']
+ remove_mock.mock.assert_called_once_with(hash_path)
+ rmdir_mock.mock.assert_called_once_with(lock_path)
+
+ @mock.patch('oslo_concurrency.lockutils.lock')
+ def test_remove_hash_not_last_account(self, lock_mock):
+ hash_list = self._get_hash_list(self.test_accounts)
+ # Pretend the pseudo-lock is there
+ self.useFixture(fixtures.MockPatch(
+ 'os.path.isfile', return_value=True))
+ # Pretend the lock dir is empty
+ self.useFixture(fixtures.MockPatch('os.listdir', return_value=[
+ hash_list[1], hash_list[4]]))
+ test_account_class = preprov_creds.PreProvisionedCredentialProvider(
+ **self.fixed_params)
+ remove_mock = self.useFixture(fixtures.MockPatch('os.remove'))
+ rmdir_mock = self.useFixture(fixtures.MockPatch('os.rmdir'))
+ test_account_class.remove_hash(hash_list[2])
+ hash_path = os.path.join(self.fixed_params['accounts_lock_dir'],
+ hash_list[2])
+ remove_mock.mock.assert_called_once_with(hash_path)
+ rmdir_mock.mock.assert_not_called()
+
+ def test_is_multi_user(self):
+ test_accounts_class = preprov_creds.PreProvisionedCredentialProvider(
+ **self.fixed_params)
+ self.assertTrue(test_accounts_class.is_multi_user())
+
+ def test_is_not_multi_user(self):
+ self.test_accounts = [self.test_accounts[0]]
+ self.useFixture(fixtures.MockPatch(
+ 'tempest.lib.common.preprov_creds.read_accounts_yaml',
+ return_value=self.test_accounts))
+ test_accounts_class = preprov_creds.PreProvisionedCredentialProvider(
+ **self.fixed_params)
+ self.assertFalse(test_accounts_class.is_multi_user())
+
+ def test__get_creds_by_roles_one_role(self):
+ test_accounts_class = preprov_creds.PreProvisionedCredentialProvider(
+ **self.fixed_params)
+ hashes = test_accounts_class.hash_dict['roles']['role4']
+ temp_hash = hashes[0]
+ get_free_hash_mock = self.useFixture(fixtures.MockPatchObject(
+ test_accounts_class, '_get_free_hash', return_value=temp_hash))
+ # Test a single role returns all matching roles
+ test_accounts_class._get_creds(roles=['role4'])
+ calls = get_free_hash_mock.mock.mock_calls
+ self.assertEqual(len(calls), 1)
+ args = calls[0][1][0]
+ for i in hashes:
+ self.assertIn(i, args)
+
+ def test__get_creds_by_roles_list_role(self):
+ test_accounts_class = preprov_creds.PreProvisionedCredentialProvider(
+ **self.fixed_params)
+ hashes = test_accounts_class.hash_dict['roles']['role4']
+ hashes2 = test_accounts_class.hash_dict['roles']['role2']
+ hashes = list(set(hashes) & set(hashes2))
+ temp_hash = hashes[0]
+ get_free_hash_mock = self.useFixture(fixtures.MockPatchObject(
+ test_accounts_class, '_get_free_hash', return_value=temp_hash))
+ # Test an intersection of multiple roles
+ test_accounts_class._get_creds(roles=['role2', 'role4'])
+ calls = get_free_hash_mock.mock.mock_calls
+ self.assertEqual(len(calls), 1)
+ args = calls[0][1][0]
+ for i in hashes:
+ self.assertIn(i, args)
+
+ def test__get_creds_by_roles_no_admin(self):
+ test_accounts_class = preprov_creds.PreProvisionedCredentialProvider(
+ **self.fixed_params)
+ hashes = list(test_accounts_class.hash_dict['creds'].keys())
+ admin_hashes = test_accounts_class.hash_dict['roles'][
+ cfg.CONF.identity.admin_role]
+ temp_hash = hashes[0]
+ get_free_hash_mock = self.useFixture(fixtures.MockPatchObject(
+ test_accounts_class, '_get_free_hash', return_value=temp_hash))
+ # Test an intersection of multiple roles
+ test_accounts_class._get_creds()
+ calls = get_free_hash_mock.mock.mock_calls
+ self.assertEqual(len(calls), 1)
+ args = calls[0][1][0]
+ self.assertEqual(len(args), 10)
+ for i in admin_hashes:
+ self.assertNotIn(i, args)
+
+ def test_networks_returned_with_creds(self):
+ test_accounts = [
+ {'username': 'test_user13', 'tenant_name': 'test_tenant13',
+ 'password': 'p', 'resources': {'network': 'network-1'}},
+ {'username': 'test_user14', 'tenant_name': 'test_tenant14',
+ 'password': 'p', 'roles': ['role-7', 'role-11'],
+ 'resources': {'network': 'network-2'}}]
+ self.useFixture(fixtures.MockPatch(
+ 'tempest.lib.common.preprov_creds.read_accounts_yaml',
+ return_value=test_accounts))
+ test_accounts_class = preprov_creds.PreProvisionedCredentialProvider(
+ **self.fixed_params)
+ with mock.patch('tempest.lib.services.network.networks_client.'
+ 'NetworksClient.list_networks',
+ return_value={'networks': [{'name': 'network-2',
+ 'id': 'fake-id',
+ 'label': 'network-2'}]}):
+ creds = test_accounts_class.get_creds_by_roles(['role-7'])
+ self.assertIsInstance(creds, cred_provider.TestResources)
+ network = creds.network
+ self.assertIsNotNone(network)
+ self.assertIn('name', network)
+ self.assertIn('id', network)
+ self.assertEqual('fake-id', network['id'])
+ self.assertEqual('network-2', network['name'])
+
+ def test_get_primary_creds(self):
+ test_accounts_class = preprov_creds.PreProvisionedCredentialProvider(
+ **self.fixed_params)
+ primary_creds = test_accounts_class.get_primary_creds()
+ self.assertNotIn('test_admin', primary_creds.username)
+
+ def test_get_primary_creds_none_available(self):
+ admin_accounts = [x for x in self.test_accounts if 'test_admin'
+ in x['username']]
+ self.useFixture(fixtures.MockPatch(
+ 'tempest.lib.common.preprov_creds.read_accounts_yaml',
+ return_value=admin_accounts))
+ test_accounts_class = preprov_creds.PreProvisionedCredentialProvider(
+ **self.fixed_params)
+ with testtools.ExpectedException(lib_exc.InvalidCredentials):
+ # Get one more
+ test_accounts_class.get_primary_creds()
+
+ def test_get_alt_creds(self):
+ test_accounts_class = preprov_creds.PreProvisionedCredentialProvider(
+ **self.fixed_params)
+ alt_creds = test_accounts_class.get_alt_creds()
+ self.assertNotIn('test_admin', alt_creds.username)
+
+ def test_get_alt_creds_none_available(self):
+ admin_accounts = [x for x in self.test_accounts if 'test_admin'
+ in x['username']]
+ self.useFixture(fixtures.MockPatch(
+ 'tempest.lib.common.preprov_creds.read_accounts_yaml',
+ return_value=admin_accounts))
+ test_accounts_class = preprov_creds.PreProvisionedCredentialProvider(
+ **self.fixed_params)
+ with testtools.ExpectedException(lib_exc.InvalidCredentials):
+ # Get one more
+ test_accounts_class.get_alt_creds()
+
+ def test_get_admin_creds(self):
+ test_accounts_class = preprov_creds.PreProvisionedCredentialProvider(
+ **self.fixed_params)
+ admin_creds = test_accounts_class.get_admin_creds()
+ self.assertIn('test_admin', admin_creds.username)
+
+ def test_get_admin_creds_by_type(self):
+ test_accounts = [
+ {'username': 'test_user10', 'project_name': 'test_tenant10',
+ 'password': 'p', 'roles': ['role1', 'role2', 'role3', 'role4']},
+ {'username': 'test_admin1', 'tenant_name': 'test_tenant11',
+ 'password': 'p', 'types': ['admin']}]
+ self.useFixture(fixtures.MockPatch(
+ 'tempest.lib.common.preprov_creds.read_accounts_yaml',
+ return_value=test_accounts))
+ test_accounts_class = preprov_creds.PreProvisionedCredentialProvider(
+ **self.fixed_params)
+ admin_creds = test_accounts_class.get_admin_creds()
+ self.assertIn('test_admin', admin_creds.username)
+
+ def test_get_admin_creds_by_role(self):
+ test_accounts = [
+ {'username': 'test_user10', 'project_name': 'test_tenant10',
+ 'password': 'p', 'roles': ['role1', 'role2', 'role3', 'role4']},
+ {'username': 'test_admin1', 'tenant_name': 'test_tenant11',
+ 'password': 'p', 'roles': [cfg.CONF.identity.admin_role]}]
+ self.useFixture(fixtures.MockPatch(
+ 'tempest.lib.common.preprov_creds.read_accounts_yaml',
+ return_value=test_accounts))
+ test_accounts_class = preprov_creds.PreProvisionedCredentialProvider(
+ **self.fixed_params)
+ admin_creds = test_accounts_class.get_admin_creds()
+ self.assertIn('test_admin', admin_creds.username)
+
+ def test_get_admin_creds_none_available(self):
+ non_admin_accounts = [x for x in self.test_accounts if 'test_admin'
+ not in x['username']]
+ self.useFixture(fixtures.MockPatch(
+ 'tempest.lib.common.preprov_creds.read_accounts_yaml',
+ return_value=non_admin_accounts))
+ test_accounts_class = preprov_creds.PreProvisionedCredentialProvider(
+ **self.fixed_params)
+ with testtools.ExpectedException(lib_exc.InvalidCredentials):
+ # Get one more
+ test_accounts_class.get_admin_creds()
+
+
+class TestPreProvisionedCredentialsV3(TestPreProvisionedCredentials):
+
+ fixed_params = {'name': 'test class',
+ 'identity_version': 'v3',
+ 'identity_uri': 'fake_uri',
+ 'test_accounts_file': 'fake_accounts_file',
+ 'accounts_lock_dir': 'fake_locks_dir_v3',
+ 'admin_role': 'admin',
+ 'object_storage_operator_role': 'operator',
+ 'object_storage_reseller_admin_role': 'reseller'}
+
+ identity_response = fake_identity._fake_v3_response
+ token_client = ('tempest.lib.services.identity.v3.token_client'
+ '.V3TokenClient.raw_request')
+
+ @classmethod
+ def _fake_accounts(cls, admin_role):
+ return [
+ {'username': 'test_user1', 'project_name': 'test_project1',
+ 'domain_name': 'domain', 'password': 'p'},
+ {'username': 'test_user2', 'project_name': 'test_project2',
+ 'domain_name': 'domain', 'password': 'p'},
+ {'username': 'test_user3', 'project_name': 'test_project3',
+ 'domain_name': 'domain', 'password': 'p'},
+ {'username': 'test_user4', 'project_name': 'test_project4',
+ 'domain_name': 'domain', 'password': 'p'},
+ {'username': 'test_user5', 'project_name': 'test_project5',
+ 'domain_name': 'domain', 'password': 'p'},
+ {'username': 'test_user6', 'project_name': 'test_project6',
+ 'domain_name': 'domain', 'password': 'p',
+ 'roles': ['role1', 'role2']},
+ {'username': 'test_user7', 'project_name': 'test_project7',
+ 'domain_name': 'domain', 'password': 'p',
+ 'roles': ['role2', 'role3']},
+ {'username': 'test_user8', 'project_name': 'test_project8',
+ 'domain_name': 'domain', 'password': 'p',
+ 'roles': ['role4', 'role1']},
+ {'username': 'test_user9', 'project_name': 'test_project9',
+ 'domain_name': 'domain', 'password': 'p',
+ 'roles': ['role1', 'role2', 'role3', 'role4']},
+ {'username': 'test_user10', 'project_name': 'test_project10',
+ 'domain_name': 'domain', 'password': 'p',
+ 'roles': ['role1', 'role2', 'role3', 'role4']},
+ {'username': 'test_admin1', 'project_name': 'test_project11',
+ 'domain_name': 'domain', 'password': 'p', 'roles': [admin_role]},
+ {'username': 'test_admin2', 'project_name': 'test_project12',
+ 'domain_name': 'domain', 'password': 'p', 'roles': [admin_role]},
+ {'username': 'test_admin3', 'project_name': 'test_tenant13',
+ 'domain_name': 'domain', 'password': 'p', 'types': ['admin']}]
diff --git a/tempest/tests/lib/common/test_rest_client.py b/tempest/tests/lib/common/test_rest_client.py
new file mode 100644
index 0000000..4c0bb57
--- /dev/null
+++ b/tempest/tests/lib/common/test_rest_client.py
@@ -0,0 +1,1158 @@
+# Copyright 2013 IBM Corp.
+#
+# 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
+import json
+
+import fixtures
+import jsonschema
+import six
+
+from tempest.lib.common import http
+from tempest.lib.common import rest_client
+from tempest.lib import exceptions
+from tempest.tests import base
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib import fake_http
+import tempest.tests.utils as utils
+
+
+class BaseRestClientTestClass(base.TestCase):
+
+ url = 'fake_endpoint'
+
+ def setUp(self):
+ super(BaseRestClientTestClass, self).setUp()
+ self.fake_auth_provider = fake_auth_provider.FakeAuthProvider()
+ self.rest_client = rest_client.RestClient(
+ self.fake_auth_provider, None, None)
+ self.patchobject(http.ClosingHttp, 'request', self.fake_http.request)
+ self.useFixture(fixtures.MockPatchObject(self.rest_client,
+ '_log_request'))
+
+
+class TestRestClientHTTPMethods(BaseRestClientTestClass):
+ def setUp(self):
+ self.fake_http = fake_http.fake_httplib2()
+ super(TestRestClientHTTPMethods, self).setUp()
+ self.useFixture(fixtures.MockPatchObject(self.rest_client,
+ '_error_checker'))
+
+ def test_post(self):
+ __, return_dict = self.rest_client.post(self.url, {}, {})
+ self.assertEqual('POST', return_dict['method'])
+
+ def test_get(self):
+ __, return_dict = self.rest_client.get(self.url)
+ self.assertEqual('GET', return_dict['method'])
+
+ def test_delete(self):
+ __, return_dict = self.rest_client.delete(self.url)
+ self.assertEqual('DELETE', return_dict['method'])
+
+ def test_patch(self):
+ __, return_dict = self.rest_client.patch(self.url, {}, {})
+ self.assertEqual('PATCH', return_dict['method'])
+
+ def test_put(self):
+ __, return_dict = self.rest_client.put(self.url, {}, {})
+ self.assertEqual('PUT', return_dict['method'])
+
+ def test_head(self):
+ self.useFixture(fixtures.MockPatchObject(self.rest_client,
+ 'response_checker'))
+ __, return_dict = self.rest_client.head(self.url)
+ self.assertEqual('HEAD', return_dict['method'])
+
+ def test_copy(self):
+ __, return_dict = self.rest_client.copy(self.url)
+ self.assertEqual('COPY', return_dict['method'])
+
+
+class TestRestClientNotFoundHandling(BaseRestClientTestClass):
+ def setUp(self):
+ self.fake_http = fake_http.fake_httplib2(404)
+ super(TestRestClientNotFoundHandling, self).setUp()
+
+ def test_post(self):
+ self.assertRaises(exceptions.NotFound, self.rest_client.post,
+ self.url, {}, {})
+
+
+class TestRestClientHeadersJSON(TestRestClientHTTPMethods):
+
+ def _verify_headers(self, resp):
+ resp = dict((k.lower(), v) for k, v in six.iteritems(resp))
+ self.assertEqual(self.header_value, resp['accept'])
+ self.assertEqual(self.header_value, resp['content-type'])
+
+ def setUp(self):
+ super(TestRestClientHeadersJSON, self).setUp()
+ self.header_value = 'application/json'
+
+ def test_post(self):
+ resp, __ = self.rest_client.post(self.url, {})
+ self._verify_headers(resp)
+
+ def test_get(self):
+ resp, __ = self.rest_client.get(self.url)
+ self._verify_headers(resp)
+
+ def test_delete(self):
+ resp, __ = self.rest_client.delete(self.url)
+ self._verify_headers(resp)
+
+ def test_patch(self):
+ resp, __ = self.rest_client.patch(self.url, {})
+ self._verify_headers(resp)
+
+ def test_put(self):
+ resp, __ = self.rest_client.put(self.url, {})
+ self._verify_headers(resp)
+
+ def test_head(self):
+ self.useFixture(fixtures.MockPatchObject(self.rest_client,
+ 'response_checker'))
+ resp, __ = self.rest_client.head(self.url)
+ self._verify_headers(resp)
+
+ def test_copy(self):
+ resp, __ = self.rest_client.copy(self.url)
+ self._verify_headers(resp)
+
+
+class TestRestClientUpdateHeaders(BaseRestClientTestClass):
+ def setUp(self):
+ self.fake_http = fake_http.fake_httplib2()
+ super(TestRestClientUpdateHeaders, self).setUp()
+ self.useFixture(fixtures.MockPatchObject(self.rest_client,
+ '_error_checker'))
+ self.headers = {'X-Configuration-Session': 'session_id'}
+
+ def test_post_update_headers(self):
+ __, return_dict = self.rest_client.post(self.url, {},
+ extra_headers=True,
+ headers=self.headers)
+
+ self.assertDictContainsSubset(
+ {'X-Configuration-Session': 'session_id',
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'},
+ return_dict['headers']
+ )
+
+ def test_get_update_headers(self):
+ __, return_dict = self.rest_client.get(self.url,
+ extra_headers=True,
+ headers=self.headers)
+
+ self.assertDictContainsSubset(
+ {'X-Configuration-Session': 'session_id',
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'},
+ return_dict['headers']
+ )
+
+ def test_delete_update_headers(self):
+ __, return_dict = self.rest_client.delete(self.url,
+ extra_headers=True,
+ headers=self.headers)
+
+ self.assertDictContainsSubset(
+ {'X-Configuration-Session': 'session_id',
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'},
+ return_dict['headers']
+ )
+
+ def test_patch_update_headers(self):
+ __, return_dict = self.rest_client.patch(self.url, {},
+ extra_headers=True,
+ headers=self.headers)
+
+ self.assertDictContainsSubset(
+ {'X-Configuration-Session': 'session_id',
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'},
+ return_dict['headers']
+ )
+
+ def test_put_update_headers(self):
+ __, return_dict = self.rest_client.put(self.url, {},
+ extra_headers=True,
+ headers=self.headers)
+
+ self.assertDictContainsSubset(
+ {'X-Configuration-Session': 'session_id',
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'},
+ return_dict['headers']
+ )
+
+ def test_head_update_headers(self):
+ self.useFixture(fixtures.MockPatchObject(self.rest_client,
+ 'response_checker'))
+
+ __, return_dict = self.rest_client.head(self.url,
+ extra_headers=True,
+ headers=self.headers)
+
+ self.assertDictContainsSubset(
+ {'X-Configuration-Session': 'session_id',
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'},
+ return_dict['headers']
+ )
+
+ def test_copy_update_headers(self):
+ __, return_dict = self.rest_client.copy(self.url,
+ extra_headers=True,
+ headers=self.headers)
+
+ self.assertDictContainsSubset(
+ {'X-Configuration-Session': 'session_id',
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'},
+ return_dict['headers']
+ )
+
+
+class TestRestClientParseRespJSON(BaseRestClientTestClass):
+ TYPE = "json"
+
+ keys = ["fake_key1", "fake_key2"]
+ values = ["fake_value1", "fake_value2"]
+ item_expected = dict((key, value) for (key, value) in zip(keys, values))
+ list_expected = {"body_list": [
+ {keys[0]: values[0]},
+ {keys[1]: values[1]},
+ ]}
+ dict_expected = {"body_dict": {
+ keys[0]: values[0],
+ keys[1]: values[1],
+ }}
+ null_dict = {}
+
+ def setUp(self):
+ self.fake_http = fake_http.fake_httplib2()
+ super(TestRestClientParseRespJSON, self).setUp()
+ self.rest_client.TYPE = self.TYPE
+
+ def test_parse_resp_body_item(self):
+ body = self.rest_client._parse_resp(json.dumps(self.item_expected))
+ self.assertEqual(self.item_expected, body)
+
+ def test_parse_resp_body_list(self):
+ body = self.rest_client._parse_resp(json.dumps(self.list_expected))
+ self.assertEqual(self.list_expected["body_list"], body)
+
+ def test_parse_resp_body_dict(self):
+ body = self.rest_client._parse_resp(json.dumps(self.dict_expected))
+ self.assertEqual(self.dict_expected["body_dict"], body)
+
+ def test_parse_resp_two_top_keys(self):
+ dict_two_keys = self.dict_expected.copy()
+ dict_two_keys.update({"second_key": ""})
+ body = self.rest_client._parse_resp(json.dumps(dict_two_keys))
+ self.assertEqual(dict_two_keys, body)
+
+ def test_parse_resp_one_top_key_without_list_or_dict(self):
+ data = {"one_top_key": "not_list_or_dict_value"}
+ body = self.rest_client._parse_resp(json.dumps(data))
+ self.assertEqual(data, body)
+
+ def test_parse_nullable_dict(self):
+ body = self.rest_client._parse_resp(json.dumps(self.null_dict))
+ self.assertEqual(self.null_dict, body)
+
+ def test_parse_empty_list(self):
+ empty_list = []
+ body = self.rest_client._parse_resp(json.dumps(empty_list))
+ self.assertEqual(empty_list, body)
+
+
+class TestRestClientErrorCheckerJSON(base.TestCase):
+ c_type = "application/json"
+
+ def set_data(self, r_code, enc=None, r_body=None, absolute_limit=True):
+ if enc is None:
+ enc = self.c_type
+ resp_dict = {'status': r_code, 'content-type': enc}
+ resp_body = {'resp_body': 'fake_resp_body'}
+
+ if absolute_limit is False:
+ resp_dict.update({'retry-after': 120})
+ resp_body.update({'overLimit': {'message': 'fake_message'}})
+ resp = fake_http.fake_http_response(headers=resp_dict,
+ status=int(r_code),
+ body=json.dumps(resp_body))
+ data = {
+ "resp": resp,
+ "resp_body": json.dumps(resp_body)
+ }
+ if r_body is not None:
+ data.update({"resp_body": r_body})
+ return data
+
+ def setUp(self):
+ super(TestRestClientErrorCheckerJSON, self).setUp()
+ self.rest_client = rest_client.RestClient(
+ fake_auth_provider.FakeAuthProvider(), None, None)
+
+ def test_response_less_than_400(self):
+ self.rest_client._error_checker(**self.set_data("399"))
+
+ def _test_error_checker(self, exception_type, data):
+ e = self.assertRaises(exception_type,
+ self.rest_client._error_checker,
+ **data)
+ self.assertEqual(e.resp, data['resp'])
+ self.assertTrue(hasattr(e, 'resp_body'))
+ return e
+
+ def test_response_400(self):
+ self._test_error_checker(exceptions.BadRequest, self.set_data("400"))
+
+ def test_response_401(self):
+ self._test_error_checker(exceptions.Unauthorized, self.set_data("401"))
+
+ def test_response_403(self):
+ self._test_error_checker(exceptions.Forbidden, self.set_data("403"))
+
+ def test_response_404(self):
+ self._test_error_checker(exceptions.NotFound, self.set_data("404"))
+
+ def test_response_409(self):
+ self._test_error_checker(exceptions.Conflict, self.set_data("409"))
+
+ def test_response_410(self):
+ self._test_error_checker(exceptions.Gone, self.set_data("410"))
+
+ def test_response_412(self):
+ self._test_error_checker(exceptions.PreconditionFailed,
+ self.set_data("412"))
+
+ def test_response_413(self):
+ self._test_error_checker(exceptions.OverLimit, self.set_data("413"))
+
+ def test_response_413_without_absolute_limit(self):
+ self._test_error_checker(exceptions.RateLimitExceeded,
+ self.set_data("413", absolute_limit=False))
+
+ def test_response_415(self):
+ self._test_error_checker(exceptions.InvalidContentType,
+ self.set_data("415"))
+
+ def test_response_422(self):
+ self._test_error_checker(exceptions.UnprocessableEntity,
+ self.set_data("422"))
+
+ def test_response_500_with_text(self):
+ # _parse_resp is expected to return 'str'
+ self._test_error_checker(exceptions.ServerFault, self.set_data("500"))
+
+ def test_response_501_with_text(self):
+ self._test_error_checker(exceptions.NotImplemented,
+ self.set_data("501"))
+
+ def test_response_400_with_dict(self):
+ r_body = '{"resp_body": {"err": "fake_resp_body"}}'
+ e = self._test_error_checker(exceptions.BadRequest,
+ self.set_data("400", r_body=r_body))
+
+ if self.c_type == 'application/json':
+ expected = {"err": "fake_resp_body"}
+ else:
+ expected = r_body
+ self.assertEqual(expected, e.resp_body)
+
+ def test_response_401_with_dict(self):
+ r_body = '{"resp_body": {"err": "fake_resp_body"}}'
+ e = self._test_error_checker(exceptions.Unauthorized,
+ self.set_data("401", r_body=r_body))
+
+ if self.c_type == 'application/json':
+ expected = {"err": "fake_resp_body"}
+ else:
+ expected = r_body
+ self.assertEqual(expected, e.resp_body)
+
+ def test_response_403_with_dict(self):
+ r_body = '{"resp_body": {"err": "fake_resp_body"}}'
+ e = self._test_error_checker(exceptions.Forbidden,
+ self.set_data("403", r_body=r_body))
+
+ if self.c_type == 'application/json':
+ expected = {"err": "fake_resp_body"}
+ else:
+ expected = r_body
+ self.assertEqual(expected, e.resp_body)
+
+ def test_response_404_with_dict(self):
+ r_body = '{"resp_body": {"err": "fake_resp_body"}}'
+ e = self._test_error_checker(exceptions.NotFound,
+ self.set_data("404", r_body=r_body))
+
+ if self.c_type == 'application/json':
+ expected = {"err": "fake_resp_body"}
+ else:
+ expected = r_body
+ self.assertEqual(expected, e.resp_body)
+
+ def test_response_404_with_invalid_dict(self):
+ r_body = '{"foo": "bar"]'
+ e = self._test_error_checker(exceptions.NotFound,
+ self.set_data("404", r_body=r_body))
+
+ expected = r_body
+ self.assertEqual(expected, e.resp_body)
+
+ def test_response_410_with_dict(self):
+ r_body = '{"resp_body": {"err": "fake_resp_body"}}'
+ e = self._test_error_checker(exceptions.Gone,
+ self.set_data("410", r_body=r_body))
+
+ if self.c_type == 'application/json':
+ expected = {"err": "fake_resp_body"}
+ else:
+ expected = r_body
+ self.assertEqual(expected, e.resp_body)
+
+ def test_response_410_with_invalid_dict(self):
+ r_body = '{"foo": "bar"]'
+ e = self._test_error_checker(exceptions.Gone,
+ self.set_data("410", r_body=r_body))
+
+ expected = r_body
+ self.assertEqual(expected, e.resp_body)
+
+ def test_response_409_with_dict(self):
+ r_body = '{"resp_body": {"err": "fake_resp_body"}}'
+ e = self._test_error_checker(exceptions.Conflict,
+ self.set_data("409", r_body=r_body))
+
+ if self.c_type == 'application/json':
+ expected = {"err": "fake_resp_body"}
+ else:
+ expected = r_body
+ self.assertEqual(expected, e.resp_body)
+
+ def test_response_500_with_dict(self):
+ r_body = '{"resp_body": {"err": "fake_resp_body"}}'
+ e = self._test_error_checker(exceptions.ServerFault,
+ self.set_data("500", r_body=r_body))
+
+ if self.c_type == 'application/json':
+ expected = {"err": "fake_resp_body"}
+ else:
+ expected = r_body
+ self.assertEqual(expected, e.resp_body)
+
+ def test_response_501_with_dict(self):
+ r_body = '{"resp_body": {"err": "fake_resp_body"}}'
+ self._test_error_checker(exceptions.NotImplemented,
+ self.set_data("501", r_body=r_body))
+
+ def test_response_bigger_than_400(self):
+ # Any response code, that bigger than 400, and not in
+ # (401, 403, 404, 409, 412, 413, 422, 500, 501)
+ self._test_error_checker(exceptions.UnexpectedResponseCode,
+ self.set_data("402"))
+
+
+class TestRestClientErrorCheckerTEXT(TestRestClientErrorCheckerJSON):
+ c_type = "text/plain"
+
+ def test_fake_content_type(self):
+ # This test is required only in one exemplar
+ # Any response code, that bigger than 400, and not in
+ # (401, 403, 404, 409, 413, 422, 500, 501)
+ self._test_error_checker(exceptions.UnexpectedContentType,
+ self.set_data("405", enc="fake_enc"))
+
+ def test_response_413_without_absolute_limit(self):
+ # Skip this test because rest_client cannot get overLimit message
+ # from text body.
+ pass
+
+
+class TestRestClientUtils(BaseRestClientTestClass):
+
+ def _is_resource_deleted(self, resource_id):
+ if not isinstance(self.retry_pass, int):
+ return False
+ if self.retry_count >= self.retry_pass:
+ return True
+ self.retry_count = self.retry_count + 1
+ return False
+
+ def setUp(self):
+ self.fake_http = fake_http.fake_httplib2()
+ super(TestRestClientUtils, self).setUp()
+ self.retry_count = 0
+ self.retry_pass = None
+ self.original_deleted_method = self.rest_client.is_resource_deleted
+ self.rest_client.is_resource_deleted = self._is_resource_deleted
+
+ def test_wait_for_resource_deletion(self):
+ self.retry_pass = 2
+ # Ensure timeout long enough for loop execution to hit retry count
+ self.rest_client.build_timeout = 500
+ sleep_mock = self.patch('time.sleep')
+ self.rest_client.wait_for_resource_deletion('1234')
+ self.assertEqual(len(sleep_mock.mock_calls), 2)
+
+ def test_wait_for_resource_deletion_not_deleted(self):
+ self.patch('time.sleep')
+ # Set timeout to be very quick to force exception faster
+ timeout = 1
+ self.rest_client.build_timeout = timeout
+
+ time_mock = self.patch('time.time')
+ time_mock.side_effect = utils.generate_timeout_series(timeout)
+
+ self.assertRaises(exceptions.TimeoutException,
+ self.rest_client.wait_for_resource_deletion,
+ '1234')
+
+ # time.time() should be called twice, first to start the timer
+ # and then to compute the timedelta
+ self.assertEqual(2, time_mock.call_count)
+
+ def test_wait_for_deletion_with_unimplemented_deleted_method(self):
+ self.rest_client.is_resource_deleted = self.original_deleted_method
+ self.assertRaises(NotImplementedError,
+ self.rest_client.wait_for_resource_deletion,
+ '1234')
+
+ def test_get_versions(self):
+ self.rest_client._parse_resp = lambda x: [{'id': 'v1'}, {'id': 'v2'}]
+ actual_resp, actual_versions = self.rest_client.get_versions()
+ self.assertEqual(['v1', 'v2'], list(actual_versions))
+
+ def test__str__(self):
+ def get_token():
+ return "deadbeef"
+
+ self.fake_auth_provider.get_token = get_token
+ self.assertIsNotNone(str(self.rest_client))
+
+
+class TestRateLimiting(BaseRestClientTestClass):
+
+ def setUp(self):
+ self.fake_http = fake_http.fake_httplib2()
+ super(TestRateLimiting, self).setUp()
+
+ def test__get_retry_after_delay_with_integer(self):
+ resp = {'retry-after': '123'}
+ self.assertEqual(123, self.rest_client._get_retry_after_delay(resp))
+
+ def test__get_retry_after_delay_with_http_date(self):
+ resp = {
+ 'date': 'Mon, 4 Apr 2016 21:56:23 GMT',
+ 'retry-after': 'Mon, 4 Apr 2016 21:58:26 GMT',
+ }
+ self.assertEqual(123, self.rest_client._get_retry_after_delay(resp))
+
+ def test__get_retry_after_delay_of_zero_with_integer(self):
+ resp = {'retry-after': '0'}
+ self.assertEqual(1, self.rest_client._get_retry_after_delay(resp))
+
+ def test__get_retry_after_delay_of_zero_with_http_date(self):
+ resp = {
+ 'date': 'Mon, 4 Apr 2016 21:56:23 GMT',
+ 'retry-after': 'Mon, 4 Apr 2016 21:56:23 GMT',
+ }
+ self.assertEqual(1, self.rest_client._get_retry_after_delay(resp))
+
+ def test__get_retry_after_delay_with_missing_date_header(self):
+ resp = {
+ 'retry-after': 'Mon, 4 Apr 2016 21:58:26 GMT',
+ }
+ self.assertRaises(ValueError, self.rest_client._get_retry_after_delay,
+ resp)
+
+ def test__get_retry_after_delay_with_invalid_http_date(self):
+ resp = {
+ 'retry-after': 'Mon, 4 AAA 2016 21:58:26 GMT',
+ 'date': 'Mon, 4 Apr 2016 21:56:23 GMT',
+ }
+ self.assertRaises(ValueError, self.rest_client._get_retry_after_delay,
+ resp)
+
+ def test__get_retry_after_delay_with_missing_retry_after_header(self):
+ self.assertRaises(ValueError, self.rest_client._get_retry_after_delay,
+ {})
+
+ def test_is_absolute_limit_gives_false_with_retry_after(self):
+ resp = {'retry-after': 123}
+
+ # is_absolute_limit() requires the overLimit body to be unwrapped
+ resp_body = self.rest_client._parse_resp("""{
+ "overLimit": {
+ "message": ""
+ }
+ }""")
+ self.assertFalse(self.rest_client.is_absolute_limit(resp, resp_body))
+
+
+class TestProperties(BaseRestClientTestClass):
+
+ def setUp(self):
+ self.fake_http = fake_http.fake_httplib2()
+ super(TestProperties, self).setUp()
+ creds_dict = {
+ 'username': 'test-user',
+ 'user_id': 'test-user_id',
+ 'tenant_name': 'test-tenant_name',
+ 'tenant_id': 'test-tenant_id',
+ 'password': 'test-password'
+ }
+ self.rest_client = rest_client.RestClient(
+ fake_auth_provider.FakeAuthProvider(creds_dict=creds_dict),
+ None, None)
+
+ def test_properties(self):
+ self.assertEqual('test-user', self.rest_client.user)
+ self.assertEqual('test-user_id', self.rest_client.user_id)
+ self.assertEqual('test-tenant_name', self.rest_client.tenant_name)
+ self.assertEqual('test-tenant_id', self.rest_client.tenant_id)
+ self.assertEqual('test-password', self.rest_client.password)
+
+ self.rest_client.api_version = 'v1'
+ expected = {'api_version': 'v1',
+ 'endpoint_type': 'publicURL',
+ 'region': None,
+ 'name': None,
+ 'service': None,
+ 'skip_path': True}
+ self.rest_client.skip_path()
+ self.assertEqual(expected, self.rest_client.filters)
+
+ self.rest_client.reset_path()
+ self.rest_client.api_version = 'v1'
+ expected = {'api_version': 'v1',
+ 'endpoint_type': 'publicURL',
+ 'region': None,
+ 'name': None,
+ 'service': None}
+ self.assertEqual(expected, self.rest_client.filters)
+
+
+class TestExpectedSuccess(BaseRestClientTestClass):
+
+ def setUp(self):
+ self.fake_http = fake_http.fake_httplib2()
+ super(TestExpectedSuccess, self).setUp()
+
+ def test_expected_succes_int_match(self):
+ expected_code = 202
+ read_code = 202
+ resp = self.rest_client.expected_success(expected_code, read_code)
+ # Assert None resp on success
+ self.assertFalse(resp)
+
+ def test_expected_succes_int_no_match(self):
+ expected_code = 204
+ read_code = 202
+ self.assertRaises(exceptions.InvalidHttpSuccessCode,
+ self.rest_client.expected_success,
+ expected_code, read_code)
+
+ def test_expected_succes_list_match(self):
+ expected_code = [202, 204]
+ read_code = 202
+ resp = self.rest_client.expected_success(expected_code, read_code)
+ # Assert None resp on success
+ self.assertFalse(resp)
+
+ def test_expected_succes_list_no_match(self):
+ expected_code = [202, 204]
+ read_code = 200
+ self.assertRaises(exceptions.InvalidHttpSuccessCode,
+ self.rest_client.expected_success,
+ expected_code, read_code)
+
+ def test_non_success_expected_int(self):
+ expected_code = 404
+ read_code = 202
+ self.assertRaises(AssertionError, self.rest_client.expected_success,
+ expected_code, read_code)
+
+ def test_non_success_expected_list(self):
+ expected_code = [404, 202]
+ read_code = 202
+ self.assertRaises(AssertionError, self.rest_client.expected_success,
+ expected_code, read_code)
+
+ def test_non_success_read_code_as_string(self):
+ expected_code = 202
+ read_code = '202'
+ self.assertRaises(TypeError, self.rest_client.expected_success,
+ expected_code, read_code)
+
+ def test_non_success_read_code_as_list(self):
+ expected_code = 202
+ read_code = [202]
+ self.assertRaises(TypeError, self.rest_client.expected_success,
+ expected_code, read_code)
+
+ def test_non_success_expected_code_as_non_int(self):
+ expected_code = ['201', 202]
+ read_code = 202
+ self.assertRaises(AssertionError, self.rest_client.expected_success,
+ expected_code, read_code)
+
+
+class TestResponseBody(base.TestCase):
+
+ def test_str(self):
+ response = {'status': 200}
+ body = {'key1': 'value1'}
+ actual = rest_client.ResponseBody(response, body)
+ self.assertEqual("response: %s\nBody: %s" % (response, body),
+ str(actual))
+
+
+class TestResponseBodyData(base.TestCase):
+
+ def test_str(self):
+ response = {'status': 200}
+ data = 'data1'
+ actual = rest_client.ResponseBodyData(response, data)
+ self.assertEqual("response: %s\nBody: %s" % (response, data),
+ str(actual))
+
+
+class TestResponseBodyList(base.TestCase):
+
+ def test_str(self):
+ response = {'status': 200}
+ body = ['value1', 'value2', 'value3']
+ actual = rest_client.ResponseBodyList(response, body)
+ self.assertEqual("response: %s\nBody: %s" % (response, body),
+ str(actual))
+
+
+class TestJSONSchemaValidationBase(base.TestCase):
+
+ class Response(dict):
+
+ def __getattr__(self, attr):
+ return self[attr]
+
+ def __setattr__(self, attr, value):
+ self[attr] = value
+
+ def setUp(self):
+ super(TestJSONSchemaValidationBase, self).setUp()
+ self.fake_auth_provider = fake_auth_provider.FakeAuthProvider()
+ self.rest_client = rest_client.RestClient(
+ self.fake_auth_provider, None, None)
+
+ def _test_validate_pass(self, schema, resp_body, status=200):
+ resp = self.Response()
+ resp.status = status
+ self.rest_client.validate_response(schema, resp, resp_body)
+
+ def _test_validate_fail(self, schema, resp_body, status=200,
+ error_msg="HTTP response body is invalid"):
+ resp = self.Response()
+ resp.status = status
+ ex = self.assertRaises(exceptions.InvalidHTTPResponseBody,
+ self.rest_client.validate_response,
+ schema, resp, resp_body)
+ self.assertIn(error_msg, ex._error_string)
+
+
+class TestRestClientJSONSchemaValidation(TestJSONSchemaValidationBase):
+
+ schema = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'foo': {
+ 'type': 'integer',
+ },
+ },
+ 'required': ['foo']
+ }
+ }
+
+ def test_validate_pass_with_http_success_code(self):
+ body = {'foo': 12}
+ self._test_validate_pass(self.schema, body, status=200)
+
+ def test_validate_pass_with_http_redirect_code(self):
+ body = {'foo': 12}
+ schema = copy.deepcopy(self.schema)
+ schema['status_code'] = 300
+ self._test_validate_pass(schema, body, status=300)
+
+ def test_validate_not_http_success_code(self):
+ schema = {
+ 'status_code': [200]
+ }
+ body = {}
+ self._test_validate_pass(schema, body, status=400)
+
+ def test_validate_multiple_allowed_type(self):
+ schema = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'foo': {
+ 'type': ['integer', 'string'],
+ },
+ },
+ 'required': ['foo']
+ }
+ }
+ body = {'foo': 12}
+ self._test_validate_pass(schema, body)
+ body = {'foo': '12'}
+ self._test_validate_pass(schema, body)
+
+ def test_validate_enable_additional_property_pass(self):
+ schema = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'foo': {'type': 'integer'}
+ },
+ 'additionalProperties': True,
+ 'required': ['foo']
+ }
+ }
+ body = {'foo': 12, 'foo2': 'foo2value'}
+ self._test_validate_pass(schema, body)
+
+ def test_validate_disable_additional_property_pass(self):
+ schema = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'foo': {'type': 'integer'}
+ },
+ 'additionalProperties': False,
+ 'required': ['foo']
+ }
+ }
+ body = {'foo': 12}
+ self._test_validate_pass(schema, body)
+
+ def test_validate_disable_additional_property_fail(self):
+ schema = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'foo': {'type': 'integer'}
+ },
+ 'additionalProperties': False,
+ 'required': ['foo']
+ }
+ }
+ body = {'foo': 12, 'foo2': 'foo2value'}
+ self._test_validate_fail(schema, body)
+
+ def test_validate_wrong_status_code(self):
+ schema = {
+ 'status_code': [202]
+ }
+ body = {}
+ resp = self.Response()
+ resp.status = 200
+ ex = self.assertRaises(exceptions.InvalidHttpSuccessCode,
+ self.rest_client.validate_response,
+ schema, resp, body)
+ self.assertIn("Unexpected http success status code", ex._error_string)
+
+ def test_validate_wrong_attribute_type(self):
+ body = {'foo': 1.2}
+ self._test_validate_fail(self.schema, body)
+
+ def test_validate_unexpected_response_body(self):
+ schema = {
+ 'status_code': [200],
+ }
+ body = {'foo': 12}
+ self._test_validate_fail(
+ schema, body,
+ error_msg="HTTP response body should not exist")
+
+ def test_validate_missing_response_body(self):
+ body = {}
+ self._test_validate_fail(self.schema, body)
+
+ def test_validate_missing_required_attribute(self):
+ body = {'notfoo': 12}
+ self._test_validate_fail(self.schema, body)
+
+ def test_validate_response_body_not_list(self):
+ schema = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'list_items': {
+ 'type': 'array',
+ 'items': {'foo': {'type': 'integer'}}
+ }
+ },
+ 'required': ['list_items'],
+ }
+ }
+ body = {'foo': 12}
+ self._test_validate_fail(schema, body)
+
+ def test_validate_response_body_list_pass(self):
+ schema = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'list_items': {
+ 'type': 'array',
+ 'items': {'foo': {'type': 'integer'}}
+ }
+ },
+ 'required': ['list_items'],
+ }
+ }
+ body = {'list_items': [{'foo': 12}, {'foo': 10}]}
+ self._test_validate_pass(schema, body)
+
+
+class TestRestClientJSONHeaderSchemaValidation(TestJSONSchemaValidationBase):
+
+ schema = {
+ 'status_code': [200],
+ 'response_header': {
+ 'type': 'object',
+ 'properties': {
+ 'foo': {'type': 'integer'}
+ },
+ 'required': ['foo']
+ }
+ }
+
+ def test_validate_header_schema_pass(self):
+ resp_body = {}
+ resp = self.Response()
+ resp.status = 200
+ resp.foo = 12
+ self.rest_client.validate_response(self.schema, resp, resp_body)
+
+ def test_validate_header_schema_fail(self):
+ resp_body = {}
+ resp = self.Response()
+ resp.status = 200
+ resp.foo = 1.2
+ ex = self.assertRaises(exceptions.InvalidHTTPResponseHeader,
+ self.rest_client.validate_response,
+ self.schema, resp, resp_body)
+ self.assertIn("HTTP response header is invalid", ex._error_string)
+
+
+class TestRestClientJSONSchemaFormatValidation(TestJSONSchemaValidationBase):
+
+ schema = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'foo': {
+ 'type': 'string',
+ 'format': 'email'
+ }
+ },
+ 'required': ['foo']
+ }
+ }
+
+ def test_validate_format_pass(self):
+ body = {'foo': 'example@example.com'}
+ self._test_validate_pass(self.schema, body)
+
+ def test_validate_format_fail(self):
+ body = {'foo': 'wrong_email'}
+ self._test_validate_fail(self.schema, body)
+
+ def test_validate_formats_in_oneOf_pass(self):
+ schema = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'foo': {
+ 'type': 'string',
+ 'oneOf': [
+ {'format': 'ipv4'},
+ {'format': 'ipv6'}
+ ]
+ }
+ },
+ 'required': ['foo']
+ }
+ }
+ body = {'foo': '10.0.0.0'}
+ self._test_validate_pass(schema, body)
+ body = {'foo': 'FE80:0000:0000:0000:0202:B3FF:FE1E:8329'}
+ self._test_validate_pass(schema, body)
+
+ def test_validate_formats_in_oneOf_fail_both_match(self):
+ schema = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'foo': {
+ 'type': 'string',
+ 'oneOf': [
+ {'format': 'ipv4'},
+ {'format': 'ipv4'}
+ ]
+ }
+ },
+ 'required': ['foo']
+ }
+ }
+ body = {'foo': '10.0.0.0'}
+ self._test_validate_fail(schema, body)
+
+ def test_validate_formats_in_oneOf_fail_no_match(self):
+ schema = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'foo': {
+ 'type': 'string',
+ 'oneOf': [
+ {'format': 'ipv4'},
+ {'format': 'ipv6'}
+ ]
+ }
+ },
+ 'required': ['foo']
+ }
+ }
+ body = {'foo': 'wrong_ip_format'}
+ self._test_validate_fail(schema, body)
+
+ def test_validate_formats_in_anyOf_pass(self):
+ schema = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'foo': {
+ 'type': 'string',
+ 'anyOf': [
+ {'format': 'ipv4'},
+ {'format': 'ipv6'}
+ ]
+ }
+ },
+ 'required': ['foo']
+ }
+ }
+ body = {'foo': '10.0.0.0'}
+ self._test_validate_pass(schema, body)
+ body = {'foo': 'FE80:0000:0000:0000:0202:B3FF:FE1E:8329'}
+ self._test_validate_pass(schema, body)
+
+ def test_validate_formats_in_anyOf_pass_both_match(self):
+ schema = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'foo': {
+ 'type': 'string',
+ 'anyOf': [
+ {'format': 'ipv4'},
+ {'format': 'ipv4'}
+ ]
+ }
+ },
+ 'required': ['foo']
+ }
+ }
+ body = {'foo': '10.0.0.0'}
+ self._test_validate_pass(schema, body)
+
+ def test_validate_formats_in_anyOf_fail_no_match(self):
+ schema = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'foo': {
+ 'type': 'string',
+ 'anyOf': [
+ {'format': 'ipv4'},
+ {'format': 'ipv6'}
+ ]
+ }
+ },
+ 'required': ['foo']
+ }
+ }
+ body = {'foo': 'wrong_ip_format'}
+ self._test_validate_fail(schema, body)
+
+ def test_validate_formats_pass_for_unknow_format(self):
+ schema = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'foo': {
+ 'type': 'string',
+ 'format': 'UNKNOWN'
+ }
+ },
+ 'required': ['foo']
+ }
+ }
+ body = {'foo': 'example@example.com'}
+ self._test_validate_pass(schema, body)
+
+
+class TestRestClientJSONSchemaValidatorVersion(TestJSONSchemaValidationBase):
+
+ schema = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'foo': {'type': 'string'}
+ }
+ }
+ }
+
+ def test_current_json_schema_validator_version(self):
+ with fixtures.MockPatchObject(jsonschema.Draft4Validator,
+ "check_schema") as chk_schema:
+ body = {'foo': 'test'}
+ self._test_validate_pass(self.schema, body)
+ chk_schema.mock.assert_called_once_with(
+ self.schema['response_body'])
diff --git a/tempest/tests/lib/common/test_validation_resources.py b/tempest/tests/lib/common/test_validation_resources.py
new file mode 100644
index 0000000..d5139f4
--- /dev/null
+++ b/tempest/tests/lib/common/test_validation_resources.py
@@ -0,0 +1,344 @@
+# Copyright (c) 2017 IBM Corp.
+# 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 fixtures
+import mock
+import testtools
+
+from tempest.lib.common import validation_resources as vr
+from tempest.lib import exceptions as lib_exc
+from tempest.lib.services import clients
+from tempest.tests import base
+from tempest.tests.lib import fake_credentials
+from tempest.tests.lib.services import registry_fixture
+
+FAKE_SECURITY_GROUP = {'security_group': {'id': 'sg_id'}}
+FAKE_KEYPAIR = {'keypair': {'name': 'keypair_name'}}
+FAKE_FIP_NOVA_NET = {'floating_ip': {'ip': '1.2.3.4', 'id': '1234'}}
+FAKE_FIP_NEUTRON = {'floatingip': {'floating_ip_address': '1.2.3.4',
+ 'id': '1234'}}
+
+SERVICES = 'tempest.lib.services'
+SG_CLIENT = (SERVICES + '.%s.security_groups_client.SecurityGroupsClient.%s')
+SGR_CLIENT = (SERVICES + '.%s.security_group_rules_client.'
+ 'SecurityGroupRulesClient.create_security_group_rule')
+KP_CLIENT = (SERVICES + '.compute.keypairs_client.KeyPairsClient.%s')
+FIP_CLIENT = (SERVICES + '.%s.floating_ips_client.FloatingIPsClient.%s')
+
+
+class TestValidationResources(base.TestCase):
+
+ def setUp(self):
+ super(TestValidationResources, self).setUp()
+ self.useFixture(registry_fixture.RegistryFixture())
+ self.mock_sg_compute = self.useFixture(fixtures.MockPatch(
+ SG_CLIENT % ('compute', 'create_security_group'), autospec=True,
+ return_value=FAKE_SECURITY_GROUP))
+ self.mock_sg_network = self.useFixture(fixtures.MockPatch(
+ SG_CLIENT % ('network', 'create_security_group'), autospec=True,
+ return_value=FAKE_SECURITY_GROUP))
+ self.mock_sgr_compute = self.useFixture(fixtures.MockPatch(
+ SGR_CLIENT % 'compute', autospec=True))
+ self.mock_sgr_network = self.useFixture(fixtures.MockPatch(
+ SGR_CLIENT % 'network', autospec=True))
+ self.mock_kp = self.useFixture(fixtures.MockPatch(
+ KP_CLIENT % 'create_keypair', autospec=True,
+ return_value=FAKE_KEYPAIR))
+ self.mock_fip_compute = self.useFixture(fixtures.MockPatch(
+ FIP_CLIENT % ('compute', 'create_floating_ip'), autospec=True,
+ return_value=FAKE_FIP_NOVA_NET))
+ self.mock_fip_network = self.useFixture(fixtures.MockPatch(
+ FIP_CLIENT % ('network', 'create_floatingip'), autospec=True,
+ return_value=FAKE_FIP_NEUTRON))
+ self.os = clients.ServiceClients(
+ fake_credentials.FakeKeystoneV3Credentials(), 'fake_uri')
+
+ def test_create_ssh_security_group_nova_net(self):
+ expected_sg_id = FAKE_SECURITY_GROUP['security_group']['id']
+ sg = vr.create_ssh_security_group(self.os, add_rule=True,
+ use_neutron=False)
+ self.assertEqual(FAKE_SECURITY_GROUP['security_group'], sg)
+ # Neutron clients have not been used
+ self.assertEqual(self.mock_sg_network.mock.call_count, 0)
+ self.assertEqual(self.mock_sgr_network.mock.call_count, 0)
+ # Nova-net clients assertions
+ self.assertGreater(self.mock_sg_compute.mock.call_count, 0)
+ self.assertGreater(self.mock_sgr_compute.mock.call_count, 0)
+ for call in self.mock_sgr_compute.mock.call_args_list[1:]:
+ self.assertIn(expected_sg_id, call[1].values())
+
+ def test_create_ssh_security_group_neutron(self):
+ expected_sg_id = FAKE_SECURITY_GROUP['security_group']['id']
+ expected_ethertype = 'fake_ethertype'
+ sg = vr.create_ssh_security_group(self.os, add_rule=True,
+ use_neutron=True,
+ ethertype=expected_ethertype)
+ self.assertEqual(FAKE_SECURITY_GROUP['security_group'], sg)
+ # Nova-net clients have not been used
+ self.assertEqual(self.mock_sg_compute.mock.call_count, 0)
+ self.assertEqual(self.mock_sgr_compute.mock.call_count, 0)
+ # Nova-net clients assertions
+ self.assertGreater(self.mock_sg_network.mock.call_count, 0)
+ self.assertGreater(self.mock_sgr_network.mock.call_count, 0)
+ # Check SG ID and ethertype are passed down to rules
+ for call in self.mock_sgr_network.mock.call_args_list[1:]:
+ self.assertIn(expected_sg_id, call[1].values())
+ self.assertIn(expected_ethertype, call[1].values())
+
+ def test_create_ssh_security_no_rules(self):
+ sg = vr.create_ssh_security_group(self.os, add_rule=False)
+ self.assertEqual(FAKE_SECURITY_GROUP['security_group'], sg)
+ # SG Rules clients have not been used
+ self.assertEqual(self.mock_sgr_compute.mock.call_count, 0)
+ self.assertEqual(self.mock_sgr_network.mock.call_count, 0)
+
+ @mock.patch.object(vr, 'create_ssh_security_group',
+ return_value=FAKE_SECURITY_GROUP['security_group'])
+ def test_create_validation_resources_nova_net(self, mock_create_sg):
+ expected_floating_network_id = 'my_fni'
+ expected_floating_network_name = 'my_fnn'
+ resources = vr.create_validation_resources(
+ self.os, keypair=True, floating_ip=True, security_group=True,
+ security_group_rules=True, ethertype='IPv6', use_neutron=False,
+ floating_network_id=expected_floating_network_id,
+ floating_network_name=expected_floating_network_name)
+ # Keypair calls
+ self.assertGreater(self.mock_kp.mock.call_count, 0)
+ # Floating IP calls
+ self.assertGreater(self.mock_fip_compute.mock.call_count, 0)
+ for call in self.mock_fip_compute.mock.call_args_list[1:]:
+ self.assertIn(expected_floating_network_name, call[1].values())
+ self.assertNotIn(expected_floating_network_id, call[1].values())
+ self.assertEqual(self.mock_fip_network.mock.call_count, 0)
+ # SG calls
+ mock_create_sg.assert_called_once()
+ # Resources
+ for resource in ['keypair', 'floating_ip', 'security_group']:
+ self.assertIn(resource, resources)
+ self.assertEqual(FAKE_KEYPAIR['keypair'], resources['keypair'])
+ self.assertEqual(FAKE_SECURITY_GROUP['security_group'],
+ resources['security_group'])
+ self.assertEqual(FAKE_FIP_NOVA_NET['floating_ip'],
+ resources['floating_ip'])
+
+ @mock.patch.object(vr, 'create_ssh_security_group',
+ return_value=FAKE_SECURITY_GROUP['security_group'])
+ def test_create_validation_resources_neutron(self, mock_create_sg):
+ expected_floating_network_id = 'my_fni'
+ expected_floating_network_name = 'my_fnn'
+ resources = vr.create_validation_resources(
+ self.os, keypair=True, floating_ip=True, security_group=True,
+ security_group_rules=True, ethertype='IPv6', use_neutron=True,
+ floating_network_id=expected_floating_network_id,
+ floating_network_name=expected_floating_network_name)
+ # Keypair calls
+ self.assertGreater(self.mock_kp.mock.call_count, 0)
+ # Floating IP calls
+ self.assertEqual(self.mock_fip_compute.mock.call_count, 0)
+ self.assertGreater(self.mock_fip_network.mock.call_count, 0)
+ for call in self.mock_fip_compute.mock.call_args_list[1:]:
+ self.assertIn(expected_floating_network_id, call[1].values())
+ self.assertNotIn(expected_floating_network_name, call[1].values())
+ # SG calls
+ mock_create_sg.assert_called_once()
+ # Resources
+ for resource in ['keypair', 'floating_ip', 'security_group']:
+ self.assertIn(resource, resources)
+ self.assertEqual(FAKE_KEYPAIR['keypair'], resources['keypair'])
+ self.assertEqual(FAKE_SECURITY_GROUP['security_group'],
+ resources['security_group'])
+ self.assertIn('ip', resources['floating_ip'])
+ self.assertEqual(resources['floating_ip']['ip'],
+ FAKE_FIP_NEUTRON['floatingip']['floating_ip_address'])
+ self.assertEqual(resources['floating_ip']['id'],
+ FAKE_FIP_NEUTRON['floatingip']['id'])
+
+
+class TestClearValidationResourcesFixture(base.TestCase):
+
+ def setUp(self):
+ super(TestClearValidationResourcesFixture, self).setUp()
+ self.useFixture(registry_fixture.RegistryFixture())
+ self.mock_sg_compute = self.useFixture(fixtures.MockPatch(
+ SG_CLIENT % ('compute', 'delete_security_group'), autospec=True))
+ self.mock_sg_network = self.useFixture(fixtures.MockPatch(
+ SG_CLIENT % ('network', 'delete_security_group'), autospec=True))
+ self.mock_sg_wait_compute = self.useFixture(fixtures.MockPatch(
+ SG_CLIENT % ('compute', 'wait_for_resource_deletion'),
+ autospec=True))
+ self.mock_sg_wait_network = self.useFixture(fixtures.MockPatch(
+ SG_CLIENT % ('network', 'wait_for_resource_deletion'),
+ autospec=True))
+ self.mock_kp = self.useFixture(fixtures.MockPatch(
+ KP_CLIENT % 'delete_keypair', autospec=True))
+ self.mock_fip_compute = self.useFixture(fixtures.MockPatch(
+ FIP_CLIENT % ('compute', 'delete_floating_ip'), autospec=True))
+ self.mock_fip_network = self.useFixture(fixtures.MockPatch(
+ FIP_CLIENT % ('network', 'delete_floatingip'), autospec=True))
+ self.os = clients.ServiceClients(
+ fake_credentials.FakeKeystoneV3Credentials(), 'fake_uri')
+
+ def test_clear_validation_resources_nova_net(self):
+ vr.clear_validation_resources(
+ self.os,
+ floating_ip=FAKE_FIP_NOVA_NET['floating_ip'],
+ security_group=FAKE_SECURITY_GROUP['security_group'],
+ keypair=FAKE_KEYPAIR['keypair'],
+ use_neutron=False)
+ self.assertGreater(self.mock_kp.mock.call_count, 0)
+ for call in self.mock_kp.mock.call_args_list[1:]:
+ self.assertIn(FAKE_KEYPAIR['keypair']['name'], call[1].values())
+ self.assertGreater(self.mock_sg_compute.mock.call_count, 0)
+ for call in self.mock_sg_compute.mock.call_args_list[1:]:
+ self.assertIn(FAKE_SECURITY_GROUP['security_group']['id'],
+ call[1].values())
+ self.assertGreater(self.mock_sg_wait_compute.mock.call_count, 0)
+ for call in self.mock_sg_wait_compute.mock.call_args_list[1:]:
+ self.assertIn(FAKE_SECURITY_GROUP['security_group']['id'],
+ call[1].values())
+ self.assertEqual(self.mock_sg_network.mock.call_count, 0)
+ self.assertEqual(self.mock_sg_wait_network.mock.call_count, 0)
+ self.assertGreater(self.mock_fip_compute.mock.call_count, 0)
+ for call in self.mock_fip_compute.mock.call_args_list[1:]:
+ self.assertIn(FAKE_FIP_NOVA_NET['floating_ip']['id'],
+ call[1].values())
+ self.assertEqual(self.mock_fip_network.mock.call_count, 0)
+
+ def test_clear_validation_resources_neutron(self):
+ vr.clear_validation_resources(
+ self.os,
+ floating_ip=FAKE_FIP_NEUTRON['floatingip'],
+ security_group=FAKE_SECURITY_GROUP['security_group'],
+ keypair=FAKE_KEYPAIR['keypair'],
+ use_neutron=True)
+ self.assertGreater(self.mock_kp.mock.call_count, 0)
+ for call in self.mock_kp.mock.call_args_list[1:]:
+ self.assertIn(FAKE_KEYPAIR['keypair']['name'], call[1].values())
+ self.assertGreater(self.mock_sg_network.mock.call_count, 0)
+ for call in self.mock_sg_network.mock.call_args_list[1:]:
+ self.assertIn(FAKE_SECURITY_GROUP['security_group']['id'],
+ call[1].values())
+ self.assertGreater(self.mock_sg_wait_network.mock.call_count, 0)
+ for call in self.mock_sg_wait_network.mock.call_args_list[1:]:
+ self.assertIn(FAKE_SECURITY_GROUP['security_group']['id'],
+ call[1].values())
+ self.assertEqual(self.mock_sg_compute.mock.call_count, 0)
+ self.assertEqual(self.mock_sg_wait_compute.mock.call_count, 0)
+ self.assertGreater(self.mock_fip_network.mock.call_count, 0)
+ for call in self.mock_fip_network.mock.call_args_list[1:]:
+ self.assertIn(FAKE_FIP_NEUTRON['floatingip']['id'],
+ call[1].values())
+ self.assertEqual(self.mock_fip_compute.mock.call_count, 0)
+
+ def test_clear_validation_resources_exceptions(self):
+ # Test that even with exceptions all cleanups are invoked and that only
+ # the first exception is reported.
+ # NOTE(andreaf) There's not way of knowing which exception is going to
+ # be raised first unless we enforce which resource is cleared first,
+ # which is not really interesting, but also not harmful. keypair first.
+ self.mock_kp.mock.side_effect = Exception('keypair exception')
+ self.mock_sg_network.mock.side_effect = Exception('sg exception')
+ self.mock_fip_network.mock.side_effect = Exception('fip exception')
+ with testtools.ExpectedException(Exception, value_re='keypair'):
+ vr.clear_validation_resources(
+ self.os,
+ floating_ip=FAKE_FIP_NEUTRON['floatingip'],
+ security_group=FAKE_SECURITY_GROUP['security_group'],
+ keypair=FAKE_KEYPAIR['keypair'],
+ use_neutron=True)
+ # Clients calls are still made, but not the wait call
+ self.assertGreater(self.mock_kp.mock.call_count, 0)
+ self.assertGreater(self.mock_sg_network.mock.call_count, 0)
+ self.assertGreater(self.mock_fip_network.mock.call_count, 0)
+
+ def test_clear_validation_resources_wait_not_found_wait(self):
+ # Test that a not found on wait is not an exception
+ self.mock_sg_wait_network.mock.side_effect = lib_exc.NotFound('yay')
+ vr.clear_validation_resources(
+ self.os,
+ floating_ip=FAKE_FIP_NEUTRON['floatingip'],
+ security_group=FAKE_SECURITY_GROUP['security_group'],
+ keypair=FAKE_KEYPAIR['keypair'],
+ use_neutron=True)
+ # Clients calls are still made, but not the wait call
+ self.assertGreater(self.mock_kp.mock.call_count, 0)
+ self.assertGreater(self.mock_sg_network.mock.call_count, 0)
+ self.assertGreater(self.mock_sg_wait_network.mock.call_count, 0)
+ self.assertGreater(self.mock_fip_network.mock.call_count, 0)
+
+ def test_clear_validation_resources_wait_not_found_delete(self):
+ # Test that a not found on delete is not an exception
+ self.mock_kp.mock.side_effect = lib_exc.NotFound('yay')
+ self.mock_sg_network.mock.side_effect = lib_exc.NotFound('yay')
+ self.mock_fip_network.mock.side_effect = lib_exc.NotFound('yay')
+ vr.clear_validation_resources(
+ self.os,
+ floating_ip=FAKE_FIP_NEUTRON['floatingip'],
+ security_group=FAKE_SECURITY_GROUP['security_group'],
+ keypair=FAKE_KEYPAIR['keypair'],
+ use_neutron=True)
+ # Clients calls are still made, but not the wait call
+ self.assertGreater(self.mock_kp.mock.call_count, 0)
+ self.assertGreater(self.mock_sg_network.mock.call_count, 0)
+ self.assertEqual(self.mock_sg_wait_network.mock.call_count, 0)
+ self.assertGreater(self.mock_fip_network.mock.call_count, 0)
+
+
+class TestValidationResourcesFixture(base.TestCase):
+
+ @mock.patch.object(vr, 'create_validation_resources', autospec=True)
+ def test_use_fixture(self, mock_vr):
+ exp_vr = dict(keypair='keypair',
+ floating_ip='floating_ip',
+ security_group='security_group')
+ mock_vr.return_value = exp_vr
+ exp_clients = 'clients'
+ exp_parameters = dict(keypair=True, floating_ip=True,
+ security_group=True, security_group_rules=True,
+ ethertype='v6', use_neutron=True,
+ floating_network_id='fnid',
+ floating_network_name='fnname')
+ # First mock cleanup
+ self.useFixture(fixtures.MockPatchObject(
+ vr, 'clear_validation_resources', autospec=True))
+ # And then use vr fixture, so when the fixture is cleaned-up, the mock
+ # is still there
+ vr_fixture = self.useFixture(vr.ValidationResourcesFixture(
+ exp_clients, **exp_parameters))
+ # Assert vr have been provisioned
+ mock_vr.assert_called_once_with(exp_clients, **exp_parameters)
+ # Assert vr have been setup in the fixture
+ self.assertEqual(exp_vr, vr_fixture.resources)
+
+ @mock.patch.object(vr, 'clear_validation_resources', autospec=True)
+ @mock.patch.object(vr, 'create_validation_resources', autospec=True)
+ def test_use_fixture_context(self, mock_vr, mock_clear):
+ exp_vr = dict(keypair='keypair',
+ floating_ip='floating_ip',
+ security_group='security_group')
+ mock_vr.return_value = exp_vr
+ exp_clients = 'clients'
+ exp_parameters = dict(keypair=True, floating_ip=True,
+ security_group=True, security_group_rules=True,
+ ethertype='v6', use_neutron=True,
+ floating_network_id='fnid',
+ floating_network_name='fnname')
+ with vr.ValidationResourcesFixture(exp_clients,
+ **exp_parameters) as vr_fixture:
+ # Assert vr have been provisioned
+ mock_vr.assert_called_once_with(exp_clients, **exp_parameters)
+ # Assert vr have been setup in the fixture
+ self.assertEqual(exp_vr, vr_fixture.resources)
+ # After context manager is closed, clear is invoked
+ exp_vr['use_neutron'] = exp_parameters['use_neutron']
+ mock_clear.assert_called_once_with(exp_clients, **exp_vr)
diff --git a/tempest/tests/negative/__init__.py b/tempest/tests/lib/common/utils/__init__.py
similarity index 100%
copy from tempest/tests/negative/__init__.py
copy to tempest/tests/lib/common/utils/__init__.py
diff --git a/tempest/tests/negative/__init__.py b/tempest/tests/lib/common/utils/linux/__init__.py
similarity index 100%
copy from tempest/tests/negative/__init__.py
copy to tempest/tests/lib/common/utils/linux/__init__.py
diff --git a/tempest/tests/lib/common/utils/linux/test_remote_client.py b/tempest/tests/lib/common/utils/linux/test_remote_client.py
new file mode 100644
index 0000000..7a21a5f
--- /dev/null
+++ b/tempest/tests/lib/common/utils/linux/test_remote_client.py
@@ -0,0 +1,67 @@
+# Copyright 2017 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.
+
+import mock
+
+from tempest.lib.common import ssh
+from tempest.lib.common.utils.linux import remote_client
+from tempest.lib import exceptions as lib_exc
+from tempest.tests import base
+
+
+class FakeServersClient(object):
+
+ def get_console_output(self, server_id):
+ return {"output": "fake_output"}
+
+
+class TestRemoteClient(base.TestCase):
+
+ @mock.patch.object(ssh.Client, 'exec_command', return_value='success')
+ def test_exec_command(self, mock_ssh_exec_command):
+ client = remote_client.RemoteClient('192.168.1.10', 'username')
+ client.exec_command('ls')
+ mock_ssh_exec_command.assert_called_once_with(
+ 'set -eu -o pipefail; PATH=$PATH:/sbin; ls')
+
+ @mock.patch.object(ssh.Client, 'test_connection_auth')
+ def test_validate_authentication(self, mock_test_connection_auth):
+ client = remote_client.RemoteClient('192.168.1.10', 'username')
+ client.validate_authentication()
+ mock_test_connection_auth.assert_called_once_with()
+
+ @mock.patch.object(remote_client.LOG, 'debug')
+ @mock.patch.object(ssh.Client, 'exec_command')
+ def test_debug_ssh_without_console(self, mock_exec_command, mock_debug):
+ mock_exec_command.side_effect = lib_exc.SSHTimeout
+ server = {'id': 'fake_id'}
+ client = remote_client.RemoteClient('192.168.1.10', 'username',
+ server=server)
+ self.assertRaises(lib_exc.SSHTimeout, client.exec_command, 'ls')
+ mock_debug.assert_called_with(
+ 'Caller: %s. Timeout trying to ssh to server %s',
+ 'TestRemoteClient:test_debug_ssh_without_console', server)
+
+ @mock.patch.object(remote_client.LOG, 'debug')
+ @mock.patch.object(ssh.Client, 'exec_command')
+ def test_debug_ssh_with_console(self, mock_exec_command, mock_debug):
+ mock_exec_command.side_effect = lib_exc.SSHTimeout
+ server = {'id': 'fake_id'}
+ client = remote_client.RemoteClient('192.168.1.10', 'username',
+ server=server,
+ servers_client=FakeServersClient())
+ self.assertRaises(lib_exc.SSHTimeout, client.exec_command, 'ls')
+ mock_debug.assert_called_with(
+ 'Console log for server %s: %s', server['id'], 'fake_output')
diff --git a/tempest/tests/lib/common/utils/test_data_utils.py b/tempest/tests/lib/common/utils/test_data_utils.py
new file mode 100644
index 0000000..b8385b2
--- /dev/null
+++ b/tempest/tests/lib/common/utils/test_data_utils.py
@@ -0,0 +1,147 @@
+# Copyright 2014 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.common.utils import data_utils
+from tempest.tests import base
+
+
+class TestDataUtils(base.TestCase):
+
+ def test_rand_uuid(self):
+ actual = data_utils.rand_uuid()
+ self.assertIsInstance(actual, str)
+ self.assertRegex(actual, "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]"
+ "{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
+ actual2 = data_utils.rand_uuid()
+ self.assertNotEqual(actual, actual2)
+
+ def test_rand_uuid_hex(self):
+ actual = data_utils.rand_uuid_hex()
+ self.assertIsInstance(actual, str)
+ self.assertRegex(actual, "^[0-9a-f]{32}$")
+
+ actual2 = data_utils.rand_uuid_hex()
+ self.assertNotEqual(actual, actual2)
+
+ def test_rand_name_with_default_prefix(self):
+ actual = data_utils.rand_name('foo')
+ self.assertIsInstance(actual, str)
+ self.assertTrue(actual.startswith('tempest-foo'))
+ actual2 = data_utils.rand_name('foo')
+ self.assertTrue(actual2.startswith('tempest-foo'))
+ self.assertNotEqual(actual, actual2)
+
+ def test_rand_name_with_none_prefix(self):
+ actual = data_utils.rand_name('foo', prefix=None)
+ self.assertIsInstance(actual, str)
+ self.assertTrue(actual.startswith('foo'))
+ actual2 = data_utils.rand_name('foo', prefix=None)
+ self.assertTrue(actual2.startswith('foo'))
+ self.assertNotEqual(actual, actual2)
+
+ def test_rand_name_with_prefix(self):
+ actual = data_utils.rand_name(prefix='prefix-str')
+ self.assertIsInstance(actual, str)
+ self.assertRegex(actual, "^prefix-str-")
+ actual2 = data_utils.rand_name(prefix='prefix-str')
+ self.assertNotEqual(actual, actual2)
+
+ def test_rand_password(self):
+ actual = data_utils.rand_password()
+ self.assertIsInstance(actual, str)
+ self.assertRegex(actual, "[A-Za-z0-9~!@#%^&*_=+]{15,}")
+ actual2 = data_utils.rand_password()
+ self.assertNotEqual(actual, actual2)
+
+ def test_rand_password_with_len(self):
+ actual = data_utils.rand_password(8)
+ self.assertIsInstance(actual, str)
+ self.assertEqual(len(actual), 8)
+ self.assertRegex(actual, "[A-Za-z0-9~!@#%^&*_=+]{8}")
+ actual2 = data_utils.rand_password(8)
+ self.assertNotEqual(actual, actual2)
+
+ def test_rand_password_with_len_2(self):
+ actual = data_utils.rand_password(2)
+ self.assertIsInstance(actual, str)
+ self.assertEqual(len(actual), 3)
+ self.assertRegex(actual, "[A-Za-z0-9~!@#%^&*_=+]{3}")
+ actual2 = data_utils.rand_password(2)
+ # NOTE(masayukig): Originally, we checked that the acutal and actual2
+ # are different each other. But only 3 letters can be the same value
+ # in a very rare case. So, we just check the length here, too,
+ # just in case.
+ self.assertEqual(len(actual2), 3)
+
+ def test_rand_url(self):
+ actual = data_utils.rand_url()
+ self.assertIsInstance(actual, str)
+ self.assertRegex(actual, "^https://url-[0-9]*\.com$")
+ actual2 = data_utils.rand_url()
+ self.assertNotEqual(actual, actual2)
+
+ def test_rand_int(self):
+ actual = data_utils.rand_int_id()
+ self.assertIsInstance(actual, int)
+
+ actual2 = data_utils.rand_int_id()
+ self.assertNotEqual(actual, actual2)
+
+ def test_rand_infiniband_guid_address(self):
+ actual = data_utils.rand_infiniband_guid_address()
+ self.assertIsInstance(actual, str)
+ self.assertRegex(actual, "^([0-9a-f][0-9a-f]:){7}"
+ "[0-9a-f][0-9a-f]$")
+
+ actual2 = data_utils.rand_infiniband_guid_address()
+ self.assertNotEqual(actual, actual2)
+
+ def test_rand_mac_address(self):
+ actual = data_utils.rand_mac_address()
+ self.assertIsInstance(actual, str)
+ self.assertRegex(actual, "^([0-9a-f][0-9a-f]:){5}"
+ "[0-9a-f][0-9a-f]$")
+
+ actual2 = data_utils.rand_mac_address()
+ self.assertNotEqual(actual, actual2)
+
+ def test_parse_image_id(self):
+ actual = data_utils.parse_image_id("/foo/bar/deadbeaf")
+ self.assertEqual("deadbeaf", actual)
+
+ def test_arbitrary_string(self):
+ actual = data_utils.arbitrary_string()
+ self.assertEqual(actual, "test")
+ actual = data_utils.arbitrary_string(size=30, base_text="abc")
+ self.assertEqual(actual, "abc" * int(30 / len("abc")))
+ actual = data_utils.arbitrary_string(size=5, base_text="deadbeaf")
+ self.assertEqual(actual, "deadb")
+
+ def test_random_bytes(self):
+ actual = data_utils.random_bytes() # default size=1024
+ self.assertIsInstance(actual, bytes)
+ self.assertEqual(1024, len(actual))
+ actual2 = data_utils.random_bytes()
+ self.assertNotEqual(actual, actual2)
+
+ actual = data_utils.random_bytes(size=2048)
+ self.assertEqual(2048, len(actual))
+
+ def test_chunkify(self):
+ data = "aaa"
+ chunks = data_utils.chunkify(data, 2)
+ self.assertEqual("aa", next(chunks))
+ self.assertEqual("a", next(chunks))
+ self.assertRaises(StopIteration, next, chunks)
diff --git a/tempest/tests/lib/common/utils/test_misc.py b/tempest/tests/lib/common/utils/test_misc.py
new file mode 100644
index 0000000..47e81d1
--- /dev/null
+++ b/tempest/tests/lib/common/utils/test_misc.py
@@ -0,0 +1,52 @@
+# Copyright 2014 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.common.utils import misc
+from tempest.tests import base
+
+
+@misc.singleton
+class TestFoo(object):
+
+ count = 0
+
+ def increment(self):
+ self.count += 1
+ return self.count
+
+
+@misc.singleton
+class TestBar(object):
+
+ count = 0
+
+ def increment(self):
+ self.count += 1
+ return self.count
+
+
+class TestMisc(base.TestCase):
+
+ def test_singleton(self):
+ test = TestFoo()
+ self.assertEqual(0, test.count)
+ self.assertEqual(1, test.increment())
+ test2 = TestFoo()
+ self.assertEqual(1, test.count)
+ self.assertEqual(1, test2.count)
+ self.assertEqual(test, test2)
+ test3 = TestBar()
+ self.assertNotEqual(test, test3)
diff --git a/tempest/tests/lib/common/utils/test_test_utils.py b/tempest/tests/lib/common/utils/test_test_utils.py
new file mode 100644
index 0000000..865767b
--- /dev/null
+++ b/tempest/tests/lib/common/utils/test_test_utils.py
@@ -0,0 +1,206 @@
+# Copyright 2016 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import time
+
+import mock
+
+from tempest.lib.common import thread
+from tempest.lib.common.utils import test_utils
+from tempest.lib import exceptions
+from tempest.tests import base
+
+
+class TestTestUtils(base.TestCase):
+
+ def test_find_test_caller_test_case(self):
+ # Calling it from here should give us the method we're in.
+ self.assertEqual('TestTestUtils:test_find_test_caller_test_case',
+ test_utils.find_test_caller())
+
+ def test_find_test_caller_setup_self(self):
+ def setUp(self):
+ return test_utils.find_test_caller()
+ self.assertEqual('TestTestUtils:setUp', setUp(self))
+
+ def test_find_test_caller_setup_no_self(self):
+ def setUp():
+ return test_utils.find_test_caller()
+ self.assertEqual(':setUp', setUp())
+
+ def test_find_test_caller_setupclass_cls(self):
+ def setUpClass(cls): # noqa
+ return test_utils.find_test_caller()
+ self.assertEqual('TestTestUtils:setUpClass',
+ setUpClass(self.__class__))
+
+ def test_find_test_caller_teardown_self(self):
+ def tearDown(self):
+ return test_utils.find_test_caller()
+ self.assertEqual('TestTestUtils:tearDown', tearDown(self))
+
+ def test_find_test_caller_teardown_no_self(self):
+ def tearDown():
+ return test_utils.find_test_caller()
+ self.assertEqual(':tearDown', tearDown())
+
+ def test_find_test_caller_teardown_class(self):
+ def tearDownClass(cls): # noqa
+ return test_utils.find_test_caller()
+ self.assertEqual('TestTestUtils:tearDownClass',
+ tearDownClass(self.__class__))
+
+ def test_call_and_ignore_notfound_exc_when_notfound_raised(self):
+ def raise_not_found():
+ raise exceptions.NotFound()
+ self.assertIsNone(
+ test_utils.call_and_ignore_notfound_exc(raise_not_found))
+
+ def test_call_and_ignore_notfound_exc_when_value_error_raised(self):
+ def raise_value_error():
+ raise ValueError()
+ self.assertRaises(ValueError, test_utils.call_and_ignore_notfound_exc,
+ raise_value_error)
+
+ def test_call_and_ignore_notfound_exc(self):
+ m = mock.Mock(return_value=42)
+ args, kwargs = (1,), {'1': None}
+ self.assertEqual(
+ 42, test_utils.call_and_ignore_notfound_exc(m, *args, **kwargs))
+ m.assert_called_once_with(*args, **kwargs)
+
+
+class TestCallUntilTrue(base.TestCase):
+
+ def test_call_until_true_when_true_at_first_call(self):
+ """func returns True at first call
+
+ """
+ self._test_call_until_true(return_values=[True],
+ duration=30.,
+ time_sequence=[10., 60.])
+
+ def test_call_until_true_when_true_before_timeout(self):
+ """func returns false at first call, then True before timeout
+
+ """
+ self._test_call_until_true(return_values=[False, True],
+ duration=30.,
+ time_sequence=[10., 39., 41.])
+
+ def test_call_until_true_when_never_true_before_timeout(self):
+ """func returns false, then false, just before timeout
+
+ """
+ self._test_call_until_true(return_values=[False, False],
+ duration=30.,
+ time_sequence=[10., 39., 41.])
+
+ def test_call_until_true_with_params(self):
+ """func is called using given parameters
+
+ """
+ self._test_call_until_true(return_values=[False, True],
+ duration=30.,
+ time_sequence=[10., 30., 60.],
+ args=(1, 2),
+ kwargs=dict(foo='bar', bar='foo'))
+
+ def _test_call_until_true(self, return_values, duration, time_sequence,
+ args=None, kwargs=None):
+ """Test call_until_true function
+
+ :param return_values: list of booleans values to be returned
+ each time given function is called. If any of these values
+ is not consumed by calling the function the test fails.
+ The list must contain a sequence of False items terminated
+ by a single True or False
+ :param duration: parameter passed to call_until_true function
+ (a floating point value).
+ :param time_sequence: sequence of time values returned by
+ mocked time.time function used to trigger call_until_true
+ behavior when handling timeout condition. The sequence must
+ contain the exact number of values expected to be consumed by
+ each time call_until_true calls time.time function.
+ :param args: sequence of positional arguments to be passed
+ to call_until_true function.
+ :param kwargs: sequence of named arguments to be passed
+ to call_until_true function.
+ """
+
+ # all values except the last are False
+ self.assertEqual([False] * len(return_values[:-1]), return_values[:-1])
+ # last value can be True or False
+ self.assertIn(return_values[-1], [True, False])
+
+ # GIVEN
+ func = mock.Mock(side_effect=return_values)
+ sleep = 10. # this value has no effect as time.sleep is being mocked
+ sleep_func = self.patch('time.sleep')
+ time_func = self._patch_time(time_sequence)
+ args = args or tuple()
+ kwargs = kwargs or dict()
+
+ # WHEN
+ result = test_utils.call_until_true(func, duration, sleep,
+ *args, **kwargs)
+ # THEN
+
+ # It must return last returned value
+ self.assertIs(return_values[-1], result)
+
+ self._test_func_calls(func, return_values, *args, **kwargs)
+ self._test_sleep_calls(sleep_func, return_values, sleep)
+ # The number of times time.time is called is not relevant as a
+ # requirement of call_until_true. What is instead relevant is that
+ # call_until_true use a mocked function to make the test reliable
+ # and the test actually provide the right sequence of numbers to
+ # reproduce the behavior has to be tested
+ self._assert_called_n_times(time_func, len(time_sequence))
+
+ def _patch_time(self, time_sequence):
+ # Iterator over time sequence
+ time_iterator = iter(time_sequence)
+ # Preserve original time.time() behavior for other threads
+ original_time = time.time
+ thread_id = thread.get_ident()
+
+ def mocked_time():
+ if thread.get_ident() == thread_id:
+ # Test thread => return time sequence values
+ return next(time_iterator)
+ else:
+ # Other threads => call original time function
+ return original_time()
+
+ return self.patch('time.time', side_effect=mocked_time)
+
+ def _test_func_calls(self, func, return_values, *args, **kwargs):
+ self._assert_called_n_times(func, len(return_values), *args, **kwargs)
+
+ def _test_sleep_calls(self, sleep_func, return_values, sleep):
+ # count first consecutive False
+ expected_count = 0
+ for value in return_values:
+ if value:
+ break
+ expected_count += 1
+ self._assert_called_n_times(sleep_func, expected_count, sleep)
+
+ def _assert_called_n_times(self, mock_func, expected_count, *args,
+ **kwargs):
+ calls = [mock.call(*args, **kwargs)] * expected_count
+ self.assertEqual(expected_count, mock_func.call_count)
+ mock_func.assert_has_calls(calls)
diff --git a/tempest/tests/lib/fake_auth_provider.py b/tempest/tests/lib/fake_auth_provider.py
new file mode 100644
index 0000000..e4582f8
--- /dev/null
+++ b/tempest/tests/lib/fake_auth_provider.py
@@ -0,0 +1,38 @@
+# Copyright 2014 Hewlett-Packard Development Company, L.P.
+# 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.
+
+
+class FakeAuthProvider(object):
+
+ def __init__(self, creds_dict=None, fake_base_url=None):
+ creds_dict = creds_dict or {}
+ self.credentials = FakeCredentials(creds_dict)
+ self.fake_base_url = fake_base_url
+
+ def auth_request(self, method, url, headers=None, body=None, filters=None):
+ return url, headers, body
+
+ def base_url(self, filters, auth_data=None):
+ return self.fake_base_url or "https://example.com"
+
+ def get_token(self):
+ return "faketoken"
+
+
+class FakeCredentials(object):
+
+ def __init__(self, creds_dict):
+ for key in creds_dict:
+ setattr(self, key, creds_dict[key])
diff --git a/tempest/tests/lib/fake_credentials.py b/tempest/tests/lib/fake_credentials.py
new file mode 100644
index 0000000..eac4ada
--- /dev/null
+++ b/tempest/tests/lib/fake_credentials.py
@@ -0,0 +1,74 @@
+# Copyright 2014 Hewlett-Packard Development Company, L.P.
+# 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 import auth
+
+
+class FakeCredentials(auth.Credentials):
+
+ def is_valid(self):
+ return True
+
+
+class FakeKeystoneV2Credentials(auth.KeystoneV2Credentials):
+
+ def __init__(self):
+ creds = dict(
+ username='fake_username',
+ password='fake_password',
+ tenant_name='fake_tenant_name'
+ )
+ super(FakeKeystoneV2Credentials, self).__init__(**creds)
+
+
+class FakeKeystoneV3Credentials(auth.KeystoneV3Credentials):
+ """Fake credentials suitable for the Keystone Identity V3 API"""
+
+ def __init__(self):
+ creds = dict(
+ username='fake_username',
+ password='fake_password',
+ user_domain_name='fake_domain_name',
+ project_name='fake_tenant_name',
+ project_domain_name='fake_domain_name'
+ )
+ super(FakeKeystoneV3Credentials, self).__init__(**creds)
+
+
+class FakeKeystoneV3DomainCredentials(auth.KeystoneV3Credentials):
+ """Fake credentials for the Keystone Identity V3 API, with no scope"""
+
+ def __init__(self):
+ creds = dict(
+ username='fake_username',
+ password='fake_password',
+ user_domain_name='fake_domain_name'
+ )
+ super(FakeKeystoneV3DomainCredentials, self).__init__(**creds)
+
+
+class FakeKeystoneV3AllCredentials(auth.KeystoneV3Credentials):
+ """Fake credentials for the Keystone Identity V3 API, with no scope"""
+
+ def __init__(self):
+ creds = dict(
+ username='fake_username',
+ password='fake_password',
+ user_domain_name='fake_domain_name',
+ project_name='fake_tenant_name',
+ project_domain_name='fake_domain_name',
+ domain_name='fake_domain_name'
+ )
+ super(FakeKeystoneV3AllCredentials, self).__init__(**creds)
diff --git a/tempest/tests/lib/fake_http.py b/tempest/tests/lib/fake_http.py
new file mode 100644
index 0000000..cfa4b93
--- /dev/null
+++ b/tempest/tests/lib/fake_http.py
@@ -0,0 +1,77 @@
+# Copyright 2013 IBM Corp.
+#
+# 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
+
+
+class fake_httplib2(object):
+
+ def __init__(self, return_type=None, *args, **kwargs):
+ self.return_type = return_type
+
+ def request(self, uri, method="GET", body=None, headers=None,
+ redirections=5, connection_type=None, chunked=False):
+ if not self.return_type:
+ fake_headers = fake_http_response(headers)
+ return_obj = {
+ 'uri': uri,
+ 'method': method,
+ 'body': body,
+ 'headers': headers
+ }
+ return (fake_headers, return_obj)
+ elif isinstance(self.return_type, int):
+ body = body or "fake_body"
+ header_info = {
+ 'content-type': 'text/plain',
+ 'content-length': len(body)
+ }
+ resp_header = fake_http_response(header_info,
+ status=self.return_type)
+ return (resp_header, body)
+ else:
+ msg = "unsupported return type %s" % self.return_type
+ raise TypeError(msg)
+
+
+class fake_http_response(dict):
+ def __init__(self, headers, body=None,
+ version=1.0, status=200, reason="Ok"):
+ """Initialization of fake HTTP Response
+
+ :param headers: dict representing HTTP response headers
+ :param body: file-like object
+ :param version: HTTP Version
+ :param status: Response status code
+ :param reason: Status code related message.
+ """
+ self.body = body
+ self.status = status
+ self['status'] = str(self.status)
+ self.reason = reason
+ self.version = version
+ self.headers = headers
+
+ if headers:
+ for key, value in headers.items():
+ self[key.lower()] = value
+
+ def getheaders(self):
+ return copy.deepcopy(self.headers).items()
+
+ def getheader(self, key, default):
+ return self.headers.get(key, default)
+
+ def read(self, amt):
+ return self.body.read(amt)
diff --git a/tempest/tests/lib/fake_identity.py b/tempest/tests/lib/fake_identity.py
new file mode 100644
index 0000000..8bae34f
--- /dev/null
+++ b/tempest/tests/lib/fake_identity.py
@@ -0,0 +1,240 @@
+# 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.tests.lib import fake_http
+
+FAKE_AUTH_URL = 'http://fake_uri.com/auth'
+
+TOKEN = "fake_token"
+ALT_TOKEN = "alt_fake_token"
+
+# Fake Identity v2 constants
+COMPUTE_ENDPOINTS_V2 = {
+ "endpoints": [
+ {
+ "adminURL": "http://fake_url/v2/first_endpoint/admin",
+ "region": "NoMatchRegion",
+ "internalURL": "http://fake_url/v2/first_endpoint/internal",
+ "publicURL": "http://fake_url/v2/first_endpoint/public"
+ },
+ {
+ "adminURL": "http://fake_url/v2/second_endpoint/admin",
+ "region": "FakeRegion",
+ "internalURL": "http://fake_url/v2/second_endpoint/internal",
+ "publicURL": "http://fake_url/v2/second_endpoint/public"
+ },
+ ],
+ "type": "compute",
+ "name": "nova"
+}
+
+CATALOG_V2 = [COMPUTE_ENDPOINTS_V2, ]
+
+ALT_IDENTITY_V2_RESPONSE = {
+ "access": {
+ "token": {
+ "expires": "2020-01-01T00:00:10Z",
+ "id": ALT_TOKEN,
+ "tenant": {
+ "id": "fake_alt_tenant_id"
+ },
+ },
+ "user": {
+ "id": "fake_alt_user_id",
+ "password_expires_at": None,
+ },
+ "serviceCatalog": CATALOG_V2,
+ },
+}
+
+IDENTITY_V2_RESPONSE = {
+ "access": {
+ "token": {
+ "expires": "2020-01-01T00:00:10Z",
+ "id": TOKEN,
+ "tenant": {
+ "id": "fake_tenant_id"
+ },
+ },
+ "user": {
+ "id": "fake_user_id",
+ "password_expires_at": None,
+ },
+ "serviceCatalog": CATALOG_V2,
+ },
+}
+
+# Fake Identity V3 constants
+COMPUTE_ENDPOINTS_V3 = {
+ "endpoints": [
+ {
+ "id": "first_compute_fake_service",
+ "interface": "public",
+ "region": "NoMatchRegion",
+ "region_id": "NoMatchRegion",
+ "url": "http://fake_url/v3/first_endpoint/api"
+ },
+ {
+ "id": "second_fake_service",
+ "interface": "public",
+ "region": "FakeRegion",
+ "region_id": "FakeRegion",
+ "url": "http://fake_url/v3/second_endpoint/api"
+ },
+ {
+ "id": "third_fake_service",
+ "interface": "admin",
+ "region": "MiddleEarthRegion",
+ "region_id": "MiddleEarthRegion",
+ "url": "http://fake_url/v3/third_endpoint/api"
+ }
+
+ ],
+ "type": "compute",
+ "id": "fake_compute_endpoint",
+ "name": "nova"
+}
+
+CATALOG_V3 = [COMPUTE_ENDPOINTS_V3, ]
+
+IDENTITY_V3_RESPONSE = {
+ "token": {
+ "audit_ids": ["ny5LA5YXToa_mAVO8Hnupw", "9NPTvsRDSkmsW61abP978Q"],
+ "methods": [
+ "token",
+ "password"
+ ],
+ "expires_at": "2020-01-01T00:00:10.000123Z",
+ "project": {
+ "domain": {
+ "id": "fake_domain_id",
+ "name": "fake"
+ },
+ "id": "project_id",
+ "name": "project_name"
+ },
+ "user": {
+ "domain": {
+ "id": "fake_domain_id",
+ "name": "domain_name"
+ },
+ "id": "fake_user_id",
+ "name": "username",
+ "password_expires_at": None,
+ },
+ "issued_at": "2013-05-29T16:55:21.468960Z",
+ "catalog": CATALOG_V3
+ }
+}
+
+IDENTITY_V3_RESPONSE_DOMAIN_SCOPE = {
+ "token": {
+ "audit_ids": ["ny5LA5YXToa_mAVO8Hnupw", "9NPTvsRDSkmsW61abP978Q"],
+ "methods": [
+ "token",
+ "password"
+ ],
+ "expires_at": "2020-01-01T00:00:10.000123Z",
+ "domain": {
+ "id": "fake_domain_id",
+ "name": "domain_name"
+ },
+ "user": {
+ "domain": {
+ "id": "fake_domain_id",
+ "name": "domain_name"
+ },
+ "id": "fake_user_id",
+ "name": "username",
+ "password_expires_at": None,
+ },
+ "issued_at": "2013-05-29T16:55:21.468960Z",
+ "catalog": CATALOG_V3
+ }
+}
+
+IDENTITY_V3_RESPONSE_NO_SCOPE = {
+ "token": {
+ "audit_ids": ["ny5LA5YXToa_mAVO8Hnupw", "9NPTvsRDSkmsW61abP978Q"],
+ "methods": [
+ "token",
+ "password"
+ ],
+ "expires_at": "2020-01-01T00:00:10.000123Z",
+ "user": {
+ "domain": {
+ "id": "fake_domain_id",
+ "name": "domain_name"
+ },
+ "id": "fake_user_id",
+ "name": "username",
+ "password_expires_at": None,
+ },
+ "issued_at": "2013-05-29T16:55:21.468960Z",
+ }
+}
+
+ALT_IDENTITY_V3 = IDENTITY_V3_RESPONSE
+
+
+def _fake_v3_response(self, uri, method="GET", body=None, headers=None,
+ redirections=5, connection_type=None):
+ fake_headers = {
+ "x-subject-token": TOKEN
+ }
+ return (fake_http.fake_http_response(fake_headers, status=201),
+ json.dumps(IDENTITY_V3_RESPONSE))
+
+
+def _fake_v3_response_domain_scope(self, uri, method="GET", body=None,
+ headers=None, redirections=5,
+ connection_type=None):
+ fake_headers = {
+ "status": "201",
+ "x-subject-token": TOKEN
+ }
+ return (fake_http.fake_http_response(fake_headers, status=201),
+ json.dumps(IDENTITY_V3_RESPONSE_DOMAIN_SCOPE))
+
+
+def _fake_v3_response_no_scope(self, uri, method="GET", body=None,
+ headers=None, redirections=5,
+ connection_type=None):
+ fake_headers = {
+ "status": "201",
+ "x-subject-token": TOKEN
+ }
+ return (fake_http.fake_http_response(fake_headers, status=201),
+ json.dumps(IDENTITY_V3_RESPONSE_NO_SCOPE))
+
+
+def _fake_v2_response(self, uri, method="GET", body=None, headers=None,
+ redirections=5, connection_type=None):
+ return (fake_http.fake_http_response({}, status=200),
+ json.dumps(IDENTITY_V2_RESPONSE))
+
+
+def _fake_auth_failure_response():
+ # the response body isn't really used in this case, but lets send it anyway
+ # to have a safe check in some future change on the rest client.
+ body = {
+ "unauthorized": {
+ "message": "Unauthorized",
+ "code": "401"
+ }
+ }
+ return fake_http.fake_http_response({}, status=401), json.dumps(body)
diff --git a/tempest/tests/services/__init__.py b/tempest/tests/lib/services/__init__.py
similarity index 100%
rename from tempest/tests/services/__init__.py
rename to tempest/tests/lib/services/__init__.py
diff --git a/tempest/tests/lib/services/base.py b/tempest/tests/lib/services/base.py
new file mode 100644
index 0000000..924f9f2
--- /dev/null
+++ b/tempest/tests/lib/services/base.py
@@ -0,0 +1,77 @@
+# Copyright 2015 Deutsche Telekom AG. 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 fixtures
+from oslo_serialization import jsonutils as json
+
+from tempest.tests import base
+from tempest.tests.lib import fake_http
+
+
+class BaseServiceTest(base.TestCase):
+ def create_response(self, body, to_utf=False, status=200, headers=None):
+ json_body = {}
+ if body:
+ json_body = json.dumps(body)
+ if to_utf:
+ json_body = json_body.encode('utf-8')
+ resp = fake_http.fake_http_response(headers, status=status), json_body
+ return resp
+
+ def check_service_client_function(self, function, function2mock,
+ body, to_utf=False, status=200,
+ headers=None, mock_args=None,
+ resp_as_string=False,
+ **kwargs):
+ """Mock a service client function for unit testing.
+
+ :param function: The service client function to call.
+ :param function2mock: The REST call to mock inside the service client
+ function.
+ :param body: Expected response body returned by the service client
+ function.
+ :param to_utf: Whether to use UTF-8 encoding for response.
+ :param status: Expected response status returned by the service client
+ function.
+ :param headers: Expected headers returned by the service client
+ function.
+ :param mock_args: List/dict/value of expected args/kwargs called by
+ function2mock. For example:
+ * If mock_args=['foo'] then ``assert_called_once_with('foo')``
+ is called.
+ * If mock_args={'foo': 'bar'} then
+ ``assert_called_once_with(foo='bar')`` is called.
+ * If mock_args='foo' then ``assert_called_once_with('foo')``
+ is called.
+ :param resp_as_string: Whether response body is retruned as string.
+ This is for service client methods which return ResponseBodyData
+ object.
+ :param kwargs: kwargs that are passed to function.
+ """
+ mocked_response = self.create_response(body, to_utf, status, headers)
+ fixture = self.useFixture(fixtures.MockPatch(
+ function2mock, return_value=mocked_response))
+ if kwargs:
+ resp = function(**kwargs)
+ else:
+ resp = function()
+ if resp_as_string:
+ resp = resp.data
+ self.assertEqual(body, resp)
+ if isinstance(mock_args, list):
+ fixture.mock.assert_called_once_with(*mock_args)
+ elif isinstance(mock_args, dict):
+ fixture.mock.assert_called_once_with(**mock_args)
+ elif mock_args is not None:
+ fixture.mock.assert_called_once_with(mock_args)
diff --git a/tempest/tests/services/compute/__init__.py b/tempest/tests/lib/services/compute/__init__.py
similarity index 100%
rename from tempest/tests/services/compute/__init__.py
rename to tempest/tests/lib/services/compute/__init__.py
diff --git a/tempest/tests/lib/services/compute/test_agents_client.py b/tempest/tests/lib/services/compute/test_agents_client.py
new file mode 100644
index 0000000..880220e
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_agents_client.py
@@ -0,0 +1,103 @@
+# Copyright 2015 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.services.compute import agents_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestAgentsClient(base.BaseServiceTest):
+ FAKE_CREATE_AGENT = {
+ "agent": {
+ "url": "http://foo.com",
+ "hypervisor": "kvm",
+ "md5hash": "md5",
+ "version": "2",
+ "architecture": "x86_64",
+ "os": "linux",
+ "agent_id": 1
+ }
+ }
+
+ FAKE_UPDATE_AGENT = {
+ "agent": {
+ "url": "http://foo.com",
+ "md5hash": "md5",
+ "version": "2",
+ "agent_id": 1
+ }
+ }
+
+ def setUp(self):
+ super(TestAgentsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = agents_client.AgentsClient(fake_auth,
+ 'compute', 'regionOne')
+
+ def _test_list_agents(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_agents,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {"agents": []},
+ bytes_body)
+ self.check_service_client_function(
+ self.client.list_agents,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {"agents": []},
+ bytes_body,
+ hypervisor="kvm")
+
+ def _test_create_agent(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_agent,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_AGENT,
+ bytes_body,
+ url="http://foo.com", hypervisor="kvm", md5hash="md5",
+ version="2", architecture="x86_64", os="linux")
+
+ def _test_delete_agent(self):
+ self.check_service_client_function(
+ self.client.delete_agent,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {}, agent_id="1")
+
+ def _test_update_agent(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_agent,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ self.FAKE_UPDATE_AGENT,
+ bytes_body,
+ agent_id="1", url="http://foo.com", md5hash="md5", version="2")
+
+ def test_list_agents_with_str_body(self):
+ self._test_list_agents()
+
+ def test_list_agents_with_bytes_body(self):
+ self._test_list_agents(bytes_body=True)
+
+ def test_create_agent_with_str_body(self):
+ self._test_create_agent()
+
+ def test_create_agent_with_bytes_body(self):
+ self._test_create_agent(bytes_body=True)
+
+ def test_delete_agent(self):
+ self._test_delete_agent()
+
+ def test_update_agent_with_str_body(self):
+ self._test_update_agent()
+
+ def test_update_agent_with_bytes_body(self):
+ self._test_update_agent(bytes_body=True)
diff --git a/tempest/tests/lib/services/compute/test_aggregates_client.py b/tempest/tests/lib/services/compute/test_aggregates_client.py
new file mode 100644
index 0000000..674d92a
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_aggregates_client.py
@@ -0,0 +1,192 @@
+# Copyright 2015 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.services.compute import aggregates_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestAggregatesClient(base.BaseServiceTest):
+ FAKE_SHOW_AGGREGATE = {
+ "aggregate":
+ {
+ "name": "hoge",
+ "availability_zone": None,
+ "deleted": False,
+ "created_at":
+ "2015-07-16T03:07:32.000000",
+ "updated_at": None,
+ "hosts": [],
+ "deleted_at": None,
+ "id": 1,
+ "metadata": {}
+ }
+ }
+
+ FAKE_CREATE_AGGREGATE = {
+ "aggregate":
+ {
+ "name": u'\xf4',
+ "availability_zone": None,
+ "deleted": False,
+ "created_at": "2015-07-21T04:11:18.000000",
+ "updated_at": None,
+ "deleted_at": None,
+ "id": 1
+ }
+ }
+
+ FAKE_UPDATE_AGGREGATE = {
+ "aggregate":
+ {
+ "name": u'\xe9',
+ "availability_zone": None,
+ "deleted": False,
+ "created_at": "2015-07-16T03:07:32.000000",
+ "updated_at": "2015-07-23T05:16:29.000000",
+ "hosts": [],
+ "deleted_at": None,
+ "id": 1,
+ "metadata": {}
+ }
+ }
+
+ FAKE_AGGREGATE = {
+ "availability_zone": "nova",
+ "created_at": "2013-08-18T12:17:56.297823",
+ "deleted": False,
+ "deleted_at": None,
+ "hosts": [
+ "21549b2f665945baaa7101926a00143c"
+ ],
+ "id": 1,
+ "metadata": {
+ "availability_zone": "nova"
+ },
+ "name": u'\xe9',
+ "updated_at": None
+ }
+
+ FAKE_ADD_HOST = {'aggregate': FAKE_AGGREGATE}
+ FAKE_REMOVE_HOST = {'aggregate': FAKE_AGGREGATE}
+ FAKE_SET_METADATA = {'aggregate': FAKE_AGGREGATE}
+
+ def setUp(self):
+ super(TestAggregatesClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = aggregates_client.AggregatesClient(
+ fake_auth, 'compute', 'regionOne')
+
+ def _test_list_aggregates(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_aggregates,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {"aggregates": []},
+ bytes_body)
+
+ def test_list_aggregates_with_str_body(self):
+ self._test_list_aggregates()
+
+ def test_list_aggregates_with_bytes_body(self):
+ self._test_list_aggregates(bytes_body=True)
+
+ def _test_show_aggregate(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_aggregate,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_SHOW_AGGREGATE,
+ bytes_body,
+ aggregate_id=1)
+
+ def test_show_aggregate_with_str_body(self):
+ self._test_show_aggregate()
+
+ def test_show_aggregate_with_bytes_body(self):
+ self._test_show_aggregate(bytes_body=True)
+
+ def _test_create_aggregate(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_aggregate,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_AGGREGATE,
+ bytes_body,
+ name='hoge')
+
+ def test_create_aggregate_with_str_body(self):
+ self._test_create_aggregate()
+
+ def test_create_aggregate_with_bytes_body(self):
+ self._test_create_aggregate(bytes_body=True)
+
+ def test_delete_aggregate(self):
+ self.check_service_client_function(
+ self.client.delete_aggregate,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {}, aggregate_id="1")
+
+ def _test_update_aggregate(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_aggregate,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ self.FAKE_UPDATE_AGGREGATE,
+ bytes_body,
+ aggregate_id=1)
+
+ def test_update_aggregate_with_str_body(self):
+ self._test_update_aggregate()
+
+ def test_update_aggregate_with_bytes_body(self):
+ self._test_update_aggregate(bytes_body=True)
+
+ def _test_add_host(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.add_host,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_ADD_HOST,
+ bytes_body,
+ aggregate_id=1)
+
+ def test_add_host_with_str_body(self):
+ self._test_add_host()
+
+ def test_add_host_with_bytes_body(self):
+ self._test_add_host(bytes_body=True)
+
+ def _test_remove_host(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.remove_host,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_REMOVE_HOST,
+ bytes_body,
+ aggregate_id=1)
+
+ def test_remove_host_with_str_body(self):
+ self._test_remove_host()
+
+ def test_remove_host_with_bytes_body(self):
+ self._test_remove_host(bytes_body=True)
+
+ def _test_set_metadata(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.set_metadata,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_SET_METADATA,
+ bytes_body,
+ aggregate_id=1)
+
+ def test_set_metadata_with_str_body(self):
+ self._test_set_metadata()
+
+ def test_set_metadata_with_bytes_body(self):
+ self._test_set_metadata(bytes_body=True)
diff --git a/tempest/tests/lib/services/compute/test_availability_zone_client.py b/tempest/tests/lib/services/compute/test_availability_zone_client.py
new file mode 100644
index 0000000..6608592
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_availability_zone_client.py
@@ -0,0 +1,51 @@
+# Copyright 2015 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.services.compute import availability_zone_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestAvailabilityZoneClient(base.BaseServiceTest):
+
+ FAKE_AVAILABIRITY_ZONE_INFO = {
+ "availabilityZoneInfo":
+ [
+ {
+ "zoneState": {
+ "available": True
+ },
+ "hosts": None,
+ "zoneName": u'\xf4'
+ }
+ ]
+ }
+
+ def setUp(self):
+ super(TestAvailabilityZoneClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = availability_zone_client.AvailabilityZoneClient(
+ fake_auth, 'compute', 'regionOne')
+
+ def test_list_availability_zones_with_str_body(self):
+ self.check_service_client_function(
+ self.client.list_availability_zones,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_AVAILABIRITY_ZONE_INFO)
+
+ def test_list_availability_zones_with_bytes_body(self):
+ self.check_service_client_function(
+ self.client.list_availability_zones,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_AVAILABIRITY_ZONE_INFO, to_utf=True)
diff --git a/tempest/tests/lib/services/compute/test_baremetal_nodes_client.py b/tempest/tests/lib/services/compute/test_baremetal_nodes_client.py
new file mode 100644
index 0000000..81c54b5
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_baremetal_nodes_client.py
@@ -0,0 +1,74 @@
+# Copyright 2015 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.
+
+import copy
+
+from tempest.lib.services.compute import baremetal_nodes_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestBareMetalNodesClient(base.BaseServiceTest):
+
+ FAKE_NODE_INFO = {'cpus': '8',
+ 'disk_gb': '64',
+ 'host': '10.0.2.15',
+ 'id': 'Identifier',
+ 'instance_uuid': "null",
+ 'interfaces': [
+ {
+ "address": "20::01",
+ "datapath_id": "null",
+ "id": 1,
+ "port_no": None
+ }
+ ],
+ 'memory_mb': '8192',
+ 'task_state': None}
+
+ def setUp(self):
+ super(TestBareMetalNodesClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.baremetal_nodes_client = (baremetal_nodes_client.
+ BaremetalNodesClient
+ (fake_auth, 'compute',
+ 'regionOne'))
+
+ def _test_bareMetal_nodes(self, operation='list', bytes_body=False):
+ if operation != 'list':
+ expected = {"node": self.FAKE_NODE_INFO}
+ function = self.baremetal_nodes_client.show_baremetal_node
+ else:
+ node_info = copy.deepcopy(self.FAKE_NODE_INFO)
+ del node_info['instance_uuid']
+ expected = {"nodes": [node_info]}
+ function = self.baremetal_nodes_client.list_baremetal_nodes
+
+ self.check_service_client_function(
+ function,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ expected, bytes_body, 200,
+ baremetal_node_id='Identifier')
+
+ def test_list_bareMetal_nodes_with_str_body(self):
+ self._test_bareMetal_nodes()
+
+ def test_list_bareMetal_nodes_with_bytes_body(self):
+ self._test_bareMetal_nodes(bytes_body=True)
+
+ def test_show_bareMetal_node_with_str_body(self):
+ self._test_bareMetal_nodes('show')
+
+ def test_show_bareMetal_node_with_bytes_body(self):
+ self._test_bareMetal_nodes('show', True)
diff --git a/tempest/tests/lib/services/compute/test_base_compute_client.py b/tempest/tests/lib/services/compute/test_base_compute_client.py
new file mode 100644
index 0000000..69e8542
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_base_compute_client.py
@@ -0,0 +1,206 @@
+# Copyright 2015 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.
+
+import mock
+
+from tempest.lib.common import rest_client
+from tempest.lib import exceptions
+from tempest.lib.services.compute import base_compute_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib import fake_http
+from tempest.tests.lib.services import base
+
+
+class TestMicroversionHeaderCheck(base.BaseServiceTest):
+
+ def setUp(self):
+ super(TestMicroversionHeaderCheck, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = base_compute_client.BaseComputeClient(
+ fake_auth, 'compute', 'regionOne')
+ base_compute_client.COMPUTE_MICROVERSION = '2.2'
+
+ def tearDown(self):
+ super(TestMicroversionHeaderCheck, self).tearDown()
+ base_compute_client.COMPUTE_MICROVERSION = None
+
+ @mock.patch('tempest.lib.common.http.ClosingHttp.request')
+ def test_correct_microverion_in_response(self, mock_request):
+ response = fake_http.fake_http_response(
+ headers={self.client.api_microversion_header_name: '2.2'},
+ )
+ mock_request.return_value = response, ''
+ self.client.get('fake_url')
+
+ @mock.patch('tempest.lib.common.http.ClosingHttp.request')
+ def test_incorrect_microverion_in_response(self, mock_request):
+ response = fake_http.fake_http_response(
+ headers={self.client.api_microversion_header_name: '2.3'},
+ )
+ mock_request.return_value = response, ''
+ self.assertRaises(exceptions.InvalidHTTPResponseHeader,
+ self.client.get, 'fake_url')
+
+ @mock.patch('tempest.lib.common.http.ClosingHttp.request')
+ def test_no_microverion_header_in_response(self, mock_request):
+ response = fake_http.fake_http_response(
+ headers={},
+ )
+ mock_request.return_value = response, ''
+ self.assertRaises(exceptions.InvalidHTTPResponseHeader,
+ self.client.get, 'fake_url')
+
+
+class DummyServiceClient1(base_compute_client.BaseComputeClient):
+ schema_versions_info = [
+ {'min': None, 'max': '2.1', 'schema': 'schemav21'},
+ {'min': '2.2', 'max': '2.9', 'schema': 'schemav22'},
+ {'min': '2.10', 'max': None, 'schema': 'schemav210'}]
+
+ def return_selected_schema(self):
+ return self.get_schema(self.schema_versions_info)
+
+
+class TestSchemaVersionsNone(base.BaseServiceTest):
+ api_microversion = None
+ expected_schema = 'schemav21'
+
+ def setUp(self):
+ super(TestSchemaVersionsNone, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = DummyServiceClient1(fake_auth, 'compute', 'regionOne')
+ base_compute_client.COMPUTE_MICROVERSION = self.api_microversion
+
+ def tearDown(self):
+ super(TestSchemaVersionsNone, self).tearDown()
+ base_compute_client.COMPUTE_MICROVERSION = None
+
+ def test_schema(self):
+ self.assertEqual(self.expected_schema,
+ self.client.return_selected_schema())
+
+
+class TestSchemaVersionsV21(TestSchemaVersionsNone):
+ api_microversion = '2.1'
+ expected_schema = 'schemav21'
+
+
+class TestSchemaVersionsV22(TestSchemaVersionsNone):
+ api_microversion = '2.2'
+ expected_schema = 'schemav22'
+
+
+class TestSchemaVersionsV25(TestSchemaVersionsNone):
+ api_microversion = '2.5'
+ expected_schema = 'schemav22'
+
+
+class TestSchemaVersionsV29(TestSchemaVersionsNone):
+ api_microversion = '2.9'
+ expected_schema = 'schemav22'
+
+
+class TestSchemaVersionsV210(TestSchemaVersionsNone):
+ api_microversion = '2.10'
+ expected_schema = 'schemav210'
+
+
+class TestSchemaVersionsLatest(TestSchemaVersionsNone):
+ api_microversion = 'latest'
+ expected_schema = 'schemav210'
+
+
+class DummyServiceClient2(base_compute_client.BaseComputeClient):
+ schema_versions_info = [
+ {'min': None, 'max': '2.1', 'schema': 'schemav21'},
+ {'min': '2.2', 'max': '2.9', 'schema': 'schemav22'}]
+
+ def return_selected_schema(self):
+ return self.get_schema(self.schema_versions_info)
+
+
+class TestSchemaVersionsNotFound(base.BaseServiceTest):
+ api_microversion = '2.10'
+ expected_schema = 'schemav210'
+
+ def setUp(self):
+ super(TestSchemaVersionsNotFound, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = DummyServiceClient2(fake_auth, 'compute', 'regionOne')
+ base_compute_client.COMPUTE_MICROVERSION = self.api_microversion
+
+ def tearDown(self):
+ super(TestSchemaVersionsNotFound, self).tearDown()
+ base_compute_client.COMPUTE_MICROVERSION = None
+
+ def test_schema(self):
+ self.assertRaises(exceptions.JSONSchemaNotFound,
+ self.client.return_selected_schema)
+
+
+class TestClientWithoutMicroversionHeader(base.BaseServiceTest):
+
+ def setUp(self):
+ super(TestClientWithoutMicroversionHeader, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = base_compute_client.BaseComputeClient(
+ fake_auth, 'compute', 'regionOne')
+
+ def test_no_microverion_header(self):
+ header = self.client.get_headers()
+ self.assertNotIn('X-OpenStack-Nova-API-Version', header)
+
+ def test_no_microverion_header_in_raw_request(self):
+ def raw_request(*args, **kwargs):
+ self.assertNotIn('X-OpenStack-Nova-API-Version', kwargs['headers'])
+ return (fake_http.fake_http_response({}, status=200), '')
+
+ with mock.patch.object(rest_client.RestClient,
+ 'raw_request') as mock_get:
+ mock_get.side_effect = raw_request
+ self.client.get('fake_url')
+
+
+class TestClientWithMicroversionHeader(base.BaseServiceTest):
+
+ def setUp(self):
+ super(TestClientWithMicroversionHeader, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = base_compute_client.BaseComputeClient(
+ fake_auth, 'compute', 'regionOne')
+ base_compute_client.COMPUTE_MICROVERSION = '2.2'
+
+ def tearDown(self):
+ super(TestClientWithMicroversionHeader, self).tearDown()
+ base_compute_client.COMPUTE_MICROVERSION = None
+
+ def test_microverion_header(self):
+ header = self.client.get_headers()
+ self.assertIn('X-OpenStack-Nova-API-Version', header)
+ self.assertEqual('2.2',
+ header['X-OpenStack-Nova-API-Version'])
+
+ def test_microverion_header_in_raw_request(self):
+ def raw_request(*args, **kwargs):
+ self.assertIn('X-OpenStack-Nova-API-Version', kwargs['headers'])
+ self.assertEqual('2.2',
+ kwargs['headers']['X-OpenStack-Nova-API-Version'])
+ return (fake_http.fake_http_response(
+ headers={self.client.api_microversion_header_name: '2.2'},
+ status=200), '')
+
+ with mock.patch.object(rest_client.RestClient,
+ 'raw_request') as mock_get:
+ mock_get.side_effect = raw_request
+ self.client.get('fake_url')
diff --git a/tempest/tests/lib/services/compute/test_certificates_client.py b/tempest/tests/lib/services/compute/test_certificates_client.py
new file mode 100644
index 0000000..9faef6f
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_certificates_client.py
@@ -0,0 +1,64 @@
+# Copyright 2015 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.
+
+import copy
+
+from tempest.lib.services.compute import certificates_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestCertificatesClient(base.BaseServiceTest):
+
+ FAKE_CERTIFICATE = {
+ "certificate": {
+ "data": "-----BEGIN----MIICyzCCAjSgAwI----END CERTIFICATE-----\n",
+ "private_key": None
+ }
+ }
+
+ def setUp(self):
+ super(TestCertificatesClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = certificates_client.CertificatesClient(
+ fake_auth, 'compute', 'regionOne')
+
+ def _test_show_certificate(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_certificate,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_CERTIFICATE,
+ bytes_body,
+ certificate_id="fake-id")
+
+ def test_show_certificate_with_str_body(self):
+ self._test_show_certificate()
+
+ def test_show_certificate_with_bytes_body(self):
+ self._test_show_certificate(bytes_body=True)
+
+ def _test_create_certificate(self, bytes_body=False):
+ cert = copy.deepcopy(self.FAKE_CERTIFICATE)
+ cert['certificate']['private_key'] = "my_private_key"
+ self.check_service_client_function(
+ self.client.create_certificate,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ cert,
+ bytes_body)
+
+ def test_create_certificate_with_str_body(self):
+ self._test_create_certificate()
+
+ def test_create_certificate_with_bytes_body(self):
+ self._test_create_certificate(bytes_body=True)
diff --git a/tempest/tests/lib/services/compute/test_extensions_client.py b/tempest/tests/lib/services/compute/test_extensions_client.py
new file mode 100644
index 0000000..d7e217e
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_extensions_client.py
@@ -0,0 +1,65 @@
+# Copyright 2015 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.services.compute import extensions_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestExtensionsClient(base.BaseServiceTest):
+
+ FAKE_SHOW_EXTENSION = {
+ "extension": {
+ "updated": "2011-06-09T00:00:00Z",
+ "name": "Multinic",
+ "links": [],
+ "namespace":
+ "http://docs.openstack.org/compute/ext/multinic/api/v1.1",
+ "alias": "NMN",
+ "description": u'\u2740(*\xb4\u25e1`*)\u2740'
+ }
+ }
+
+ def setUp(self):
+ super(TestExtensionsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = extensions_client.ExtensionsClient(
+ fake_auth, 'compute', 'regionOne')
+
+ def _test_list_extensions(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_extensions,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {"extensions": []},
+ bytes_body)
+
+ def test_list_extensions_with_str_body(self):
+ self._test_list_extensions()
+
+ def test_list_extensions_with_bytes_body(self):
+ self._test_list_extensions(bytes_body=True)
+
+ def _test_show_extension(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_extension,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_SHOW_EXTENSION,
+ bytes_body,
+ extension_alias="NMN")
+
+ def test_show_extension_with_str_body(self):
+ self._test_show_extension()
+
+ def test_show_extension_with_bytes_body(self):
+ self._test_show_extension(bytes_body=True)
diff --git a/tempest/tests/lib/services/compute/test_fixedIPs_client.py b/tempest/tests/lib/services/compute/test_fixedIPs_client.py
new file mode 100644
index 0000000..65bda45
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_fixedIPs_client.py
@@ -0,0 +1,58 @@
+# Copyright 2015 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.services.compute import fixed_ips_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestFixedIPsClient(base.BaseServiceTest):
+ FIXED_IP_INFO = {"fixed_ip": {"address": "10.0.0.1",
+ "cidr": "10.11.12.0/24",
+ "host": "localhost",
+ "hostname": "OpenStack"}}
+
+ def setUp(self):
+ super(TestFixedIPsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.fixedIPsClient = (fixed_ips_client.
+ FixedIPsClient
+ (fake_auth, 'compute',
+ 'regionOne'))
+
+ def _test_show_fixed_ip(self, bytes_body=False):
+ self.check_service_client_function(
+ self.fixedIPsClient.show_fixed_ip,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FIXED_IP_INFO, bytes_body,
+ status=200, fixed_ip='Identifier')
+
+ def test_show_fixed_ip_with_str_body(self):
+ self._test_show_fixed_ip()
+
+ def test_show_fixed_ip_with_bytes_body(self):
+ self._test_show_fixed_ip(True)
+
+ def _test_reserve_fixed_ip(self, bytes_body=False):
+ self.check_service_client_function(
+ self.fixedIPsClient.reserve_fixed_ip,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {}, bytes_body,
+ status=202, fixed_ip='Identifier')
+
+ def test_reserve_fixed_ip_with_str_body(self):
+ self._test_reserve_fixed_ip()
+
+ def test_reserve_fixed_ip_with_bytes_body(self):
+ self._test_reserve_fixed_ip(True)
diff --git a/tempest/tests/lib/services/compute/test_flavors_client.py b/tempest/tests/lib/services/compute/test_flavors_client.py
new file mode 100644
index 0000000..5325036
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_flavors_client.py
@@ -0,0 +1,290 @@
+# Copyright 2015 IBM Corp.
+#
+# 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
+
+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
+from tempest.tests.lib.services import base
+
+
+class TestFlavorsClient(base.BaseServiceTest):
+
+ FAKE_FLAVOR = {
+ "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
+ }
+
+ 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"}
+ }
+
+ FAKE_FLAVOR_ACCESS = {
+ "flavor_id": "10",
+ "tenant_id": "1a951d988e264818afe520e78697dcbf"
+ }
+
+ def setUp(self):
+ super(TestFlavorsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = flavors_client.FlavorsClient(fake_auth,
+ 'compute', 'regionOne')
+
+ def _test_list_flavors(self, bytes_body=False):
+ flavor = copy.deepcopy(TestFlavorsClient.FAKE_FLAVOR)
+ # Remove extra attributes
+ for attribute in ('disk', 'vcpus', 'ram', 'swap'):
+ del flavor[attribute]
+ expected = {'flavors': [flavor]}
+ self.check_service_client_function(
+ self.client.list_flavors,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ expected,
+ bytes_body)
+
+ def test_list_flavors_str_body(self):
+ self._test_list_flavors(bytes_body=False)
+
+ def test_list_flavors_byte_body(self):
+ self._test_list_flavors(bytes_body=True)
+
+ def _test_show_flavor(self, bytes_body=False):
+ expected = {"flavor": TestFlavorsClient.FAKE_FLAVOR}
+ self.check_service_client_function(
+ self.client.show_flavor,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ expected,
+ bytes_body,
+ flavor_id='fake-id')
+
+ def test_show_flavor_str_body(self):
+ self._test_show_flavor(bytes_body=False)
+
+ def test_show_flavor_byte_body(self):
+ self._test_show_flavor(bytes_body=True)
+
+ def _test_create_flavor(self, bytes_body=False):
+ expected = {"flavor": TestFlavorsClient.FAKE_FLAVOR}
+ request = copy.deepcopy(TestFlavorsClient.FAKE_FLAVOR)
+ # The 'links' parameter should not be passed in
+ del request['links']
+ self.check_service_client_function(
+ self.client.create_flavor,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ expected,
+ bytes_body,
+ **request)
+
+ def test_create_flavor_str_body(self):
+ self._test_create_flavor(bytes_body=False)
+
+ 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,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {}, status=202, flavor_id='c782b7a9-33cd-45f0-b795-7f87f456408b')
+
+ def _test_is_resource_deleted(self, flavor_id, is_deleted=True,
+ bytes_body=False):
+ body = json.dumps({'flavors': [TestFlavorsClient.FAKE_FLAVOR]})
+ if bytes_body:
+ body = body.encode('utf-8')
+ response = fake_http.fake_http_response({}, status=200), body
+ self.useFixture(fixtures.MockPatch(
+ 'tempest.lib.common.rest_client.RestClient.get',
+ return_value=response))
+ self.assertEqual(is_deleted,
+ self.client.is_resource_deleted(flavor_id))
+
+ def test_is_resource_deleted_true_str_body(self):
+ self._test_is_resource_deleted('2', bytes_body=False)
+
+ def test_is_resource_deleted_true_byte_body(self):
+ self._test_is_resource_deleted('2', bytes_body=True)
+
+ def test_is_resource_deleted_false_str_body(self):
+ self._test_is_resource_deleted('1', is_deleted=False, bytes_body=False)
+
+ def test_is_resource_deleted_false_byte_body(self):
+ self._test_is_resource_deleted('1', is_deleted=False, bytes_body=True)
+
+ def _test_set_flavor_extra_spec(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.set_flavor_extra_spec,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ TestFlavorsClient.EXTRA_SPECS,
+ bytes_body,
+ flavor_id='8c7aae5a-d315-4216-875b-ed9b6a5bcfc6',
+ **TestFlavorsClient.EXTRA_SPECS)
+
+ def test_set_flavor_extra_spec_str_body(self):
+ self._test_set_flavor_extra_spec(bytes_body=False)
+
+ def test_set_flavor_extra_spec_byte_body(self):
+ self._test_set_flavor_extra_spec(bytes_body=True)
+
+ def _test_list_flavor_extra_specs(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_flavor_extra_specs,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ TestFlavorsClient.EXTRA_SPECS,
+ bytes_body,
+ flavor_id='8c7aae5a-d315-4216-875b-ed9b6a5bcfc6')
+
+ def test_list_flavor_extra_specs_str_body(self):
+ self._test_list_flavor_extra_specs(bytes_body=False)
+
+ def test_list_flavor_extra_specs__byte_body(self):
+ self._test_list_flavor_extra_specs(bytes_body=True)
+
+ def _test_show_flavor_extra_spec(self, bytes_body=False):
+ expected = {"key": "value"}
+ self.check_service_client_function(
+ self.client.show_flavor_extra_spec,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ expected,
+ bytes_body,
+ flavor_id='8c7aae5a-d315-4216-875b-ed9b6a5bcfc6',
+ key='key')
+
+ def test_show_flavor_extra_spec_str_body(self):
+ self._test_show_flavor_extra_spec(bytes_body=False)
+
+ def test_show_flavor_extra_spec__byte_body(self):
+ self._test_show_flavor_extra_spec(bytes_body=True)
+
+ def _test_update_flavor_extra_spec(self, bytes_body=False):
+ expected = {"key1": "value"}
+ self.check_service_client_function(
+ self.client.update_flavor_extra_spec,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ expected,
+ bytes_body,
+ flavor_id='8c7aae5a-d315-4216-875b-ed9b6a5bcfc6',
+ key='key1', **expected)
+
+ def test_update_flavor_extra_spec_str_body(self):
+ self._test_update_flavor_extra_spec(bytes_body=False)
+
+ def test_update_flavor_extra_spec_byte_body(self):
+ self._test_update_flavor_extra_spec(bytes_body=True)
+
+ def test_unset_flavor_extra_spec(self):
+ self.check_service_client_function(
+ self.client.unset_flavor_extra_spec,
+ 'tempest.lib.common.rest_client.RestClient.delete', {},
+ flavor_id='c782b7a9-33cd-45f0-b795-7f87f456408b', key='key')
+
+ def _test_list_flavor_access(self, bytes_body=False):
+ expected = {'flavor_access': [TestFlavorsClient.FAKE_FLAVOR_ACCESS]}
+ self.check_service_client_function(
+ self.client.list_flavor_access,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ expected,
+ bytes_body,
+ flavor_id='8c7aae5a-d315-4216-875b-ed9b6a5bcfc6')
+
+ def test_list_flavor_access_str_body(self):
+ self._test_list_flavor_access(bytes_body=False)
+
+ def test_list_flavor_access_byte_body(self):
+ self._test_list_flavor_access(bytes_body=True)
+
+ def _test_add_flavor_access(self, bytes_body=False):
+ expected = {
+ "flavor_access": [TestFlavorsClient.FAKE_FLAVOR_ACCESS]
+ }
+ self.check_service_client_function(
+ self.client.add_flavor_access,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ expected,
+ bytes_body,
+ flavor_id='8c7aae5a-d315-4216-875b-ed9b6a5bcfc6',
+ tenant_id='1a951d988e264818afe520e78697dcbf')
+
+ def test_add_flavor_access_str_body(self):
+ self._test_add_flavor_access(bytes_body=False)
+
+ def test_add_flavor_access_byte_body(self):
+ self._test_add_flavor_access(bytes_body=True)
+
+ def _test_remove_flavor_access(self, bytes_body=False):
+ expected = {
+ "flavor_access": [TestFlavorsClient.FAKE_FLAVOR_ACCESS]
+ }
+ self.check_service_client_function(
+ self.client.remove_flavor_access,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ expected,
+ bytes_body,
+ flavor_id='10',
+ tenant_id='a6edd4d66ad04245b5d2d8716ecc91e3')
+
+ def test_remove_flavor_access_str_body(self):
+ self._test_remove_flavor_access(bytes_body=False)
+
+ def test_remove_flavor_access_byte_body(self):
+ self._test_remove_flavor_access(bytes_body=True)
diff --git a/tempest/tests/lib/services/compute/test_floating_ip_pools_client.py b/tempest/tests/lib/services/compute/test_floating_ip_pools_client.py
new file mode 100644
index 0000000..b0c00f0
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_floating_ip_pools_client.py
@@ -0,0 +1,46 @@
+# Copyright 2015 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.services.compute import floating_ip_pools_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestFloatingIPPoolsClient(base.BaseServiceTest):
+
+ FAKE_FLOATING_IP_POOLS = {
+ "floating_ip_pools":
+ [
+ {"name": u'\u3042'},
+ {"name": u'\u3044'}
+ ]
+ }
+
+ def setUp(self):
+ super(TestFloatingIPPoolsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = floating_ip_pools_client.FloatingIPPoolsClient(
+ fake_auth, 'compute', 'regionOne')
+
+ def test_list_floating_ip_pools_with_str_body(self):
+ self.check_service_client_function(
+ self.client.list_floating_ip_pools,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_FLOATING_IP_POOLS)
+
+ def test_list_floating_ip_pools_with_bytes_body(self):
+ self.check_service_client_function(
+ self.client.list_floating_ip_pools,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_FLOATING_IP_POOLS, to_utf=True)
diff --git a/tempest/tests/lib/services/compute/test_floating_ips_bulk_client.py b/tempest/tests/lib/services/compute/test_floating_ips_bulk_client.py
new file mode 100644
index 0000000..ace76f8
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_floating_ips_bulk_client.py
@@ -0,0 +1,88 @@
+# Copyright 2015 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.tests.lib import fake_auth_provider
+
+from tempest.lib.services.compute import floating_ips_bulk_client
+from tempest.tests.lib.services import base
+
+
+class TestFloatingIPsBulkClient(base.BaseServiceTest):
+
+ FAKE_FIP_BULK_LIST = {"floating_ip_info": [{
+ "address": "10.10.10.1",
+ "instance_uuid": None,
+ "fixed_ip": None,
+ "interface": "eth0",
+ "pool": "nova",
+ "project_id": None
+ },
+ {
+ "address": "10.10.10.2",
+ "instance_uuid": None,
+ "fixed_ip": None,
+ "interface": "eth0",
+ "pool": "nova",
+ "project_id": None
+ }]}
+
+ def setUp(self):
+ super(TestFloatingIPsBulkClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = floating_ips_bulk_client.FloatingIPsBulkClient(
+ fake_auth, 'compute', 'regionOne')
+
+ def _test_list_floating_ips_bulk(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_floating_ips_bulk,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_FIP_BULK_LIST,
+ to_utf=bytes_body)
+
+ def _test_create_floating_ips_bulk(self, bytes_body=False):
+ fake_fip_create_data = {"floating_ips_bulk_create": {
+ "ip_range": "192.168.1.0/24", "pool": "nova", "interface": "eth0"}}
+ self.check_service_client_function(
+ self.client.create_floating_ips_bulk,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ fake_fip_create_data,
+ to_utf=bytes_body,
+ ip_range="192.168.1.0/24", pool="nova", interface="eth0")
+
+ def _test_delete_floating_ips_bulk(self, bytes_body=False):
+ fake_fip_delete_data = {"floating_ips_bulk_delete": "192.168.1.0/24"}
+ self.check_service_client_function(
+ self.client.delete_floating_ips_bulk,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ fake_fip_delete_data,
+ to_utf=bytes_body,
+ ip_range="192.168.1.0/24")
+
+ def test_list_floating_ips_bulk_with_str_body(self):
+ self._test_list_floating_ips_bulk()
+
+ def test_list_floating_ips_bulk_with_bytes_body(self):
+ self._test_list_floating_ips_bulk(True)
+
+ def test_create_floating_ips_bulk_with_str_body(self):
+ self._test_create_floating_ips_bulk()
+
+ def test_create_floating_ips_bulk_with_bytes_body(self):
+ self._test_create_floating_ips_bulk(True)
+
+ def test_delete_floating_ips_bulk_with_str_body(self):
+ self._test_delete_floating_ips_bulk()
+
+ def test_delete_floating_ips_bulk_with_bytes_body(self):
+ self._test_delete_floating_ips_bulk(True)
diff --git a/tempest/tests/lib/services/compute/test_floating_ips_client.py b/tempest/tests/lib/services/compute/test_floating_ips_client.py
new file mode 100644
index 0000000..950f9ce
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_floating_ips_client.py
@@ -0,0 +1,113 @@
+# Copyright 2015 IBM Corp.
+#
+# 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 fixtures
+
+from tempest.lib import exceptions as lib_exc
+from tempest.lib.services.compute import floating_ips_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestFloatingIpsClient(base.BaseServiceTest):
+
+ floating_ip = {"fixed_ip": None,
+ "id": "46d61064-13ba-4bf0-9557-69de824c3d6f",
+ "instance_id": "a1daa443-a6bb-463e-aea2-104b7d912eb8",
+ "ip": "10.10.10.1",
+ "pool": "nova"}
+
+ def setUp(self):
+ super(TestFloatingIpsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = floating_ips_client.FloatingIPsClient(
+ fake_auth, 'compute', 'regionOne')
+
+ def _test_list_floating_ips(self, bytes_body=False):
+ expected = {'floating_ips': [TestFloatingIpsClient.floating_ip]}
+ self.check_service_client_function(
+ self.client.list_floating_ips,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ expected,
+ bytes_body)
+
+ def test_list_floating_ips_str_body(self):
+ self._test_list_floating_ips(bytes_body=False)
+
+ def test_list_floating_ips_byte_body(self):
+ self._test_list_floating_ips(bytes_body=True)
+
+ def _test_show_floating_ip(self, bytes_body=False):
+ expected = {"floating_ip": TestFloatingIpsClient.floating_ip}
+ self.check_service_client_function(
+ self.client.show_floating_ip,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ expected,
+ bytes_body,
+ floating_ip_id='a1daa443-a6bb-463e-aea2-104b7d912eb8')
+
+ def test_show_floating_ip_str_body(self):
+ self._test_show_floating_ip(bytes_body=False)
+
+ def test_show_floating_ip_byte_body(self):
+ self._test_show_floating_ip(bytes_body=True)
+
+ def _test_create_floating_ip(self, bytes_body=False):
+ expected = {"floating_ip": TestFloatingIpsClient.floating_ip}
+ self.check_service_client_function(
+ self.client.create_floating_ip,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ expected,
+ bytes_body,
+ pool_name='nova')
+
+ def test_create_floating_ip_str_body(self):
+ self._test_create_floating_ip(bytes_body=False)
+
+ def test_create_floating_ip_byte_body(self):
+ self._test_create_floating_ip(bytes_body=True)
+
+ def test_delete_floating_ip(self):
+ self.check_service_client_function(
+ self.client.delete_floating_ip,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {}, status=202, floating_ip_id='fake-id')
+
+ def test_associate_floating_ip_to_server(self):
+ self.check_service_client_function(
+ self.client.associate_floating_ip_to_server,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {}, status=202, floating_ip='10.10.10.1',
+ server_id='c782b7a9-33cd-45f0-b795-7f87f456408b')
+
+ def test_disassociate_floating_ip_from_server(self):
+ self.check_service_client_function(
+ self.client.disassociate_floating_ip_from_server,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {}, status=202, floating_ip='10.10.10.1',
+ server_id='c782b7a9-33cd-45f0-b795-7f87f456408b')
+
+ def test_is_resource_deleted_true(self):
+ self.useFixture(fixtures.MockPatch(
+ 'tempest.lib.services.compute.floating_ips_client.'
+ 'FloatingIPsClient.show_floating_ip',
+ side_effect=lib_exc.NotFound()))
+ self.assertTrue(self.client.is_resource_deleted('fake-id'))
+
+ def test_is_resource_deleted_false(self):
+ self.useFixture(fixtures.MockPatch(
+ 'tempest.lib.services.compute.floating_ips_client.'
+ 'FloatingIPsClient.show_floating_ip',
+ return_value={"floating_ip": TestFloatingIpsClient.floating_ip}))
+ self.assertFalse(self.client.is_resource_deleted('fake-id'))
diff --git a/tempest/tests/lib/services/compute/test_hosts_client.py b/tempest/tests/lib/services/compute/test_hosts_client.py
new file mode 100644
index 0000000..78537c2
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_hosts_client.py
@@ -0,0 +1,147 @@
+# Copyright 2015 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.services.compute import hosts_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestHostsClient(base.BaseServiceTest):
+ FAKE_HOST_DATA = {
+ "host": {
+ "resource": {
+ "cpu": 1,
+ "disk_gb": 1028,
+ "host": "c1a7de0ac9d94e4baceae031d05caae3",
+ "memory_mb": 8192,
+ "project": "(total)"
+ }
+ },
+ "hosts": {
+ "host_name": "c1a7de0ac9d94e4baceae031d05caae3",
+ "service": "conductor",
+ "zone": "internal"
+ },
+ "enable_hosts": {
+ "host": "65c5d5b7e3bd44308e67fc50f362aee6",
+ "maintenance_mode": "off_maintenance",
+ "status": "enabled"
+ }
+ }
+
+ FAKE_CONTROL_DATA = {
+ "shutdown": {
+ "host": "c1a7de0ac9d94e4baceae031d05caae3",
+ "power_action": "shutdown"
+ },
+ "startup": {
+ "host": "c1a7de0ac9d94e4baceae031d05caae3",
+ "power_action": "startup"
+ },
+ "reboot": {
+ "host": "c1a7de0ac9d94e4baceae031d05caae3",
+ "power_action": "reboot"
+ }}
+
+ HOST_DATA = {'host': [FAKE_HOST_DATA['host']]}
+ HOSTS_DATA = {'hosts': [FAKE_HOST_DATA['hosts']]}
+ ENABLE_HOST_DATA = FAKE_HOST_DATA['enable_hosts']
+ HOST_ID = "c1a7de0ac9d94e4baceae031d05caae3"
+ TEST_HOST_DATA = {
+ "status": "enable",
+ "maintenance_mode": "disable"
+ }
+
+ def setUp(self):
+ super(TestHostsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = hosts_client.HostsClient(fake_auth, 'compute',
+ 'regionOne')
+ self.params = {'hostname': self.HOST_ID}
+ self.func2mock = {
+ 'get': 'tempest.lib.common.rest_client.RestClient.get',
+ 'put': 'tempest.lib.common.rest_client.RestClient.put'}
+
+ def _test_host_data(self, test_type='list', bytes_body=False):
+ expected_resp = self.HOST_DATA
+ if test_type != 'list':
+ function_call = self.client.show_host
+ else:
+ expected_resp = self.HOSTS_DATA
+ function_call = self.client.list_hosts
+ self.params = {'host_name': self.HOST_ID}
+
+ self.check_service_client_function(
+ function_call, self.func2mock['get'],
+ expected_resp, bytes_body,
+ 200, **self.params)
+
+ def _test_update_hosts(self, bytes_body=False):
+ expected_resp = self.ENABLE_HOST_DATA
+ self.check_service_client_function(
+ self.client.update_host, self.func2mock['put'],
+ expected_resp, bytes_body,
+ 200, **self.params)
+
+ def _test_control_host(self, control_op='reboot', bytes_body=False):
+ if control_op == 'start':
+ expected_resp = self.FAKE_CONTROL_DATA['startup']
+ function_call = self.client.startup_host
+ elif control_op == 'stop':
+ expected_resp = self.FAKE_CONTROL_DATA['shutdown']
+ function_call = self.client.shutdown_host
+ else:
+ expected_resp = self.FAKE_CONTROL_DATA['reboot']
+ function_call = self.client.reboot_host
+
+ self.check_service_client_function(
+ function_call, self.func2mock['get'],
+ expected_resp, bytes_body,
+ 200, **self.params)
+
+ def test_show_host_with_str_body(self):
+ self._test_host_data('show')
+
+ def test_show_host_with_bytes_body(self):
+ self._test_host_data('show', True)
+
+ def test_list_host_with_str_body(self):
+ self._test_host_data()
+
+ def test_list_host_with_bytes_body(self):
+ self._test_host_data(bytes_body=True)
+
+ def test_start_host_with_str_body(self):
+ self._test_control_host('start')
+
+ def test_start_host_with_bytes_body(self):
+ self._test_control_host('start', True)
+
+ def test_stop_host_with_str_body(self):
+ self._test_control_host('stop')
+
+ def test_stop_host_with_bytes_body(self):
+ self._test_control_host('stop', True)
+
+ def test_reboot_host_with_str_body(self):
+ self._test_control_host('reboot')
+
+ def test_reboot_host_with_bytes_body(self):
+ self._test_control_host('reboot', True)
+
+ def test_update_host_with_str_body(self):
+ self._test_update_hosts()
+
+ def test_update_host_with_bytes_body(self):
+ self._test_update_hosts(True)
diff --git a/tempest/tests/lib/services/compute/test_hypervisor_client.py b/tempest/tests/lib/services/compute/test_hypervisor_client.py
new file mode 100644
index 0000000..89eb698
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_hypervisor_client.py
@@ -0,0 +1,167 @@
+# Copyright 2015 IBM Corp.
+#
+# 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.compute import hypervisor_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestHypervisorClient(base.BaseServiceTest):
+
+ hypervisor_id = "1"
+ hypervisor_name = "hyper.hostname.com"
+
+ def setUp(self):
+ super(TestHypervisorClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = hypervisor_client.HypervisorClient(
+ fake_auth, 'compute', 'regionOne')
+
+ def test_list_hypervisor_str_body(self):
+ self._test_list_hypervisor(bytes_body=False)
+
+ def test_list_hypervisor_byte_body(self):
+ self._test_list_hypervisor(bytes_body=True)
+
+ def _test_list_hypervisor(self, bytes_body=False):
+ expected = {"hypervisors": [{
+ "id": 1,
+ "hypervisor_hostname": "hypervisor1.hostname.com"},
+ {
+ "id": 2,
+ "hypervisor_hostname": "hypervisor2.hostname.com"}]}
+ self.check_service_client_function(
+ self.client.list_hypervisors,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ expected, bytes_body)
+
+ def test_show_hypervisor_str_body(self):
+ self._test_show_hypervisor(bytes_body=False)
+
+ def test_show_hypervisor_byte_body(self):
+ self._test_show_hypervisor(bytes_body=True)
+
+ def _test_show_hypervisor(self, bytes_body=False):
+ expected = {"hypervisor": {
+ "cpu_info": "?",
+ "current_workload": 0,
+ "disk_available_least": 1,
+ "host_ip": "10.10.10.10",
+ "free_disk_gb": 1028,
+ "free_ram_mb": 7680,
+ "hypervisor_hostname": "fake-mini",
+ "hypervisor_type": "fake",
+ "hypervisor_version": 1,
+ "id": 1,
+ "local_gb": 1028,
+ "local_gb_used": 0,
+ "memory_mb": 8192,
+ "memory_mb_used": 512,
+ "running_vms": 0,
+ "service": {
+ "host": "fake_host",
+ "id": 2},
+ "vcpus": 1,
+ "vcpus_used": 0}}
+ self.check_service_client_function(
+ self.client.show_hypervisor,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ expected, bytes_body,
+ hypervisor_id=self.hypervisor_id)
+
+ def test_list_servers_on_hypervisor_str_body(self):
+ self._test_list_servers_on_hypervisor(bytes_body=False)
+
+ def test_list_servers_on_hypervisor_byte_body(self):
+ self._test_list_servers_on_hypervisor(bytes_body=True)
+
+ def _test_list_servers_on_hypervisor(self, bytes_body=False):
+ expected = {"hypervisors": [{
+ "id": 1,
+ "hypervisor_hostname": "hyper.hostname.com",
+ "servers": [{
+ "uuid": "e1ae8fc4-b72d-4c2f-a427-30dd420b6277",
+ "name": "instance-00000001"},
+ {
+ "uuid": "e1ae8fc4-b72d-4c2f-a427-30dd42066666",
+ "name": "instance-00000002"}
+ ]}
+ ]}
+ self.check_service_client_function(
+ self.client.list_servers_on_hypervisor,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ expected, bytes_body,
+ hypervisor_name=self.hypervisor_name)
+
+ def test_show_hypervisor_statistics_str_body(self):
+ self._test_show_hypervisor_statistics(bytes_body=False)
+
+ def test_show_hypervisor_statistics_byte_body(self):
+ self._test_show_hypervisor_statistics(bytes_body=True)
+
+ def _test_show_hypervisor_statistics(self, bytes_body=False):
+ expected = {
+ "hypervisor_statistics": {
+ "count": 1,
+ "current_workload": 0,
+ "disk_available_least": 0,
+ "free_disk_gb": 1028,
+ "free_ram_mb": 7680,
+ "local_gb": 1028,
+ "local_gb_used": 0,
+ "memory_mb": 8192,
+ "memory_mb_used": 512,
+ "running_vms": 0,
+ "vcpus": 1,
+ "vcpus_used": 0}}
+ self.check_service_client_function(
+ self.client.show_hypervisor_statistics,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ expected, bytes_body)
+
+ def test_show_hypervisor_uptime_str_body(self):
+ self._test_show_hypervisor_uptime(bytes_body=False)
+
+ def test_show_hypervisor_uptime_byte_body(self):
+ self._test_show_hypervisor_uptime(bytes_body=True)
+
+ def _test_show_hypervisor_uptime(self, bytes_body=False):
+ expected = {
+ "hypervisor": {
+ "hypervisor_hostname": "fake-mini",
+ "id": 1,
+ "uptime": (" 08:32:11 up 93 days, 18:25, 12 users, "
+ " load average: 0.20, 0.12, 0.14")
+ }}
+ self.check_service_client_function(
+ self.client.show_hypervisor_uptime,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ expected, bytes_body,
+ hypervisor_id=self.hypervisor_id)
+
+ def test_search_hypervisor_str_body(self):
+ self._test_search_hypervisor(bytes_body=False)
+
+ def test_search_hypervisor_byte_body(self):
+ self._test_search_hypervisor(bytes_body=True)
+
+ def _test_search_hypervisor(self, bytes_body=False):
+ expected = {"hypervisors": [{
+ "id": 2,
+ "hypervisor_hostname": "hyper.hostname.com"}]}
+ self.check_service_client_function(
+ self.client.search_hypervisor,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ expected, bytes_body,
+ hypervisor_name=self.hypervisor_name)
diff --git a/tempest/tests/lib/services/compute/test_images_client.py b/tempest/tests/lib/services/compute/test_images_client.py
new file mode 100644
index 0000000..c2c3b76
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_images_client.py
@@ -0,0 +1,264 @@
+# Copyright 2015 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.
+
+import copy
+
+import fixtures
+from tempest.lib import exceptions as lib_exc
+from tempest.lib.services.compute import images_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestImagesClient(base.BaseServiceTest):
+ # Data Dictionaries used for testing #
+ FAKE_IMAGE_METADATA = {
+ "list":
+ {"metadata": {
+ "auto_disk_config": "True",
+ "Label": "Changed"
+ }},
+ "set_item":
+ {"meta": {
+ "auto_disk_config": "True"
+ }},
+ "show_item":
+ {"meta": {
+ "kernel_id": "nokernel",
+ }},
+ "update":
+ {"metadata": {
+ "kernel_id": "False",
+ "Label": "UpdatedImage"
+ }},
+ "set":
+ {"metadata": {
+ "Label": "Changed",
+ "auto_disk_config": "True"
+ }},
+ "delete_item": {}
+ }
+
+ FAKE_IMAGE_DATA = {
+ "list":
+ {"images": [
+ {"id": "70a599e0-31e7-49b7-b260-868f441e862b",
+ "links": [
+ {"href": "http://openstack.example.com/v2/openstack" +
+ "/images/70a599e0-31e7-49b7-b260-868f441e862b",
+ "rel": "self"
+ }
+ ],
+ "name": "fakeimage7"
+ }]},
+ "show": {"image": {
+ "created": "2011-01-01T01:02:03Z",
+ "id": "70a599e0-31e7-49b7-b260-868f441e862b",
+ "links": [
+ {
+ "href": "http://openstack.example.com/v2/openstack" +
+ "/images/70a599e0-31e7-49b7-b260-868f441e862b",
+ "rel": "self"
+ },
+ ],
+ "metadata": {
+ "architecture": "x86_64",
+ "auto_disk_config": "True",
+ "kernel_id": "nokernel",
+ "ramdisk_id": "nokernel"
+ },
+ "minDisk": 0,
+ "minRam": 0,
+ "name": "fakeimage7",
+ "progress": 100,
+ "status": "ACTIVE",
+ "updated": "2011-01-01T01:02:03Z"}},
+ "create": {},
+ "delete": {}
+ }
+ func2mock = {
+ 'get': 'tempest.lib.common.rest_client.RestClient.get',
+ 'post': 'tempest.lib.common.rest_client.RestClient.post',
+ 'put': 'tempest.lib.common.rest_client.RestClient.put',
+ 'delete': 'tempest.lib.common.rest_client.RestClient.delete'}
+ # Variable definition
+ FAKE_IMAGE_ID = FAKE_IMAGE_DATA['show']['image']['id']
+ FAKE_SERVER_ID = "80a599e0-31e7-49b7-b260-868f441e343f"
+ FAKE_CREATE_INFO = {'location': 'None'}
+ FAKE_METADATA = FAKE_IMAGE_METADATA['show_item']['meta']
+
+ def setUp(self):
+ super(TestImagesClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = images_client.ImagesClient(fake_auth,
+ "compute", "regionOne")
+
+ def _test_image_operation(self, operation="delete", bytes_body=False):
+ response_code = 200
+ mock_operation = self.func2mock['get']
+ expected_op = self.FAKE_IMAGE_DATA[operation]
+ params = {"image_id": self.FAKE_IMAGE_ID}
+ headers = None
+ if operation == 'list':
+ function = self.client.list_images
+ elif operation == 'show':
+ function = self.client.show_image
+ elif operation == 'create':
+ function = self.client.create_image
+ mock_operation = self.func2mock['post']
+ params = {"server_id": self.FAKE_SERVER_ID}
+ response_code = 202
+ headers = {
+ 'connection': 'keep-alive',
+ 'content-length': '0',
+ 'content-type': 'application/json',
+ 'status': '202',
+ 'x-compute-request-id': 'req-fake',
+ 'vary': 'accept-encoding',
+ 'x-openstack-nova-api-version': 'v2.1',
+ 'date': '13 Oct 2015 05:55:36 GMT',
+ 'location': 'http://fake.com/images/fake'
+ }
+ else:
+ function = self.client.delete_image
+ mock_operation = self.func2mock['delete']
+ response_code = 204
+
+ self.check_service_client_function(
+ function, mock_operation, expected_op,
+ bytes_body, response_code, headers, **params)
+
+ def _test_image_metadata(self, operation="set_item", bytes_body=False):
+ response_code = 200
+ expected_op = self.FAKE_IMAGE_METADATA[operation]
+ if operation == 'list':
+ function = self.client.list_image_metadata
+ mock_operation = self.func2mock['get']
+ params = {"image_id": self.FAKE_IMAGE_ID}
+
+ elif operation == 'set':
+ function = self.client.set_image_metadata
+ mock_operation = self.func2mock['put']
+ params = {"image_id": "_dummy_data",
+ "meta": self.FAKE_METADATA}
+
+ elif operation == 'update':
+ function = self.client.update_image_metadata
+ mock_operation = self.func2mock['post']
+ params = {"image_id": self.FAKE_IMAGE_ID,
+ "meta": self.FAKE_METADATA}
+
+ elif operation == 'show_item':
+ mock_operation = self.func2mock['get']
+ function = self.client.show_image_metadata_item
+ params = {"image_id": self.FAKE_IMAGE_ID,
+ "key": "123"}
+
+ elif operation == 'delete_item':
+ function = self.client.delete_image_metadata_item
+ mock_operation = self.func2mock['delete']
+ response_code = 204
+ params = {"image_id": self.FAKE_IMAGE_ID,
+ "key": "123"}
+
+ else:
+ function = self.client.set_image_metadata_item
+ mock_operation = self.func2mock['put']
+ params = {"image_id": self.FAKE_IMAGE_ID,
+ "key": "123",
+ "meta": self.FAKE_METADATA}
+
+ self.check_service_client_function(
+ function, mock_operation, expected_op,
+ bytes_body, response_code, **params)
+
+ def _test_resource_deleted(self, bytes_body=False):
+ params = {"id": self.FAKE_IMAGE_ID}
+ expected_op = self.FAKE_IMAGE_DATA['show']
+ self.useFixture(fixtures.MockPatch('tempest.lib.services.compute'
+ '.images_client.ImagesClient.show_image',
+ side_effect=lib_exc.NotFound))
+ self.assertEqual(True, self.client.is_resource_deleted(**params))
+ tempdata = copy.deepcopy(self.FAKE_IMAGE_DATA['show'])
+ tempdata['image']['id'] = None
+ self.useFixture(fixtures.MockPatch('tempest.lib.services.compute'
+ '.images_client.ImagesClient.show_image',
+ return_value=expected_op))
+ self.assertEqual(False, self.client.is_resource_deleted(**params))
+
+ def test_list_images_with_str_body(self):
+ self._test_image_operation('list')
+
+ def test_list_images_with_bytes_body(self):
+ self._test_image_operation('list', True)
+
+ def test_show_image_with_str_body(self):
+ self._test_image_operation('show')
+
+ def test_show_image_with_bytes_body(self):
+ self._test_image_operation('show', True)
+
+ def test_create_image_with_str_body(self):
+ self._test_image_operation('create')
+
+ def test_create_image_with_bytes_body(self):
+ self._test_image_operation('create', True)
+
+ def test_delete_image_with_str_body(self):
+ self._test_image_operation('delete')
+
+ def test_delete_image_with_bytes_body(self):
+ self._test_image_operation('delete', True)
+
+ def test_list_image_metadata_with_str_body(self):
+ self._test_image_metadata('list')
+
+ def test_list_image_metadata_with_bytes_body(self):
+ self._test_image_metadata('list', True)
+
+ def test_set_image_metadata_with_str_body(self):
+ self._test_image_metadata('set')
+
+ def test_set_image_metadata_with_bytes_body(self):
+ self._test_image_metadata('set', True)
+
+ def test_update_image_metadata_with_str_body(self):
+ self._test_image_metadata('update')
+
+ def test_update_image_metadata_with_bytes_body(self):
+ self._test_image_metadata('update', True)
+
+ def test_set_image_metadata_item_with_str_body(self):
+ self._test_image_metadata()
+
+ def test_set_image_metadata_item_with_bytes_body(self):
+ self._test_image_metadata(bytes_body=True)
+
+ def test_show_image_metadata_item_with_str_body(self):
+ self._test_image_metadata('show_item')
+
+ def test_show_image_metadata_item_with_bytes_body(self):
+ self._test_image_metadata('show_item', True)
+
+ def test_delete_image_metadata_item_with_str_body(self):
+ self._test_image_metadata('delete_item')
+
+ def test_delete_image_metadata_item_with_bytes_body(self):
+ self._test_image_metadata('delete_item', True)
+
+ def test_resource_delete_with_str_body(self):
+ self._test_resource_deleted()
+
+ def test_resource_delete_with_bytes_body(self):
+ self._test_resource_deleted(True)
diff --git a/tempest/tests/lib/services/compute/test_instance_usage_audit_log_client.py b/tempest/tests/lib/services/compute/test_instance_usage_audit_log_client.py
new file mode 100644
index 0000000..21764ff
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_instance_usage_audit_log_client.py
@@ -0,0 +1,73 @@
+# Copyright 2015 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.
+
+import datetime
+
+from tempest.lib.services.compute import instance_usage_audit_log_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestInstanceUsagesAuditLogClient(base.BaseServiceTest):
+
+ FAKE_AUDIT_LOG = {
+ "hosts_not_run": [
+ "f4eb7cfd155f4574967f8b55a7faed75"
+ ],
+ "log": {},
+ "num_hosts": 1,
+ "num_hosts_done": 0,
+ "num_hosts_not_run": 1,
+ "num_hosts_running": 0,
+ "overall_status": "0 of 1 hosts done. 0 errors.",
+ "period_beginning": "2012-12-01 00:00:00",
+ "period_ending": "2013-01-01 00:00:00",
+ "total_errors": 0,
+ "total_instances": 0
+ }
+
+ def setUp(self):
+ super(TestInstanceUsagesAuditLogClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = (instance_usage_audit_log_client.
+ InstanceUsagesAuditLogClient(fake_auth, 'compute',
+ 'regionOne'))
+
+ def _test_list_instance_usage_audit_logs(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_instance_usage_audit_logs,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {"instance_usage_audit_logs": self.FAKE_AUDIT_LOG},
+ bytes_body)
+
+ def test_list_instance_usage_audit_logs_with_str_body(self):
+ self._test_list_instance_usage_audit_logs()
+
+ def test_list_instance_usage_audit_logs_with_bytes_body(self):
+ self._test_list_instance_usage_audit_logs(bytes_body=True)
+
+ def _test_show_instance_usage_audit_log(self, bytes_body=False):
+ before_time = datetime.datetime(2012, 12, 1, 0, 0)
+ self.check_service_client_function(
+ self.client.show_instance_usage_audit_log,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {"instance_usage_audit_log": self.FAKE_AUDIT_LOG},
+ bytes_body,
+ time_before=before_time)
+
+ def test_show_instance_usage_audit_log_with_str_body(self):
+ self._test_show_instance_usage_audit_log()
+
+ def test_show_network_with_bytes_body_with_bytes_body(self):
+ self._test_show_instance_usage_audit_log(bytes_body=True)
diff --git a/tempest/tests/lib/services/compute/test_interfaces_client.py b/tempest/tests/lib/services/compute/test_interfaces_client.py
new file mode 100644
index 0000000..4d78103
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_interfaces_client.py
@@ -0,0 +1,98 @@
+# Copyright 2015 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.services.compute import interfaces_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestInterfacesClient(base.BaseServiceTest):
+ # Data Values to be used for testing #
+ FAKE_INTERFACE_DATA = {
+ "fixed_ips": [{
+ "ip_address": "192.168.1.1",
+ "subnet_id": "f8a6e8f8-c2ec-497c-9f23-da9616de54ef"
+ }],
+ "mac_addr": "fa:16:3e:4c:2c:30",
+ "net_id": "3cb9bc59-5699-4588-a4b1-b87f96708bc6",
+ "port_id": "ce531f90-199f-48c0-816c-13e38010b442",
+ "port_state": "ACTIVE"}
+
+ FAKE_SHOW_DATA = {
+ "interfaceAttachment": FAKE_INTERFACE_DATA}
+ FAKE_LIST_DATA = {
+ "interfaceAttachments": [FAKE_INTERFACE_DATA]}
+
+ FAKE_SERVER_ID = "ec14c864-096e-4e27-bb8a-2c2b4dc6f3f5"
+ FAKE_PORT_ID = FAKE_SHOW_DATA['interfaceAttachment']['port_id']
+ func2mock = {
+ 'delete': 'tempest.lib.common.rest_client.RestClient.delete',
+ 'get': 'tempest.lib.common.rest_client.RestClient.get',
+ 'post': 'tempest.lib.common.rest_client.RestClient.post'}
+
+ def setUp(self):
+ super(TestInterfacesClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = interfaces_client.InterfacesClient(fake_auth,
+ "compute",
+ "regionOne")
+
+ def _test_interface_operation(self, operation="create", bytes_body=False):
+ response_code = 200
+ expected_op = self.FAKE_SHOW_DATA
+ mock_operation = self.func2mock['get']
+ params = {'server_id': self.FAKE_SERVER_ID,
+ 'port_id': self.FAKE_PORT_ID}
+ if operation == 'list':
+ expected_op = self.FAKE_LIST_DATA
+ function = self.client.list_interfaces
+ params = {'server_id': self.FAKE_SERVER_ID}
+ elif operation == 'show':
+ function = self.client.show_interface
+ elif operation == 'delete':
+ expected_op = {}
+ mock_operation = self.func2mock['delete']
+ function = self.client.delete_interface
+ response_code = 202
+ else:
+ function = self.client.create_interface
+ mock_operation = self.func2mock['post']
+
+ self.check_service_client_function(
+ function, mock_operation, expected_op,
+ bytes_body, response_code, **params)
+
+ def test_list_interfaces_with_str_body(self):
+ self._test_interface_operation('list')
+
+ def test_list_interfaces_with_bytes_body(self):
+ self._test_interface_operation('list', True)
+
+ def test_show_interface_with_str_body(self):
+ self._test_interface_operation('show')
+
+ def test_show_interface_with_bytes_body(self):
+ self._test_interface_operation('show', True)
+
+ def test_delete_interface_with_str_body(self):
+ self._test_interface_operation('delete')
+
+ def test_delete_interface_with_bytes_body(self):
+ self._test_interface_operation('delete', True)
+
+ def test_create_interface_with_str_body(self):
+ self._test_interface_operation()
+
+ def test_create_interface_with_bytes_body(self):
+ self._test_interface_operation(bytes_body=True)
diff --git a/tempest/tests/lib/services/compute/test_keypairs_client.py b/tempest/tests/lib/services/compute/test_keypairs_client.py
new file mode 100644
index 0000000..ed3b9dd
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_keypairs_client.py
@@ -0,0 +1,94 @@
+# Copyright 2015 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.
+
+import copy
+
+from tempest.lib.services.compute import keypairs_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestKeyPairsClient(base.BaseServiceTest):
+
+ FAKE_KEYPAIR = {"keypair": {
+ "public_key": "ssh-rsa foo Generated-by-Nova",
+ "name": u'\u2740(*\xb4\u25e1`*)\u2740',
+ "user_id": "525d55f98980415ba98e634972fa4a10",
+ "fingerprint": "76:24:66:49:d7:ca:6e:5c:77:ea:8e:bb:9c:15:5f:98"
+ }}
+
+ def setUp(self):
+ super(TestKeyPairsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = keypairs_client.KeyPairsClient(
+ fake_auth, 'compute', 'regionOne')
+
+ def _test_list_keypairs(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_keypairs,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {"keypairs": []},
+ bytes_body)
+
+ def test_list_keypairs_with_str_body(self):
+ self._test_list_keypairs()
+
+ def test_list_keypairs_with_bytes_body(self):
+ self._test_list_keypairs(bytes_body=True)
+
+ def _test_show_keypair(self, bytes_body=False):
+ fake_keypair = copy.deepcopy(self.FAKE_KEYPAIR)
+ fake_keypair["keypair"].update({
+ "deleted": False,
+ "created_at": "2015-07-22T04:53:52.000000",
+ "updated_at": None,
+ "deleted_at": None,
+ "id": 1
+ })
+
+ self.check_service_client_function(
+ self.client.show_keypair,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ fake_keypair,
+ bytes_body,
+ keypair_name="test")
+
+ def test_show_keypair_with_str_body(self):
+ self._test_show_keypair()
+
+ def test_show_keypair_with_bytes_body(self):
+ self._test_show_keypair(bytes_body=True)
+
+ def _test_create_keypair(self, bytes_body=False):
+ fake_keypair = copy.deepcopy(self.FAKE_KEYPAIR)
+ fake_keypair["keypair"].update({"private_key": "foo"})
+
+ self.check_service_client_function(
+ self.client.create_keypair,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ fake_keypair,
+ bytes_body,
+ name="test")
+
+ def test_create_keypair_with_str_body(self):
+ self._test_create_keypair()
+
+ def test_create_keypair_with_bytes_body(self):
+ self._test_create_keypair(bytes_body=True)
+
+ def test_delete_keypair(self):
+ self.check_service_client_function(
+ self.client.delete_keypair,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {}, status=202, keypair_name='test')
diff --git a/tempest/tests/lib/services/compute/test_limits_client.py b/tempest/tests/lib/services/compute/test_limits_client.py
new file mode 100644
index 0000000..5f3fa5a
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_limits_client.py
@@ -0,0 +1,66 @@
+# Copyright 2015 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.services.compute import limits_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestLimitsClient(base.BaseServiceTest):
+
+ def setUp(self):
+ super(TestLimitsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = limits_client.LimitsClient(
+ fake_auth, 'compute', 'regionOne')
+
+ def _test_show_limits(self, bytes_body=False):
+ expected = {
+ "limits": {
+ "rate": [],
+ "absolute": {
+ "maxServerMeta": 128,
+ "maxPersonality": 5,
+ "totalServerGroupsUsed": 0,
+ "maxImageMeta": 128,
+ "maxPersonalitySize": 10240,
+ "maxServerGroups": 10,
+ "maxSecurityGroupRules": 20,
+ "maxTotalKeypairs": 100,
+ "totalCoresUsed": 0,
+ "totalRAMUsed": 0,
+ "totalInstancesUsed": 0,
+ "maxSecurityGroups": 10,
+ "totalFloatingIpsUsed": 0,
+ "maxTotalCores": 20,
+ "totalSecurityGroupsUsed": 0,
+ "maxTotalFloatingIps": 10,
+ "maxTotalInstances": 10,
+ "maxTotalRAMSize": 51200,
+ "maxServerGroupMembers": 10
+ }
+ }
+ }
+
+ self.check_service_client_function(
+ self.client.show_limits,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ expected,
+ bytes_body)
+
+ def test_show_limits_with_str_body(self):
+ self._test_show_limits()
+
+ def test_show_limits_with_bytes_body(self):
+ self._test_show_limits(bytes_body=True)
diff --git a/tempest/tests/lib/services/compute/test_migrations_client.py b/tempest/tests/lib/services/compute/test_migrations_client.py
new file mode 100644
index 0000000..be62c0c
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_migrations_client.py
@@ -0,0 +1,52 @@
+# Copyright 2015 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.services.compute import migrations_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestMigrationsClient(base.BaseServiceTest):
+ FAKE_MIGRATION_INFO = {"migrations": [{
+ "created_at": "2012-10-29T13:42:02",
+ "dest_compute": "compute2",
+ "dest_host": "1.2.3.4",
+ "dest_node": "node2",
+ "id": 1234,
+ "instance_uuid": "e9e4fdd7-f956-44ff-bfeb-d654a96ab3a2",
+ "new_instance_type_id": 2,
+ "old_instance_type_id": 1,
+ "source_compute": "compute1",
+ "source_node": "node1",
+ "status": "finished",
+ "updated_at": "2012-10-29T13:42:02"}]}
+
+ def setUp(self):
+ super(TestMigrationsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.mg_client_obj = migrations_client.MigrationsClient(
+ fake_auth, 'compute', 'regionOne')
+
+ def _test_list_migrations(self, bytes_body=False):
+ self.check_service_client_function(
+ self.mg_client_obj.list_migrations,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_MIGRATION_INFO,
+ bytes_body)
+
+ def test_list_migration_with_str_body(self):
+ self._test_list_migrations()
+
+ def test_list_migration_with_bytes_body(self):
+ self._test_list_migrations(True)
diff --git a/tempest/tests/lib/services/compute/test_networks_client.py b/tempest/tests/lib/services/compute/test_networks_client.py
new file mode 100644
index 0000000..1908b57
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_networks_client.py
@@ -0,0 +1,94 @@
+# Copyright 2015 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.services.compute import networks_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestNetworksClient(base.BaseServiceTest):
+
+ FAKE_NETWORK = {
+ "bridge": None,
+ "vpn_public_port": None,
+ "dhcp_start": None,
+ "bridge_interface": None,
+ "share_address": None,
+ "updated_at": None,
+ "id": "34d5ae1e-5659-49cf-af80-73bccd7d7ad3",
+ "cidr_v6": None,
+ "deleted_at": None,
+ "gateway": None,
+ "rxtx_base": None,
+ "label": u'30d7',
+ "priority": None,
+ "project_id": None,
+ "vpn_private_address": None,
+ "deleted": None,
+ "vlan": None,
+ "broadcast": None,
+ "netmask": None,
+ "injected": None,
+ "cidr": None,
+ "vpn_public_address": None,
+ "multi_host": None,
+ "enable_dhcp": None,
+ "dns2": None,
+ "created_at": None,
+ "host": None,
+ "mtu": None,
+ "gateway_v6": None,
+ "netmask_v6": None,
+ "dhcp_server": None,
+ "dns1": None
+ }
+
+ network_id = "34d5ae1e-5659-49cf-af80-73bccd7d7ad3"
+
+ FAKE_NETWORKS = [FAKE_NETWORK]
+
+ def setUp(self):
+ super(TestNetworksClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = networks_client.NetworksClient(
+ fake_auth, 'compute', 'regionOne')
+
+ def _test_list_networks(self, bytes_body=False):
+ fake_list = {"networks": self.FAKE_NETWORKS}
+ self.check_service_client_function(
+ self.client.list_networks,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ fake_list,
+ bytes_body)
+
+ def test_list_networks_with_str_body(self):
+ self._test_list_networks()
+
+ def test_list_networks_with_bytes_body(self):
+ self._test_list_networks(bytes_body=True)
+
+ def _test_show_network(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_network,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {"network": self.FAKE_NETWORK},
+ bytes_body,
+ network_id=self.network_id
+ )
+
+ def test_show_network_with_str_body(self):
+ self._test_show_network()
+
+ def test_show_network_with_bytes_body(self):
+ self._test_show_network(bytes_body=True)
diff --git a/tempest/tests/lib/services/compute/test_quota_classes_client.py b/tempest/tests/lib/services/compute/test_quota_classes_client.py
new file mode 100644
index 0000000..22d8b91
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_quota_classes_client.py
@@ -0,0 +1,71 @@
+# Copyright 2015 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.
+
+import copy
+
+from tempest.lib.services.compute import quota_classes_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestQuotaClassesClient(base.BaseServiceTest):
+
+ FAKE_QUOTA_CLASS_SET = {
+ "injected_file_content_bytes": 10240,
+ "metadata_items": 128,
+ "server_group_members": 10,
+ "server_groups": 10,
+ "ram": 51200,
+ "floating_ips": 10,
+ "key_pairs": 100,
+ "id": u'\u2740(*\xb4\u25e1`*)\u2740',
+ "instances": 10,
+ "security_group_rules": 20,
+ "security_groups": 10,
+ "injected_files": 5,
+ "cores": 20,
+ "fixed_ips": -1,
+ "injected_file_path_bytes": 255,
+ }
+
+ def setUp(self):
+ super(TestQuotaClassesClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = quota_classes_client.QuotaClassesClient(
+ fake_auth, 'compute', 'regionOne')
+
+ def _test_show_quota_class_set(self, bytes_body=False):
+ fake_body = {'quota_class_set': self.FAKE_QUOTA_CLASS_SET}
+ self.check_service_client_function(
+ self.client.show_quota_class_set,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ fake_body,
+ bytes_body,
+ quota_class_id="test")
+
+ def test_show_quota_class_set_with_str_body(self):
+ self._test_show_quota_class_set()
+
+ def test_show_quota_class_set_with_bytes_body(self):
+ self._test_show_quota_class_set(bytes_body=True)
+
+ def test_update_quota_class_set(self):
+ fake_quota_class_set = copy.deepcopy(self.FAKE_QUOTA_CLASS_SET)
+ fake_quota_class_set.pop("id")
+ fake_body = {'quota_class_set': fake_quota_class_set}
+ self.check_service_client_function(
+ self.client.update_quota_class_set,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ fake_body,
+ quota_class_id="test")
diff --git a/tempest/tests/lib/services/compute/test_quotas_client.py b/tempest/tests/lib/services/compute/test_quotas_client.py
new file mode 100644
index 0000000..bbb8eb7
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_quotas_client.py
@@ -0,0 +1,146 @@
+# Copyright 2015 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.
+
+import copy
+
+from tempest.lib.services.compute import quotas_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestQuotasClient(base.BaseServiceTest):
+
+ FAKE_QUOTA_SET = {
+ "quota_set": {
+ "injected_file_content_bytes": 10240,
+ "metadata_items": 128,
+ "server_group_members": 10,
+ "server_groups": 10,
+ "ram": 51200,
+ "floating_ips": 10,
+ "key_pairs": 100,
+ "id": "8421f7be61064f50b680465c07f334af",
+ "instances": 10,
+ "security_group_rules": 20,
+ "injected_files": 5,
+ "cores": 20,
+ "fixed_ips": -1,
+ "injected_file_path_bytes": 255,
+ "security_groups": 10}
+ }
+
+ project_id = "8421f7be61064f50b680465c07f334af"
+ fake_user_id = "65f09168cbb04eb593f3138b63b67b67"
+
+ def setUp(self):
+ super(TestQuotasClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = quotas_client.QuotasClient(
+ fake_auth, 'compute', 'regionOne')
+
+ def _get_quota_set(self, detail):
+ if not detail:
+ return self.FAKE_QUOTA_SET
+ fake_quota_set = {"quota_set": {}}
+ for key, val in self.FAKE_QUOTA_SET['quota_set'].items():
+ fake_quota_set['quota_set'][key] = \
+ {'limit': val, 'reserved': 0, 'in_use': 0}
+ fake_quota_set['quota_set']['id'] = "8421f7be61064f50b680465c07f334af"
+ return fake_quota_set
+
+ def _test_show_quota_set(self, bytes_body=False, detail=False,
+ user_id=None):
+ if user_id:
+ self.check_service_client_function(
+ self.client.show_quota_set,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self._get_quota_set(detail),
+ to_utf=bytes_body,
+ tenant_id=self.project_id,
+ detail=detail,
+ user_id=user_id)
+ else:
+ self.check_service_client_function(
+ self.client.show_quota_set,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self._get_quota_set(detail),
+ to_utf=bytes_body,
+ tenant_id=self.project_id,
+ detail=detail)
+
+ def test_show_quota_set_with_str_body(self):
+ self._test_show_quota_set()
+
+ def test_show_quota_set_with_bytes_body(self):
+ self._test_show_quota_set(bytes_body=True)
+
+ def test_show_quota_set_for_user_with_str_body(self):
+ self._test_show_quota_set(user_id=self.fake_user_id)
+
+ def test_show_quota_set_for_user_with_bytes_body(self):
+ self._test_show_quota_set(bytes_body=True, user_id=self.fake_user_id)
+
+ def test_show_quota_set_with_details(self):
+ self._test_show_quota_set(detail=True)
+
+ def _test_show_default_quota_set(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_default_quota_set,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_QUOTA_SET,
+ to_utf=bytes_body,
+ tenant_id=self.project_id)
+
+ def test_show_default_quota_set_with_str_body(self):
+ self._test_show_default_quota_set()
+
+ def test_show_default_quota_set_with_bytes_body(self):
+ self._test_show_default_quota_set(bytes_body=True)
+
+ def _test_update_quota_set(self, bytes_body=False, user_id=None):
+ fake_quota_set = copy.deepcopy(self.FAKE_QUOTA_SET)
+ fake_quota_set['quota_set'].pop("id")
+ if user_id:
+ self.check_service_client_function(
+ self.client.update_quota_set,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ fake_quota_set,
+ to_utf=bytes_body,
+ tenant_id=self.project_id,
+ user_id=user_id)
+ else:
+ self.check_service_client_function(
+ self.client.update_quota_set,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ fake_quota_set,
+ to_utf=bytes_body,
+ tenant_id=self.project_id)
+
+ def test_update_quota_set_with_str_body(self):
+ self._test_update_quota_set()
+
+ def test_update_quota_set_with_bytes_body(self):
+ self._test_update_quota_set(bytes_body=True)
+
+ def test_update_quota_set_for_user_with_str_body(self):
+ self._test_update_quota_set(user_id=self.fake_user_id)
+
+ def test_update_quota_set_for_user_with_bytes_body(self):
+ self._test_update_quota_set(bytes_body=True, user_id=self.fake_user_id)
+
+ def test_delete_quota_set(self):
+ self.check_service_client_function(
+ self.client.delete_quota_set,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {}, status=202, tenant_id=self.project_id)
diff --git a/tempest/tests/lib/services/compute/test_security_group_default_rules_client.py b/tempest/tests/lib/services/compute/test_security_group_default_rules_client.py
new file mode 100644
index 0000000..86eee02
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_security_group_default_rules_client.py
@@ -0,0 +1,88 @@
+# Copyright 2015 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.services.compute import security_group_default_rules_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestSecurityGroupDefaultRulesClient(base.BaseServiceTest):
+ FAKE_RULE = {
+ "from_port": 80,
+ "id": 1,
+ "ip_protocol": "TCP",
+ "ip_range": {
+ "cidr": "10.10.10.0/24"
+ },
+ "to_port": 80
+ }
+
+ def setUp(self):
+ super(TestSecurityGroupDefaultRulesClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = (security_group_default_rules_client.
+ SecurityGroupDefaultRulesClient(fake_auth, 'compute',
+ 'regionOne'))
+
+ def _test_list_security_group_default_rules(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_security_group_default_rules,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {"security_group_default_rules": [self.FAKE_RULE]},
+ to_utf=bytes_body)
+
+ def test_list_security_group_default_rules_with_str_body(self):
+ self._test_list_security_group_default_rules()
+
+ def test_list_security_group_default_rules_with_bytes_body(self):
+ self._test_list_security_group_default_rules(bytes_body=True)
+
+ def _test_show_security_group_default_rule(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_security_group_default_rule,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {"security_group_default_rule": self.FAKE_RULE},
+ to_utf=bytes_body,
+ security_group_default_rule_id=1)
+
+ def test_show_security_group_default_rule_with_str_body(self):
+ self._test_show_security_group_default_rule()
+
+ def test_show_security_group_default_rule_with_bytes_body(self):
+ self._test_show_security_group_default_rule(bytes_body=True)
+
+ def _test_create_security_default_group_rule(self, bytes_body=False):
+ request_body = {
+ "to_port": 80,
+ "from_port": 80,
+ "ip_protocol": "TCP",
+ "cidr": "10.10.10.0/24"
+ }
+ self.check_service_client_function(
+ self.client.create_security_default_group_rule,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {"security_group_default_rule": self.FAKE_RULE},
+ to_utf=bytes_body, **request_body)
+
+ def test_create_security_default_group_rule_with_str_body(self):
+ self._test_create_security_default_group_rule()
+
+ def test_create_security_default_group_rule_with_bytes_body(self):
+ self._test_create_security_default_group_rule(bytes_body=True)
+
+ def test_delete_security_group_default_rule(self):
+ self.check_service_client_function(
+ self.client.delete_security_group_default_rule,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {}, status=204, security_group_default_rule_id=1)
diff --git a/tempest/tests/lib/services/compute/test_security_group_rules_client.py b/tempest/tests/lib/services/compute/test_security_group_rules_client.py
new file mode 100644
index 0000000..2b0a94d
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_security_group_rules_client.py
@@ -0,0 +1,66 @@
+# Copyright 2015 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.services.compute import security_group_rules_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestSecurityGroupRulesClient(base.BaseServiceTest):
+
+ FAKE_SECURITY_GROUP_RULE = {
+ "security_group_rule": {
+ "id": "2d021cf1-ce4b-4292-994f-7a785d62a144",
+ "ip_range": {
+ "cidr": "0.0.0.0/0"
+ },
+ "parent_group_id": "48700ff3-30b8-4e63-845f-a79c9633e9fb",
+ "to_port": 443,
+ "ip_protocol": "tcp",
+ "group": {},
+ "from_port": 443
+ }
+ }
+
+ def setUp(self):
+ super(TestSecurityGroupRulesClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = security_group_rules_client.SecurityGroupRulesClient(
+ fake_auth, 'compute', 'regionOne')
+
+ def _test_create_security_group_rule(self, bytes_body=False):
+ req_body = {
+ "from_port": "443",
+ "ip_protocol": "tcp",
+ "to_port": "443",
+ "cidr": "0.0.0.0/0",
+ "parent_group_id": "48700ff3-30b8-4e63-845f-a79c9633e9fb"
+ }
+ self.check_service_client_function(
+ self.client.create_security_group_rule,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_SECURITY_GROUP_RULE,
+ to_utf=bytes_body, **req_body)
+
+ def test_create_security_group_rule_with_str_body(self):
+ self._test_create_security_group_rule()
+
+ def test_create_security_group_rule_with_bytes_body(self):
+ self._test_create_security_group_rule(bytes_body=True)
+
+ def test_delete_security_group_rule(self):
+ self.check_service_client_function(
+ self.client.delete_security_group_rule,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {}, status=202, group_rule_id='group-id')
diff --git a/tempest/tests/lib/services/compute/test_security_groups_client.py b/tempest/tests/lib/services/compute/test_security_groups_client.py
new file mode 100644
index 0000000..7bbf20e
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_security_groups_client.py
@@ -0,0 +1,113 @@
+# Copyright 2015 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.
+
+import fixtures
+
+from tempest.lib import exceptions as lib_exc
+from tempest.lib.services.compute import security_groups_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestSecurityGroupsClient(base.BaseServiceTest):
+
+ FAKE_SECURITY_GROUP_INFO = [{
+ "description": "default",
+ "id": "3fb26eb3-581b-4420-9963-b0879a026506",
+ "name": "default",
+ "rules": [],
+ "tenant_id": "openstack"
+ }]
+
+ def setUp(self):
+ super(TestSecurityGroupsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = security_groups_client.SecurityGroupsClient(
+ fake_auth, 'compute', 'regionOne')
+
+ def _test_list_security_groups(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_security_groups,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {"security_groups": self.FAKE_SECURITY_GROUP_INFO},
+ to_utf=bytes_body)
+
+ def test_list_security_groups_with_str_body(self):
+ self._test_list_security_groups()
+
+ def test_list_security_groups_with_bytes_body(self):
+ self._test_list_security_groups(bytes_body=True)
+
+ def _test_show_security_group(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_security_group,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {"security_group": self.FAKE_SECURITY_GROUP_INFO[0]},
+ to_utf=bytes_body,
+ security_group_id='fake-id')
+
+ def test_show_security_group_with_str_body(self):
+ self._test_show_security_group()
+
+ def test_show_security_group_with_bytes_body(self):
+ self._test_show_security_group(bytes_body=True)
+
+ def _test_create_security_group(self, bytes_body=False):
+ post_body = {"name": "test", "description": "test_group"}
+ self.check_service_client_function(
+ self.client.create_security_group,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {"security_group": self.FAKE_SECURITY_GROUP_INFO[0]},
+ to_utf=bytes_body,
+ kwargs=post_body)
+
+ def test_create_security_group_with_str_body(self):
+ self._test_create_security_group()
+
+ def test_create_security_group_with_bytes_body(self):
+ self._test_create_security_group(bytes_body=True)
+
+ def _test_update_security_group(self, bytes_body=False):
+ req_body = {"name": "test", "description": "test_group"}
+ self.check_service_client_function(
+ self.client.update_security_group,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ {"security_group": self.FAKE_SECURITY_GROUP_INFO[0]},
+ to_utf=bytes_body,
+ security_group_id='fake-id',
+ kwargs=req_body)
+
+ def test_update_security_group_with_str_body(self):
+ self._test_update_security_group()
+
+ def test_update_security_group_with_bytes_body(self):
+ self._test_update_security_group(bytes_body=True)
+
+ def test_delete_security_group(self):
+ self.check_service_client_function(
+ self.client.delete_security_group,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {}, status=202, security_group_id='fake-id')
+
+ def test_is_resource_deleted_true(self):
+ mod = ('tempest.lib.services.compute.security_groups_client.'
+ 'SecurityGroupsClient.show_security_group')
+ self.useFixture(fixtures.MockPatch(mod, side_effect=lib_exc.NotFound))
+ self.assertTrue(self.client.is_resource_deleted('fake-id'))
+
+ def test_is_resource_deleted_false(self):
+ mod = ('tempest.lib.services.compute.security_groups_client.'
+ 'SecurityGroupsClient.show_security_group')
+ self.useFixture(fixtures.MockPatch(mod, return_value='success'))
+ self.assertFalse(self.client.is_resource_deleted('fake-id'))
diff --git a/tempest/tests/lib/services/compute/test_server_groups_client.py b/tempest/tests/lib/services/compute/test_server_groups_client.py
new file mode 100644
index 0000000..9055a36
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_server_groups_client.py
@@ -0,0 +1,101 @@
+# Copyright 2015 IBM Corp.
+#
+# 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 fixtures
+
+from tempest.lib.services.compute import base_compute_client
+from tempest.lib.services.compute import server_groups_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib import fake_http
+from tempest.tests.lib.services import base
+
+
+class TestServerGroupsClient(base.BaseServiceTest):
+
+ server_group = {
+ "id": "5bbcc3c4-1da2-4437-a48a-66f15b1b13f9",
+ "name": "test",
+ "policies": ["anti-affinity"],
+ "members": [],
+ "metadata": {}}
+
+ def setUp(self):
+ super(TestServerGroupsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = server_groups_client.ServerGroupsClient(
+ fake_auth, 'compute', 'regionOne')
+
+ def _test_create_server_group(self, bytes_body=False):
+ expected = {"server_group": self.server_group}
+ self.check_service_client_function(
+ self.client.create_server_group,
+ 'tempest.lib.common.rest_client.RestClient.post', expected,
+ bytes_body, name='fake-group', policies=['affinity'])
+
+ def test_create_server_group_str_body(self):
+ self._test_create_server_group(bytes_body=False)
+
+ def test_create_server_group_byte_body(self):
+ self._test_create_server_group(bytes_body=True)
+
+ def test_delete_server_group(self):
+ response = fake_http.fake_http_response({}, status=204), ''
+ self.useFixture(fixtures.MockPatch(
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ return_value=response))
+ self.client.delete_server_group('fake-group')
+
+ def _test_list_server_groups(self, bytes_body=False):
+ expected = {"server_groups": [self.server_group]}
+ self.check_service_client_function(
+ self.client.list_server_groups,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ expected, bytes_body)
+
+ def test_list_server_groups_str_body(self):
+ self._test_list_server_groups(bytes_body=False)
+
+ def test_list_server_groups_byte_body(self):
+ self._test_list_server_groups(bytes_body=True)
+
+ def _test_show_server_group(self, bytes_body=False):
+ expected = {"server_group": self.server_group}
+ self.check_service_client_function(
+ self.client.show_server_group,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ expected, bytes_body,
+ server_group_id='5bbcc3c4-1da2-4437-a48a-66f15b1b13f9')
+
+ def test_show_server_group_str_body(self):
+ self._test_show_server_group(bytes_body=False)
+
+ def test_show_server_group_byte_body(self):
+ self._test_show_server_group(bytes_body=True)
+
+
+class TestServerGroupsClientMinV213(TestServerGroupsClient):
+
+ server_group = {
+ "id": "5bbcc3c4-1da2-4437-a48a-66f15b1b13f9",
+ "name": "test",
+ "policies": ["anti-affinity"],
+ "members": [],
+ "metadata": {},
+ "project_id": "0beb4bffb7a445eb8eb05fee3ee7660a",
+ "user_id": "86031628064a4f99bb66ec03c507dcd8"}
+
+ def setUp(self):
+ super(TestServerGroupsClientMinV213, self).setUp()
+ self.patchobject(base_compute_client, 'COMPUTE_MICROVERSION',
+ new='2.13')
diff --git a/tempest/tests/lib/services/compute/test_servers_client.py b/tempest/tests/lib/services/compute/test_servers_client.py
new file mode 100644
index 0000000..86f6ad5
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_servers_client.py
@@ -0,0 +1,1076 @@
+# Copyright 2015 IBM Corp.
+# Copyright 2017 AT&T Corp.
+#
+# 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
+
+import mock
+
+from tempest.lib.services.compute import base_compute_client
+from tempest.lib.services.compute import servers_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestServersClient(base.BaseServiceTest):
+
+ FAKE_SERVERS = {'servers': [{
+ "id": "616fb98f-46ca-475e-917e-2563e5a8cd19",
+ "links": [
+ {
+ "href": "http://os.co/v2/616fb98f-46ca-475e-917e-2563e5a8cd19",
+ "rel": "self"
+ },
+ {
+ "href": "http://os.co/616fb98f-46ca-475e-917e-2563e5a8cd19",
+ "rel": "bookmark"
+ }
+ ],
+ "name": u"new\u1234-server-test"}]
+ }
+
+ FAKE_SERVER_DIAGNOSTICS = {
+ "cpu0_time": 17300000000,
+ "memory": 524288,
+ "vda_errors": -1,
+ "vda_read": 262144,
+ "vda_read_req": 112,
+ "vda_write": 5778432,
+ "vda_write_req": 488,
+ "vnet1_rx": 2070139,
+ "vnet1_rx_drop": 0,
+ "vnet1_rx_errors": 0,
+ "vnet1_rx_packets": 26701,
+ "vnet1_tx": 140208,
+ "vnet1_tx_drop": 0,
+ "vnet1_tx_errors": 0,
+ "vnet1_tx_packets": 662
+ }
+
+ FAKE_SERVER_GET = {'server': {
+ "accessIPv4": "",
+ "accessIPv6": "",
+ "addresses": {
+ "private": [
+ {
+ "addr": "192.168.0.3",
+ "version": 4
+ }
+ ]
+ },
+ "created": "2012-08-20T21:11:09Z",
+ "flavor": {
+ "id": "1",
+ "links": [
+ {
+ "href": "http://os.com/openstack/flavors/1",
+ "rel": "bookmark"
+ }
+ ]
+ },
+ "hostId": "65201c14a29663e06d0748e561207d998b343e1d164bfa0aafa9c45d",
+ "id": "893c7791-f1df-4c3d-8383-3caae9656c62",
+ "image": {
+ "id": "70a599e0-31e7-49b7-b260-868f441e862b",
+ "links": [
+ {
+ "href": "http://imgs/70a599e0-31e7-49b7-b260-868f441e862b",
+ "rel": "bookmark"
+ }
+ ]
+ },
+ "links": [
+ {
+ "href": "http://v2/srvs/893c7791-f1df-4c3d-8383-3caae9656c62",
+ "rel": "self"
+ },
+ {
+ "href": "http://srvs/893c7791-f1df-4c3d-8383-3caae9656c62",
+ "rel": "bookmark"
+ }
+ ],
+ "metadata": {
+ u"My Server N\u1234me": u"Apa\u1234che1"
+ },
+ "name": u"new\u1234-server-test",
+ "progress": 0,
+ "status": "ACTIVE",
+ "tenant_id": "openstack",
+ "updated": "2012-08-20T21:11:09Z",
+ "user_id": "fake"}
+ }
+
+ FAKE_SERVER_POST = {"server": {
+ "id": "616fb98f-46ca-475e-917e-2563e5a8cd19",
+ "adminPass": "fake-admin-pass",
+ "security_groups": [
+ 'fake-security-group-1',
+ 'fake-security-group-2'
+ ],
+ "links": [
+ {
+ "href": "http://os.co/v2/616fb98f-46ca-475e-917e-2563e5a8cd19",
+ "rel": "self"
+ },
+ {
+ "href": "http://os.co/616fb98f-46ca-475e-917e-2563e5a8cd19",
+ "rel": "bookmark"
+ }
+ ],
+ "OS-DCF:diskConfig": "fake-disk-config"}
+ }
+
+ FAKE_ADDRESS = {"addresses": {
+ "private": [
+ {
+ "addr": "192.168.0.3",
+ "version": 4
+ }
+ ]}
+ }
+
+ FAKE_COMMON_VOLUME = {
+ "id": "a6b0875b-6b5d-4a5a-81eb-0c3aa62e5fdb",
+ "device": "fake-device",
+ "volumeId": "a6b0875b-46ca-475e-917e-0c3aa62e5fdb",
+ "serverId": "616fb98f-46ca-475e-917e-2563e5a8cd19"
+ }
+
+ FAKE_VIRTUAL_INTERFACES = {
+ "id": "a6b0875b-46ca-475e-917e-0c3aa62e5fdb",
+ "mac_address": "00:25:90:5b:f8:c3",
+ "OS-EXT-VIF-NET:net_id": "fake-os-net-id"
+ }
+
+ FAKE_INSTANCE_ACTIONS = {
+ "action": "fake-action",
+ "request_id": "16fb98f-46ca-475e-917e-2563e5a8cd19",
+ "user_id": "16fb98f-46ca-475e-917e-2563e5a8cd12",
+ "project_id": "16fb98f-46ca-475e-917e-2563e5a8cd34",
+ "start_time": "2016-10-02T10:00:00-05:00",
+ "message": "fake-msg",
+ "instance_uuid": "16fb98f-46ca-475e-917e-2563e5a8cd12"
+ }
+
+ FAKE_VNC_CONSOLE = {
+ "type": "fake-type",
+ "url": "http://os.co/v2/616fb98f-46ca-475e-917e-2563e5a8cd19"
+ }
+
+ FAKE_SERVER_PASSWORD = {
+ "adminPass": "fake-password",
+ }
+
+ FAKE_INSTANCE_ACTION_EVENTS = {
+ "event": "fake-event",
+ "start_time": "2016-10-02T10:00:00-05:00",
+ "finish_time": "2016-10-02T10:00:00-05:00",
+ "result": "fake-result",
+ "traceback": "fake-trace-back"
+ }
+
+ FAKE_SECURITY_GROUPS = [{
+ "description": "default",
+ "id": "3fb26eb3-581b-4420-9963-b0879a026506",
+ "name": "default",
+ "rules": [],
+ "tenant_id": "openstack"
+ }]
+
+ FAKE_INSTANCE_WITH_EVENTS = copy.deepcopy(FAKE_INSTANCE_ACTIONS)
+ FAKE_INSTANCE_WITH_EVENTS['events'] = [FAKE_INSTANCE_ACTION_EVENTS]
+
+ FAKE_REBUILD_SERVER = copy.deepcopy(FAKE_SERVER_GET)
+ FAKE_REBUILD_SERVER['server']['adminPass'] = 'fake-admin-pass'
+
+ FAKE_TAGS = ["foo", "bar"]
+ REPLACE_FAKE_TAGS = ["baz", "qux"]
+
+ server_id = FAKE_SERVER_GET['server']['id']
+ network_id = 'a6b0875b-6b5d-4a5a-81eb-0c3aa62e5fdb'
+
+ def setUp(self):
+ super(TestServersClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = servers_client.ServersClient(
+ fake_auth, 'compute', 'regionOne')
+ self.addCleanup(mock.patch.stopall)
+
+ def test_list_servers_with_str_body(self):
+ self._test_list_servers()
+
+ def test_list_servers_with_bytes_body(self):
+ self._test_list_servers(bytes_body=True)
+
+ def _test_list_servers(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_servers,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_SERVERS,
+ bytes_body)
+
+ def test_show_server_with_str_body(self):
+ self._test_show_server()
+
+ def test_show_server_with_bytes_body(self):
+ self._test_show_server(bytes_body=True)
+
+ def _test_show_server(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_server,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_SERVER_GET,
+ bytes_body,
+ server_id=self.server_id
+ )
+
+ def test_delete_server(self):
+ self.check_service_client_function(
+ self.client.delete_server,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ status=204,
+ server_id=self.server_id
+ )
+
+ def test_create_server_with_str_body(self):
+ self._test_create_server()
+
+ def test_create_server_with_bytes_body(self):
+ self._test_create_server(True)
+
+ def _test_create_server(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_server,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_SERVER_POST,
+ bytes_body,
+ status=202,
+ name='fake-name',
+ imageRef='fake-image-ref',
+ flavorRef='fake-flavor-ref'
+ )
+
+ def test_list_addresses_with_str_body(self):
+ self._test_list_addresses()
+
+ def test_list_addresses_with_bytes_body(self):
+ self._test_list_addresses(True)
+
+ def _test_list_addresses(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_addresses,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_ADDRESS,
+ bytes_body,
+ server_id=self.server_id
+ )
+
+ def test_list_addresses_by_network_with_str_body(self):
+ self._test_list_addresses_by_network()
+
+ def test_list_addresses_by_network_with_bytes_body(self):
+ self._test_list_addresses_by_network(True)
+
+ def _test_list_addresses_by_network(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_addresses_by_network,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_ADDRESS['addresses'],
+ bytes_body,
+ server_id=self.server_id,
+ network_id=self.network_id
+ )
+
+ def test_action_with_str_body(self):
+ self._test_action()
+
+ def test_action_with_bytes_body(self):
+ self._test_action(True)
+
+ def _test_action(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.action,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ bytes_body,
+ server_id=self.server_id,
+ action_name='fake-action-name',
+ schema={'status_code': 200}
+ )
+
+ def test_create_backup_with_str_body(self):
+ self._test_create_backup()
+
+ def test_create_backup_with_bytes_body(self):
+ self._test_create_backup(True)
+
+ def _test_create_backup(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_backup,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ bytes_body,
+ status=202,
+ server_id=self.server_id,
+ backup_type='fake-backup',
+ rotation='fake-rotation',
+ name='fake-name'
+ )
+
+ def test_evacuate_server_with_str_body(self):
+ self._test_evacuate_server()
+
+ def test_evacuate_server_with_bytes_body(self):
+ self._test_evacuate_server(bytes_body=True)
+
+ def _test_evacuate_server(self, bytes_body=False):
+ kwargs = {'server_id': self.server_id,
+ 'host': 'fake-target-host'}
+ self.check_service_client_function(
+ self.client.evacuate_server,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_SERVER_PASSWORD,
+ bytes_body,
+ **kwargs)
+
+ def test_change_password_with_str_body(self):
+ self._test_change_password()
+
+ def test_change_password_with_bytes_body(self):
+ self._test_change_password(True)
+
+ def _test_change_password(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.change_password,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ bytes_body,
+ status=202,
+ server_id=self.server_id,
+ adminPass='fake-admin-pass'
+ )
+
+ def test_show_password_with_str_body(self):
+ self._test_show_password()
+
+ def test_show_password_with_bytes_body(self):
+ self._test_show_password(True)
+
+ def _test_show_password(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_password,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {'password': 'fake-password'},
+ bytes_body,
+ server_id=self.server_id
+ )
+
+ def test_delete_password(self):
+ self.check_service_client_function(
+ self.client.delete_password,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ status=204,
+ server_id=self.server_id
+ )
+
+ def test_reboot_server(self):
+ self.check_service_client_function(
+ self.client.reboot_server,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ status=202,
+ server_id=self.server_id,
+ type='fake-reboot-type'
+ )
+
+ def test_rebuild_server_with_str_body(self):
+ self._test_rebuild_server()
+
+ def test_rebuild_server_with_bytes_body(self):
+ self._test_rebuild_server(True)
+
+ def _test_rebuild_server(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.rebuild_server,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_REBUILD_SERVER,
+ bytes_body,
+ status=202,
+ server_id=self.server_id,
+ image_ref='fake-image-ref'
+ )
+
+ def test_resize_server(self):
+ self.check_service_client_function(
+ self.client.resize_server,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ status=202,
+ server_id=self.server_id,
+ flavor_ref='fake-flavor-ref'
+ )
+
+ def test_confirm_resize_server(self):
+ self.check_service_client_function(
+ self.client.confirm_resize_server,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ status=204,
+ server_id=self.server_id
+ )
+
+ def test_revert_resize(self):
+ self.check_service_client_function(
+ self.client.revert_resize_server,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ status=202,
+ server_id=self.server_id
+ )
+
+ def test_list_server_metadata_with_str_body(self):
+ self._test_list_server_metadata()
+
+ def test_list_server_metadata_with_bytes_body(self):
+ self._test_list_server_metadata()
+
+ def _test_list_server_metadata(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_server_metadata,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {'metadata': {'fake-key': 'fake-meta-data'}},
+ bytes_body,
+ server_id=self.server_id
+ )
+
+ def test_set_server_metadata_with_str_body(self):
+ self._test_set_server_metadata()
+
+ def test_set_server_metadata_with_bytes_body(self):
+ self._test_set_server_metadata(True)
+
+ def _test_set_server_metadata(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.set_server_metadata,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ {'metadata': {'fake-key': 'fake-meta-data'}},
+ bytes_body,
+ server_id=self.server_id,
+ meta='fake-meta'
+ )
+
+ def test_update_server_metadata_with_str_body(self):
+ self._test_update_server_metadata()
+
+ def test_update_server_metadata_with_bytes_body(self):
+ self._test_update_server_metadata(True)
+
+ def _test_update_server_metadata(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_server_metadata,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {'metadata': {'fake-key': 'fake-meta-data'}},
+ bytes_body,
+ server_id=self.server_id,
+ meta='fake-meta'
+ )
+
+ def test_show_server_metadata_item_with_str_body(self):
+ self._test_show_server_metadata()
+
+ def test_show_server_metadata_item_with_bytes_body(self):
+ self._test_show_server_metadata(True)
+
+ def _test_show_server_metadata(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_server_metadata_item,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {'meta': {'fake-key': 'fake-meta-data'}},
+ bytes_body,
+ server_id=self.server_id,
+ key='fake-key'
+ )
+
+ def test_set_server_metadata_item_with_str_body(self):
+ self._test_set_server_metadata_item()
+
+ def test_set_server_metadata_item_with_bytes_body(self):
+ self._test_set_server_metadata_item(True)
+
+ def _test_set_server_metadata_item(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.set_server_metadata_item,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ {'meta': {'fake-key': 'fake-meta-data'}},
+ bytes_body,
+ server_id=self.server_id,
+ key='fake-key',
+ meta='fake-meta'
+ )
+
+ def test_delete_server_metadata(self):
+ self.check_service_client_function(
+ self.client.delete_server_metadata_item,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ status=204,
+ server_id=self.server_id,
+ key='fake-key'
+ )
+
+ def test_stop_server(self):
+ self.check_service_client_function(
+ self.client.stop_server,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ status=202,
+ server_id=self.server_id
+ )
+
+ def test_start_server(self):
+ self.check_service_client_function(
+ self.client.start_server,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ status=202,
+ server_id=self.server_id
+ )
+
+ def test_attach_volume_with_str_body(self):
+ self._test_attach_volume_server()
+
+ def test_attach_volume_with_bytes_body(self):
+ self._test_attach_volume_server(True)
+
+ def _test_attach_volume_server(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.attach_volume,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {'volumeAttachment': self.FAKE_COMMON_VOLUME},
+ bytes_body,
+ server_id=self.server_id
+ )
+
+ def test_update_attached_volume(self):
+ self.check_service_client_function(
+ self.client.update_attached_volume,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ {},
+ status=202,
+ server_id=self.server_id,
+ attachment_id='fake-attachment-id',
+ volumeId='fake-volume-id'
+ )
+
+ def test_detach_volume_with_str_body(self):
+ self._test_detach_volume_server()
+
+ def test_detach_volume_with_bytes_body(self):
+ self._test_detach_volume_server(True)
+
+ def _test_detach_volume_server(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.detach_volume,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ bytes_body,
+ status=202,
+ server_id=self.server_id,
+ volume_id=self.FAKE_COMMON_VOLUME['volumeId']
+ )
+
+ def test_show_volume_attachment_with_str_body(self):
+ self._test_show_volume_attachment()
+
+ def test_show_volume_attachment_with_bytes_body(self):
+ self._test_show_volume_attachment(True)
+
+ def _test_show_volume_attachment(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_volume_attachment,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {'volumeAttachment': self.FAKE_COMMON_VOLUME},
+ bytes_body,
+ server_id=self.server_id,
+ volume_id=self.FAKE_COMMON_VOLUME['volumeId']
+ )
+
+ def test_list_volume_attachments_with_str_body(self):
+ self._test_list_volume_attachments()
+
+ def test_list_volume_attachments_with_bytes_body(self):
+ self._test_list_volume_attachments(True)
+
+ def _test_list_volume_attachments(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_volume_attachments,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {'volumeAttachments': [self.FAKE_COMMON_VOLUME]},
+ bytes_body,
+ server_id=self.server_id
+ )
+
+ def test_add_security_group_with_str_body(self):
+ self._test_add_security_group()
+
+ def test_add_security_group_with_bytes_body(self):
+ self._test_add_security_group(True)
+
+ def _test_add_security_group(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.add_security_group,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ bytes_body,
+ status=202,
+ server_id=self.server_id,
+ name='fake-name'
+ )
+
+ def test_remove_security_group(self):
+ self.check_service_client_function(
+ self.client.remove_security_group,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ status=202,
+ server_id=self.server_id,
+ name='fake-name'
+ )
+
+ def test_live_migrate_server_with_str_body(self):
+ self._test_live_migrate_server()
+
+ def test_live_migrate_server_with_bytes_body(self):
+ self._test_live_migrate_server(True)
+
+ def _test_live_migrate_server(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.live_migrate_server,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ bytes_body,
+ status=202,
+ server_id=self.server_id
+ )
+
+ def test_migrate_server_with_str_body(self):
+ self._test_migrate_server()
+
+ def test_migrate_server_with_bytes_body(self):
+ self._test_migrate_server(True)
+
+ def _test_migrate_server(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.migrate_server,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ bytes_body,
+ status=202,
+ server_id=self.server_id
+ )
+
+ def test_lock_server(self):
+ self.check_service_client_function(
+ self.client.lock_server,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ status=202,
+ server_id=self.server_id
+ )
+
+ def test_unlock_server(self):
+ self.check_service_client_function(
+ self.client.unlock_server,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ status=202,
+ server_id=self.server_id
+ )
+
+ def test_suspend_server(self):
+ self.check_service_client_function(
+ self.client.suspend_server,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ status=202,
+ server_id=self.server_id
+ )
+
+ def test_resume_server(self):
+ self.check_service_client_function(
+ self.client.resume_server,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ status=202,
+ server_id=self.server_id
+ )
+
+ def test_pause_server(self):
+ self.check_service_client_function(
+ self.client.pause_server,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ status=202,
+ server_id=self.server_id
+ )
+
+ def test_unpause_server(self):
+ self.check_service_client_function(
+ self.client.unpause_server,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ status=202,
+ server_id=self.server_id
+ )
+
+ def test_reset_state(self):
+ self.check_service_client_function(
+ self.client.reset_state,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ status=202,
+ server_id=self.server_id,
+ state='fake-state'
+ )
+
+ def test_shelve_server(self):
+ self.check_service_client_function(
+ self.client.shelve_server,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ status=202,
+ server_id=self.server_id
+ )
+
+ def test_unshelve_server(self):
+ self.check_service_client_function(
+ self.client.unshelve_server,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ status=202,
+ server_id=self.server_id
+ )
+
+ def test_shelve_offload_server(self):
+ self.check_service_client_function(
+ self.client.shelve_offload_server,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ status=202,
+ server_id=self.server_id
+ )
+
+ def test_get_console_output_with_str_body(self):
+ self._test_get_console_output()
+
+ def test_get_console_output_with_bytes_body(self):
+ self._test_get_console_output(True)
+
+ def _test_get_console_output(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.get_console_output,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {'output': 'fake-output'},
+ bytes_body,
+ server_id=self.server_id,
+ length='fake-length'
+ )
+
+ def test_list_virtual_interfaces_with_str_body(self):
+ self._test_list_virtual_interfaces()
+
+ def test_list_virtual_interfaces_with_bytes_body(self):
+ self._test_list_virtual_interfaces(True)
+
+ def _test_list_virtual_interfaces(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_virtual_interfaces,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {'virtual_interfaces': [self.FAKE_VIRTUAL_INTERFACES]},
+ bytes_body,
+ server_id=self.server_id
+ )
+
+ def test_rescue_server_with_str_body(self):
+ self._test_rescue_server()
+
+ def test_rescue_server_with_bytes_body(self):
+ self._test_rescue_server(True)
+
+ def _test_rescue_server(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.rescue_server,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {'adminPass': 'fake-admin-pass'},
+ bytes_body,
+ server_id=self.server_id
+ )
+
+ def test_unrescue_server(self):
+ self.check_service_client_function(
+ self.client.unrescue_server,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ status=202,
+ server_id=self.server_id
+ )
+
+ def test_show_server_diagnostics_with_str_body(self):
+ self._test_show_server_diagnostics()
+
+ def test_show_server_diagnostics_with_bytes_body(self):
+ self._test_show_server_diagnostics(True)
+
+ def _test_show_server_diagnostics(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_server_diagnostics,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_SERVER_DIAGNOSTICS,
+ bytes_body,
+ status=200,
+ server_id=self.server_id
+ )
+
+ def test_list_instance_actions_with_str_body(self):
+ self._test_list_instance_actions()
+
+ def test_list_instance_actions_with_bytes_body(self):
+ self._test_list_instance_actions(True)
+
+ def _test_list_instance_actions(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_instance_actions,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {'instanceActions': [self.FAKE_INSTANCE_ACTIONS]},
+ bytes_body,
+ server_id=self.server_id
+ )
+
+ def test_show_instance_action_with_str_body(self):
+ self._test_show_instance_action()
+
+ def test_show_instance_action_with_bytes_body(self):
+ self._test_show_instance_action(True)
+
+ def _test_show_instance_action(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_instance_action,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {'instanceAction': self.FAKE_INSTANCE_WITH_EVENTS},
+ bytes_body,
+ server_id=self.server_id,
+ request_id='fake-request-id'
+ )
+
+ def test_force_delete_server(self):
+ self.check_service_client_function(
+ self.client.force_delete_server,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ status=202,
+ server_id=self.server_id
+ )
+
+ def test_restore_soft_deleted_server(self):
+ self.check_service_client_function(
+ self.client.restore_soft_deleted_server,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ status=202,
+ server_id=self.server_id
+ )
+
+ def test_reset_network(self):
+ self.check_service_client_function(
+ self.client.reset_network,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ status=202,
+ server_id=self.server_id
+ )
+
+ def test_inject_network_info(self):
+ self.check_service_client_function(
+ self.client.inject_network_info,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ status=202,
+ server_id=self.server_id
+ )
+
+ def test_get_vnc_console_with_str_body(self):
+ self._test_get_vnc_console()
+
+ def test_get_vnc_console_with_bytes_body(self):
+ self._test_get_vnc_console(True)
+
+ def _test_get_vnc_console(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.get_vnc_console,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {'console': self.FAKE_VNC_CONSOLE},
+ bytes_body,
+ server_id=self.server_id,
+ type='fake-console-type'
+ )
+
+ def test_list_security_groups_by_server_with_str_body(self):
+ self._test_list_security_groups_by_server()
+
+ def test_list_security_groups_by_server_with_bytes_body(self):
+ self._test_list_security_groups_by_server(True)
+
+ def _test_list_security_groups_by_server(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_security_groups_by_server,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {'security_groups': self.FAKE_SECURITY_GROUPS},
+ bytes_body,
+ server_id=self.server_id
+ )
+
+ @mock.patch.object(base_compute_client, 'COMPUTE_MICROVERSION',
+ new_callable=mock.PropertyMock(return_value='2.26'))
+ def test_list_tags_str_body(self, _):
+ self._test_list_tags()
+
+ @mock.patch.object(base_compute_client, 'COMPUTE_MICROVERSION',
+ new_callable=mock.PropertyMock(return_value='2.26'))
+ def test_list_tags_byte_body(self, _):
+ self._test_list_tags(bytes_body=True)
+
+ def _test_list_tags(self, bytes_body=False):
+ expected = {"tags": self.FAKE_TAGS}
+ self.check_service_client_function(
+ self.client.list_tags,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ expected,
+ server_id=self.server_id,
+ to_utf=bytes_body)
+
+ @mock.patch.object(base_compute_client, 'COMPUTE_MICROVERSION',
+ new_callable=mock.PropertyMock(return_value='2.26'))
+ def test_update_all_tags_str_body(self, _):
+ self._test_update_all_tags()
+
+ @mock.patch.object(base_compute_client, 'COMPUTE_MICROVERSION',
+ new_callable=mock.PropertyMock(return_value='2.26'))
+ def test_update_all_tags_byte_body(self, _):
+ self._test_update_all_tags(bytes_body=True)
+
+ def _test_update_all_tags(self, bytes_body=False):
+ expected = {"tags": self.REPLACE_FAKE_TAGS}
+ self.check_service_client_function(
+ self.client.update_all_tags,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ expected,
+ server_id=self.server_id,
+ tags=self.REPLACE_FAKE_TAGS,
+ to_utf=bytes_body)
+
+ @mock.patch.object(base_compute_client, 'COMPUTE_MICROVERSION',
+ new_callable=mock.PropertyMock(return_value='2.26'))
+ def test_delete_all_tags(self, _):
+ self.check_service_client_function(
+ self.client.delete_all_tags,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ server_id=self.server_id,
+ status=204)
+
+ @mock.patch.object(base_compute_client, 'COMPUTE_MICROVERSION',
+ new_callable=mock.PropertyMock(return_value='2.26'))
+ def test_check_tag_existence_str_body(self, _):
+ self._test_check_tag_existence()
+
+ @mock.patch.object(base_compute_client, 'COMPUTE_MICROVERSION',
+ new_callable=mock.PropertyMock(return_value='2.26'))
+ def test_check_tag_existence_byte_body(self, _):
+ self._test_check_tag_existence(bytes_body=True)
+
+ def _test_check_tag_existence(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.check_tag_existence,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {},
+ server_id=self.server_id,
+ tag=self.FAKE_TAGS[0],
+ status=204,
+ to_utf=bytes_body)
+
+ @mock.patch.object(base_compute_client, 'COMPUTE_MICROVERSION',
+ new_callable=mock.PropertyMock(return_value='2.26'))
+ def test_update_tag_str_body(self, _):
+ self._test_update_tag()
+
+ @mock.patch.object(base_compute_client, 'COMPUTE_MICROVERSION',
+ new_callable=mock.PropertyMock(return_value='2.26'))
+ def test_update_tag_byte_body(self, _):
+ self._test_update_tag(bytes_body=True)
+
+ def _test_update_tag(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_tag,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ {},
+ server_id=self.server_id,
+ tag=self.FAKE_TAGS[0],
+ status=201,
+ headers={'location': 'fake_location'},
+ to_utf=bytes_body)
+
+ @mock.patch.object(base_compute_client, 'COMPUTE_MICROVERSION',
+ new_callable=mock.PropertyMock(return_value='2.26'))
+ def test_delete_tag(self, _):
+ self.check_service_client_function(
+ self.client.delete_tag,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ server_id=self.server_id,
+ tag=self.FAKE_TAGS[0],
+ status=204,
+ )
+
+
+class TestServersClientMinV26(base.BaseServiceTest):
+
+ def setUp(self):
+ super(TestServersClientMinV26, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = servers_client.ServersClient(fake_auth, 'compute',
+ 'regionOne')
+ base_compute_client.COMPUTE_MICROVERSION = '2.6'
+ self.server_id = "920eaac8-a284-4fd1-9c2c-b30f0181b125"
+
+ def tearDown(self):
+ super(TestServersClientMinV26, self).tearDown()
+ base_compute_client.COMPUTE_MICROVERSION = None
+
+ def test_get_remote_consoles(self):
+ self.check_service_client_function(
+ self.client.get_remote_console,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {
+ 'remote_console': {
+ 'protocol': 'serial',
+ 'type': 'serial',
+ 'url': 'ws://127.0.0.1:6083/?token=IllAllowIt'
+ }
+ },
+ server_id=self.server_id,
+ console_type='serial',
+ protocol='serial',
+ )
diff --git a/tempest/tests/lib/services/compute/test_services_client.py b/tempest/tests/lib/services/compute/test_services_client.py
new file mode 100644
index 0000000..2dd981c
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_services_client.py
@@ -0,0 +1,146 @@
+# Copyright 2015 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.
+
+import copy
+
+import mock
+
+from tempest.lib.services.compute import base_compute_client
+from tempest.lib.services.compute import services_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestServicesClient(base.BaseServiceTest):
+
+ FAKE_SERVICES = {
+ "services":
+ [{
+ "status": "enabled",
+ "binary": "nova-conductor",
+ "zone": "internal",
+ "state": "up",
+ "updated_at": "2015-08-19T06:50:55.000000",
+ "host": "controller",
+ "disabled_reason": None,
+ "id": 1
+ }]
+ }
+
+ FAKE_SERVICE = {
+ "service":
+ {
+ "status": "enabled",
+ "binary": "nova-conductor",
+ "host": "controller"
+ }
+ }
+
+ FAKE_UPDATE_FORCED_DOWN = {
+ "service":
+ {
+ "forced_down": True,
+ "binary": "nova-conductor",
+ "host": "controller"
+ }
+ }
+
+ def setUp(self):
+ super(TestServicesClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = services_client.ServicesClient(
+ fake_auth, 'compute', 'regionOne')
+ self.addCleanup(mock.patch.stopall)
+
+ def test_list_services_with_str_body(self):
+ self.check_service_client_function(
+ self.client.list_services,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_SERVICES)
+
+ def test_list_services_with_bytes_body(self):
+ self.check_service_client_function(
+ self.client.list_services,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_SERVICES, to_utf=True)
+
+ def _test_enable_service(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.enable_service,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ self.FAKE_SERVICE,
+ bytes_body,
+ host="nova-conductor", binary="controller")
+
+ def test_enable_service_with_str_body(self):
+ self._test_enable_service()
+
+ def test_enable_service_with_bytes_body(self):
+ self._test_enable_service(bytes_body=True)
+
+ def _test_disable_service(self, bytes_body=False):
+ fake_service = copy.deepcopy(self.FAKE_SERVICE)
+ fake_service["service"]["status"] = "disable"
+
+ self.check_service_client_function(
+ self.client.disable_service,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ fake_service,
+ bytes_body,
+ host="nova-conductor", binary="controller")
+
+ def test_disable_service_with_str_body(self):
+ self._test_disable_service()
+
+ def test_disable_service_with_bytes_body(self):
+ self._test_disable_service(bytes_body=True)
+
+ def _test_log_reason_disabled_service(self, bytes_body=False):
+ resp_body = copy.deepcopy(self.FAKE_SERVICE)
+ resp_body['service']['disabled_reason'] = 'test reason'
+
+ self.check_service_client_function(
+ self.client.disable_log_reason,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ resp_body,
+ bytes_body,
+ host="nova-conductor",
+ binary="controller",
+ disabled_reason='test reason')
+
+ def test_log_reason_disabled_service_with_str_body(self):
+ self._test_log_reason_disabled_service()
+
+ def test_log_reason_disabled_service_with_bytes_body(self):
+ self._test_log_reason_disabled_service(bytes_body=True)
+
+ def _test_update_forced_down(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_forced_down,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ self.FAKE_UPDATE_FORCED_DOWN,
+ bytes_body,
+ host="nova-conductor",
+ binary="controller",
+ forced_down=True)
+
+ @mock.patch.object(base_compute_client, 'COMPUTE_MICROVERSION',
+ new_callable=mock.PropertyMock(return_value='2.11'))
+ def test_update_forced_down_with_str_body(self, _):
+ self._test_update_forced_down()
+
+ @mock.patch.object(base_compute_client, 'COMPUTE_MICROVERSION',
+ new_callable=mock.PropertyMock(return_value='2.11'))
+ def test_update_forced_down_with_bytes_body(self, _):
+ self._test_update_forced_down(bytes_body=True)
diff --git a/tempest/tests/lib/services/compute/test_snapshots_client.py b/tempest/tests/lib/services/compute/test_snapshots_client.py
new file mode 100644
index 0000000..1e2902c
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_snapshots_client.py
@@ -0,0 +1,103 @@
+# Copyright 2015 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.
+
+import fixtures
+
+from tempest.lib import exceptions as lib_exc
+from tempest.lib.services.compute import snapshots_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestSnapshotsClient(base.BaseServiceTest):
+
+ FAKE_SNAPSHOT = {
+ "createdAt": "2015-10-02T16:27:54.724209",
+ "displayDescription": u"Another \u1234.",
+ "displayName": u"v\u1234-001",
+ "id": "100",
+ "size": 100,
+ "status": "available",
+ "volumeId": "12"
+ }
+
+ FAKE_SNAPSHOTS = {"snapshots": [FAKE_SNAPSHOT]}
+
+ def setUp(self):
+ super(TestSnapshotsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = snapshots_client.SnapshotsClient(
+ fake_auth, 'compute', 'regionOne')
+
+ def _test_create_snapshot(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_snapshot,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {"snapshot": self.FAKE_SNAPSHOT},
+ to_utf=bytes_body, status=200,
+ volume_id=self.FAKE_SNAPSHOT["volumeId"])
+
+ def test_create_snapshot_with_str_body(self):
+ self._test_create_snapshot()
+
+ def test_create_shapshot_with_bytes_body(self):
+ self._test_create_snapshot(bytes_body=True)
+
+ def _test_show_snapshot(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_snapshot,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {"snapshot": self.FAKE_SNAPSHOT},
+ to_utf=bytes_body, snapshot_id=self.FAKE_SNAPSHOT["id"])
+
+ def test_show_snapshot_with_str_body(self):
+ self._test_show_snapshot()
+
+ def test_show_snapshot_with_bytes_body(self):
+ self._test_show_snapshot(bytes_body=True)
+
+ def _test_list_snapshots(self, bytes_body=False, **params):
+ self.check_service_client_function(
+ self.client.list_snapshots,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_SNAPSHOTS, to_utf=bytes_body, **params)
+
+ def test_list_snapshots_with_str_body(self):
+ self._test_list_snapshots()
+
+ def test_list_snapshots_with_byte_body(self):
+ self._test_list_snapshots(bytes_body=True)
+
+ def test_list_snapshots_with_params(self):
+ self._test_list_snapshots('fake')
+
+ def test_delete_snapshot(self):
+ self.check_service_client_function(
+ self.client.delete_snapshot,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {}, status=202, snapshot_id=self.FAKE_SNAPSHOT['id'])
+
+ def test_is_resource_deleted_true(self):
+ module = ('tempest.lib.services.compute.snapshots_client.'
+ 'SnapshotsClient.show_snapshot')
+ self.useFixture(fixtures.MockPatch(
+ module, side_effect=lib_exc.NotFound))
+ self.assertTrue(self.client.is_resource_deleted('fake-id'))
+
+ def test_is_resource_deleted_false(self):
+ module = ('tempest.lib.services.compute.snapshots_client.'
+ 'SnapshotsClient.show_snapshot')
+ self.useFixture(fixtures.MockPatch(
+ module, return_value={}))
+ self.assertFalse(self.client.is_resource_deleted('fake-id'))
diff --git a/tempest/tests/lib/services/compute/test_tenant_networks_client.py b/tempest/tests/lib/services/compute/test_tenant_networks_client.py
new file mode 100644
index 0000000..f71aad9
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_tenant_networks_client.py
@@ -0,0 +1,63 @@
+# Copyright 2015 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.services.compute import tenant_networks_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestTenantNetworksClient(base.BaseServiceTest):
+
+ FAKE_NETWORK = {
+ "cidr": "None",
+ "id": "c2329eb4-cc8e-4439-ac4c-932369309e36",
+ "label": u'\u30d7'
+ }
+
+ FAKE_NETWORKS = [FAKE_NETWORK]
+
+ NETWORK_ID = FAKE_NETWORK['id']
+
+ def setUp(self):
+ super(TestTenantNetworksClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = tenant_networks_client.TenantNetworksClient(
+ fake_auth, 'compute', 'regionOne')
+
+ def _test_list_tenant_networks(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_tenant_networks,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {"networks": self.FAKE_NETWORKS},
+ bytes_body)
+
+ def test_list_tenant_networks_with_str_body(self):
+ self._test_list_tenant_networks()
+
+ def test_list_tenant_networks_with_bytes_body(self):
+ self._test_list_tenant_networks(bytes_body=True)
+
+ def _test_show_tenant_network(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_tenant_network,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {"network": self.FAKE_NETWORK},
+ bytes_body,
+ network_id=self.NETWORK_ID)
+
+ def test_show_tenant_network_with_str_body(self):
+ self._test_show_tenant_network()
+
+ def test_show_tenant_network_with_bytes_body(self):
+ self._test_show_tenant_network(bytes_body=True)
diff --git a/tempest/tests/lib/services/compute/test_tenant_usages_client.py b/tempest/tests/lib/services/compute/test_tenant_usages_client.py
new file mode 100644
index 0000000..49eae08
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_tenant_usages_client.py
@@ -0,0 +1,79 @@
+# Copyright 2015 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.services.compute import tenant_usages_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestTenantUsagesClient(base.BaseServiceTest):
+
+ FAKE_SERVER_USAGES = [{
+ "ended_at": None,
+ "flavor": "m1.tiny",
+ "hours": 1.0,
+ "instance_id": "1f1deceb-17b5-4c04-84c7-e0d4499c8fe0",
+ "local_gb": 1,
+ "memory_mb": 512,
+ "name": "new-server-test",
+ "started_at": "2012-10-08T20:10:44.541277",
+ "state": "active",
+ "tenant_id": "openstack",
+ "uptime": 3600,
+ "vcpus": 1
+ }]
+
+ FAKE_TENANT_USAGES = [{
+ "server_usages": FAKE_SERVER_USAGES,
+ "start": "2012-10-08T21:10:44.587336",
+ "stop": "2012-10-08T22:10:44.587336",
+ "tenant_id": "openstack",
+ "total_hours": 1,
+ "total_local_gb_usage": 1,
+ "total_memory_mb_usage": 512,
+ "total_vcpus_usage": 1
+ }]
+
+ def setUp(self):
+ super(TestTenantUsagesClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = tenant_usages_client.TenantUsagesClient(
+ fake_auth, 'compute', 'regionOne')
+
+ def _test_list_tenant_usages(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_tenant_usages,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {"tenant_usages": self.FAKE_TENANT_USAGES},
+ to_utf=bytes_body)
+
+ def test_list_tenant_usages_with_str_body(self):
+ self._test_list_tenant_usages()
+
+ def test_list_tenant_usages_with_bytes_body(self):
+ self._test_list_tenant_usages(bytes_body=True)
+
+ def _test_show_tenant_usage(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_tenant_usage,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {"tenant_usage": self.FAKE_TENANT_USAGES[0]},
+ to_utf=bytes_body,
+ tenant_id='openstack')
+
+ def test_show_tenant_usage_with_str_body(self):
+ self._test_show_tenant_usage()
+
+ def test_show_tenant_usage_with_bytes_body(self):
+ self._test_show_tenant_usage(bytes_body=True)
diff --git a/tempest/tests/lib/services/compute/test_versions_client.py b/tempest/tests/lib/services/compute/test_versions_client.py
new file mode 100644
index 0000000..40d424f
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_versions_client.py
@@ -0,0 +1,136 @@
+# Copyright 2015 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.
+
+import copy
+
+import fixtures
+
+from tempest.lib.services.compute import versions_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestVersionsClient(base.BaseServiceTest):
+
+ FAKE_INIT_VERSION = {
+ "version": {
+ "id": "v2.1",
+ "links": [
+ {
+ "href": "http://openstack.example.com/v2.1/",
+ "rel": "self"
+ },
+ {
+ "href": "http://docs.openstack.org/",
+ "rel": "describedby",
+ "type": "text/html"
+ }
+ ],
+ "status": "CURRENT",
+ "updated": "2013-07-23T11:33:21Z",
+ "version": "2.1",
+ "min_version": "2.1"
+ }
+ }
+
+ FAKE_VERSIONS_INFO = {
+ "versions": [FAKE_INIT_VERSION["version"]]
+ }
+
+ FAKE_VERSION_INFO = copy.deepcopy(FAKE_INIT_VERSION)
+
+ FAKE_VERSION_INFO["version"]["media-types"] = [
+ {
+ "base": "application/json",
+ "type": "application/vnd.openstack.compute+json;version=2.1"
+ }
+ ]
+
+ def setUp(self):
+ super(TestVersionsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.versions_client = (
+ versions_client.VersionsClient
+ (fake_auth, 'compute', 'regionOne'))
+
+ def _test_versions_client(self, bytes_body=False):
+ self.check_service_client_function(
+ self.versions_client.list_versions,
+ 'tempest.lib.common.rest_client.RestClient.raw_request',
+ self.FAKE_VERSIONS_INFO,
+ bytes_body,
+ 200)
+
+ def _test_get_version_by_url(self, bytes_body=False):
+ self.useFixture(fixtures.MockPatch(
+ "tempest.lib.common.rest_client.RestClient.token",
+ return_value="Dummy Token"))
+ params = {"version_url": self.versions_client._get_base_version_url()}
+ self.check_service_client_function(
+ self.versions_client.get_version_by_url,
+ 'tempest.lib.common.rest_client.RestClient.raw_request',
+ self.FAKE_VERSION_INFO,
+ bytes_body,
+ 200, **params)
+
+ def test_list_versions_client_with_str_body(self):
+ self._test_versions_client()
+
+ def test_list_versions_client_with_bytes_body(self):
+ self._test_versions_client(bytes_body=True)
+
+ def test_get_version_by_url_with_str_body(self):
+ self._test_get_version_by_url()
+
+ def test_get_version_by_url_with_bytes_body(self):
+ self._test_get_version_by_url(bytes_body=True)
+
+ def _test_get_base_version_url(self, url, expected_base_url):
+ auth = fake_auth_provider.FakeAuthProvider(fake_base_url=url)
+ client = versions_client.VersionsClient(auth, 'compute', 'regionOne')
+ self.assertEqual(expected_base_url, client._get_base_version_url())
+
+ def test_get_base_version_url(self):
+ self._test_get_base_version_url('https://bar.org/v2/123',
+ 'https://bar.org/')
+ self._test_get_base_version_url('https://bar.org/v2.1/123',
+ 'https://bar.org/')
+ self._test_get_base_version_url('https://bar.org/v2.15/123',
+ 'https://bar.org/')
+ self._test_get_base_version_url('https://bar.org/v22.2/123',
+ 'https://bar.org/')
+ self._test_get_base_version_url('https://bar.org/v22/123',
+ 'https://bar.org/')
+
+ def test_get_base_version_url_app_name(self):
+ self._test_get_base_version_url('https://bar.org/compute/v2/123',
+ 'https://bar.org/compute/')
+ self._test_get_base_version_url('https://bar.org/compute/v2.1/123',
+ 'https://bar.org/compute/')
+ self._test_get_base_version_url('https://bar.org/compute/v2.15/123',
+ 'https://bar.org/compute/')
+ self._test_get_base_version_url('https://bar.org/compute/v22.2/123',
+ 'https://bar.org/compute/')
+ self._test_get_base_version_url('https://bar.org/compute/v22/123',
+ 'https://bar.org/compute/')
+
+ def test_get_base_version_url_double_slash(self):
+ self._test_get_base_version_url('https://bar.org//v2/123',
+ 'https://bar.org/')
+ self._test_get_base_version_url('https://bar.org//v2.1/123',
+ 'https://bar.org/')
+ self._test_get_base_version_url('https://bar.org/compute//v2/123',
+ 'https://bar.org/compute/')
+ self._test_get_base_version_url('https://bar.org/compute//v2.1/123',
+ 'https://bar.org/compute/')
diff --git a/tempest/tests/lib/services/compute/test_volumes_client.py b/tempest/tests/lib/services/compute/test_volumes_client.py
new file mode 100644
index 0000000..4b4f02e
--- /dev/null
+++ b/tempest/tests/lib/services/compute/test_volumes_client.py
@@ -0,0 +1,114 @@
+# Copyright 2015 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.
+
+import copy
+
+import fixtures
+
+from tempest.lib import exceptions as lib_exc
+from tempest.lib.services.compute import volumes_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestVolumesClient(base.BaseServiceTest):
+
+ FAKE_VOLUME = {
+ "id": "521752a6-acf6-4b2d-bc7a-119f9148cd8c",
+ "displayName": u"v\u12345ol-001",
+ "displayDescription": u"Another \u1234volume.",
+ "size": 30,
+ "status": "Active",
+ "volumeType": "289da7f8-6440-407c-9fb4-7db01ec49164",
+ "metadata": {
+ "contents": "junk"
+ },
+ "availabilityZone": "us-east1",
+ "snapshotId": None,
+ "attachments": [],
+ "createdAt": "2012-02-14T20:53:07Z"
+ }
+
+ FAKE_VOLUMES = {"volumes": [FAKE_VOLUME]}
+
+ def setUp(self):
+ super(TestVolumesClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = volumes_client.VolumesClient(
+ fake_auth, 'compute', 'regionOne')
+
+ def _test_list_volumes(self, bytes_body=False, **params):
+ self.check_service_client_function(
+ self.client.list_volumes,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_VOLUMES, to_utf=bytes_body, **params)
+
+ def test_list_volumes_with_str_body(self):
+ self._test_list_volumes()
+
+ def test_list_volumes_with_byte_body(self):
+ self._test_list_volumes(bytes_body=True)
+
+ def test_list_volumes_with_params(self):
+ self._test_list_volumes(name='fake')
+
+ def _test_show_volume(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_volume,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {"volume": self.FAKE_VOLUME},
+ to_utf=bytes_body, volume_id=self.FAKE_VOLUME['id'])
+
+ def test_show_volume_with_str_body(self):
+ self._test_show_volume()
+
+ def test_show_volume_with_bytes_body(self):
+ self._test_show_volume(bytes_body=True)
+
+ def _test_create_volume(self, bytes_body=False):
+ post_body = copy.deepcopy(self.FAKE_VOLUME)
+ del post_body['id']
+ del post_body['createdAt']
+ del post_body['status']
+ self.check_service_client_function(
+ self.client.create_volume,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {"volume": self.FAKE_VOLUME},
+ to_utf=bytes_body, status=200, **post_body)
+
+ def test_create_volume_with_str_body(self):
+ self._test_create_volume()
+
+ def test_create_volume_with_bytes_body(self):
+ self._test_create_volume(bytes_body=True)
+
+ def test_delete_volume(self):
+ self.check_service_client_function(
+ self.client.delete_volume,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {}, status=202, volume_id=self.FAKE_VOLUME['id'])
+
+ def test_is_resource_deleted_true(self):
+ module = ('tempest.lib.services.compute.volumes_client.'
+ 'VolumesClient.show_volume')
+ self.useFixture(fixtures.MockPatch(
+ module, side_effect=lib_exc.NotFound))
+ self.assertTrue(self.client.is_resource_deleted('fake-id'))
+
+ def test_is_resource_deleted_false(self):
+ module = ('tempest.lib.services.compute.volumes_client.'
+ 'VolumesClient.show_volume')
+ self.useFixture(fixtures.MockPatch(
+ module, return_value={}))
+ self.assertFalse(self.client.is_resource_deleted('fake-id'))
diff --git a/tempest/services/identity/__init__.py b/tempest/tests/lib/services/identity/__init__.py
similarity index 100%
rename from tempest/services/identity/__init__.py
rename to tempest/tests/lib/services/identity/__init__.py
diff --git a/tempest/services/identity/v2/__init__.py b/tempest/tests/lib/services/identity/v2/__init__.py
similarity index 100%
rename from tempest/services/identity/v2/__init__.py
rename to tempest/tests/lib/services/identity/v2/__init__.py
diff --git a/tempest/tests/lib/services/identity/v2/test_endpoints_client.py b/tempest/tests/lib/services/identity/v2/test_endpoints_client.py
new file mode 100644
index 0000000..7d2cac2
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v2/test_endpoints_client.py
@@ -0,0 +1,99 @@
+# Copyright 2016 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.services.identity.v2 import endpoints_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestEndpointsClient(base.BaseServiceTest):
+ FAKE_CREATE_ENDPOINT = {
+ "endpoint": {
+ "id": 1,
+ "tenantId": 1,
+ "region": "North",
+ "type": "compute",
+ "publicURL": "https://compute.north.public.com/v1",
+ "internalURL": "https://compute.north.internal.com/v1",
+ "adminURL": "https://compute.north.internal.com/v1"
+ }
+ }
+
+ FAKE_LIST_ENDPOINTS = {
+ "endpoints": [
+ {
+ "id": 1,
+ "tenantId": "1",
+ "region": "North",
+ "type": "compute",
+ "publicURL": "https://compute.north.public.com/v1",
+ "internalURL": "https://compute.north.internal.com/v1",
+ "adminURL": "https://compute.north.internal.com/v1"
+ },
+ {
+ "id": 2,
+ "tenantId": "1",
+ "region": "South",
+ "type": "compute",
+ "publicURL": "https://compute.north.public.com/v1",
+ "internalURL": "https://compute.north.internal.com/v1",
+ "adminURL": "https://compute.north.internal.com/v1"
+ }
+ ]
+ }
+
+ def setUp(self):
+ super(TestEndpointsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = endpoints_client.EndpointsClient(fake_auth,
+ 'identity', 'regionOne')
+
+ def _test_create_endpoint(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_endpoint,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_ENDPOINT,
+ bytes_body,
+ service_id="b344506af7644f6794d9cb316600b020",
+ region="region-demo",
+ publicurl="https://compute.north.public.com/v1",
+ adminurl="https://compute.north.internal.com/v1",
+ internalurl="https://compute.north.internal.com/v1")
+
+ def _test_list_endpoints(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_endpoints,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_ENDPOINTS,
+ bytes_body)
+
+ def test_create_endpoint_with_str_body(self):
+ self._test_create_endpoint()
+
+ def test_create_endpoint_with_bytes_body(self):
+ self._test_create_endpoint(bytes_body=True)
+
+ def test_list_endpoints_with_str_body(self):
+ self._test_list_endpoints()
+
+ def test_list_endpoints_with_bytes_body(self):
+ self._test_list_endpoints(bytes_body=True)
+
+ def test_delete_endpoint(self):
+ self.check_service_client_function(
+ self.client.delete_endpoint,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ endpoint_id="b344506af7644f6794d9cb316600b020",
+ status=204)
diff --git a/tempest/tests/lib/services/identity/v2/test_identity_client.py b/tempest/tests/lib/services/identity/v2/test_identity_client.py
new file mode 100644
index 0000000..303d1f7
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v2/test_identity_client.py
@@ -0,0 +1,230 @@
+# Copyright 2016 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.services.identity.v2 import identity_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestIdentityClient(base.BaseServiceTest):
+ FAKE_TOKEN = {
+ "tokens": {
+ "id": "cbc36478b0bd8e67e89",
+ "name": "FakeToken",
+ "type": "token",
+ }
+ }
+
+ FAKE_ENDPOINTS_FOR_TOKEN = {
+ "endpoints_links": [],
+ "endpoints": [
+ {
+ "name": "nova",
+ "adminURL": "https://nova.region-one.internal.com/" +
+ "v2/be1319401cfa4a0aa590b97cc7b64d8d",
+ "region": "RegionOne",
+ "internalURL": "https://nova.region-one.internal.com/" +
+ "v2/be1319401cfa4a0aa590b97cc7b64d8d",
+ "type": "compute",
+ "id": "11b41ee1b00841128b7333d4bf1a6140",
+ "publicURL": "https://nova.region-one.public.com/v2/" +
+ "be1319401cfa4a0aa590b97cc7b64d8d"
+ },
+ {
+ "name": "neutron",
+ "adminURL": "https://neutron.region-one.internal.com/",
+ "region": "RegionOne",
+ "internalURL": "https://neutron.region-one.internal.com/",
+ "type": "network",
+ "id": "cdbfa3c416d741a9b5c968f2dc628acb",
+ "publicURL": "https://neutron.region-one.public.com/"
+ }
+ ]
+ }
+
+ FAKE_API_INFO = {
+ "name": "API_info",
+ "type": "API",
+ "description": "test_description"
+ }
+
+ FAKE_LIST_EXTENSIONS = {
+ "extensions": {
+ "values": [
+ {
+ "updated": "2013-07-07T12:00:0-00:00",
+ "name": "OpenStack S3 API",
+ "links": [
+ {
+ "href": "https://github.com/openstack/" +
+ "identity-api",
+ "type": "text/html",
+ "rel": "describedby"
+ }
+ ],
+ "namespace": "http://docs.openstack.org/identity/" +
+ "api/ext/s3tokens/v1.0",
+ "alias": "s3tokens",
+ "description": "OpenStack S3 API."
+ },
+ {
+ "updated": "2013-12-17T12:00:0-00:00",
+ "name": "OpenStack Federation APIs",
+ "links": [
+ {
+ "href": "https://github.com/openstack/" +
+ "identity-api",
+ "type": "text/html",
+ "rel": "describedby"
+ }
+ ],
+ "namespace": "http://docs.openstack.org/identity/" +
+ "api/ext/OS-FEDERATION/v1.0",
+ "alias": "OS-FEDERATION",
+ "description": "OpenStack Identity Providers Mechanism."
+ },
+ {
+ "updated": "2014-01-20T12:00:0-00:00",
+ "name": "OpenStack Simple Certificate API",
+ "links": [
+ {
+ "href": "https://github.com/openstack/" +
+ "identity-api",
+ "type": "text/html",
+ "rel": "describedby"
+ }
+ ],
+ "namespace": "http://docs.openstack.org/identity/api/" +
+ "ext/OS-SIMPLE-CERT/v1.0",
+ "alias": "OS-SIMPLE-CERT",
+ "description": "OpenStack simple certificate extension"
+ },
+ {
+ "updated": "2013-07-07T12:00:0-00:00",
+ "name": "OpenStack OAUTH1 API",
+ "links": [
+ {
+ "href": "https://github.com/openstack/" +
+ "identity-api",
+ "type": "text/html",
+ "rel": "describedby"
+ }
+ ],
+ "namespace": "http://docs.openstack.org/identity/" +
+ "api/ext/OS-OAUTH1/v1.0",
+ "alias": "OS-OAUTH1",
+ "description": "OpenStack OAuth Delegated Auth Mechanism."
+ },
+ {
+ "updated": "2013-07-07T12:00:0-00:00",
+ "name": "OpenStack EC2 API",
+ "links": [
+ {
+ "href": "https://github.com/openstack/" +
+ "identity-api",
+ "type": "text/html",
+ "rel": "describedby"
+ }
+ ],
+ "namespace": "http://docs.openstack.org/identity/api/" +
+ "ext/OS-EC2/v1.0",
+ "alias": "OS-EC2",
+ "description": "OpenStack EC2 Credentials backend."
+ }
+ ]
+ }
+ }
+
+ def setUp(self):
+ super(TestIdentityClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = identity_client.IdentityClient(fake_auth,
+ 'identity',
+ 'regionOne')
+
+ def _test_show_api_description(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_api_description,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_API_INFO,
+ bytes_body)
+
+ def _test_list_extensions(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_extensions,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_EXTENSIONS,
+ bytes_body)
+
+ def _test_show_token(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_token,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_TOKEN,
+ bytes_body,
+ token_id="cbc36478b0bd8e67e89")
+
+ def _test_list_endpoints_for_token(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_endpoints_for_token,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_ENDPOINTS_FOR_TOKEN,
+ bytes_body,
+ token_id="cbc36478b0bd8e67e89")
+
+ def _test_check_token_existence(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.check_token_existence,
+ 'tempest.lib.common.rest_client.RestClient.head',
+ {},
+ bytes_body,
+ token_id="cbc36478b0bd8e67e89")
+
+ def test_show_api_description_with_str_body(self):
+ self._test_show_api_description()
+
+ def test_show_api_description_with_bytes_body(self):
+ self._test_show_api_description(bytes_body=True)
+
+ def test_show_list_extensions_with_str_body(self):
+ self._test_list_extensions()
+
+ def test_show_list_extensions_with_bytes_body(self):
+ self._test_list_extensions(bytes_body=True)
+
+ def test_show_token_with_str_body(self):
+ self._test_show_token()
+
+ def test_show_token_with_bytes_body(self):
+ self._test_show_token(bytes_body=True)
+
+ def test_list_endpoints_for_token_with_str_body(self):
+ self._test_list_endpoints_for_token()
+
+ def test_list_endpoints_for_token_with_bytes_body(self):
+ self._test_list_endpoints_for_token(bytes_body=True)
+
+ def test_check_token_existence_with_bytes_body(self):
+ self._test_check_token_existence(bytes_body=True)
+
+ def test_check_token_existence_with_str_body(self):
+ self._test_check_token_existence()
+
+ def test_delete_token(self):
+ self.check_service_client_function(
+ self.client.delete_token,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ token_id="cbc36478b0bd8e67e89",
+ status=204)
diff --git a/tempest/tests/lib/services/identity/v2/test_roles_client.py b/tempest/tests/lib/services/identity/v2/test_roles_client.py
new file mode 100644
index 0000000..464a715
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v2/test_roles_client.py
@@ -0,0 +1,141 @@
+# Copyright 2016 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.services.identity.v2 import roles_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestRolesClient(base.BaseServiceTest):
+ FAKE_ROLE_INFO = {
+ "role": {
+ "id": "1",
+ "name": "test",
+ "description": "test_description"
+ }
+ }
+
+ FAKE_LIST_ROLES = {
+ "roles": [
+ {
+ "id": "1",
+ "name": "test",
+ "description": "test_description"
+ },
+ {
+ "id": "2",
+ "name": "test2",
+ "description": "test2_description"
+ }
+ ]
+ }
+
+ def setUp(self):
+ super(TestRolesClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = roles_client.RolesClient(fake_auth,
+ 'identity', 'regionOne')
+
+ def _test_create_role(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_role,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_ROLE_INFO,
+ bytes_body,
+ id="1",
+ name="test",
+ description="test_description")
+
+ def _test_show_role(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_role,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_ROLE_INFO,
+ bytes_body,
+ role_id_or_name="1")
+
+ def _test_list_roles(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_roles,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_ROLES,
+ bytes_body)
+
+ def _test_create_user_role_on_project(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_user_role_on_project,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ self.FAKE_ROLE_INFO,
+ bytes_body,
+ tenant_id="b344506af7644f6794d9cb316600b020",
+ user_id="123",
+ role_id="1234",
+ status=200)
+
+ def _test_list_user_roles_on_project(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_user_roles_on_project,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_ROLES,
+ bytes_body,
+ tenant_id="b344506af7644f6794d9cb316600b020",
+ user_id="123")
+
+ def test_create_role_with_str_body(self):
+ self._test_create_role()
+
+ def test_create_role_with_bytes_body(self):
+ self._test_create_role(bytes_body=True)
+
+ def test_show_role_with_str_body(self):
+ self._test_show_role()
+
+ def test_show_role_with_bytes_body(self):
+ self._test_show_role(bytes_body=True)
+
+ def test_list_roles_with_str_body(self):
+ self._test_list_roles()
+
+ def test_list_roles_with_bytes_body(self):
+ self._test_list_roles(bytes_body=True)
+
+ def test_delete_role(self):
+ self.check_service_client_function(
+ self.client.delete_role,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ role_id="1",
+ status=204)
+
+ def test_create_user_role_on_project_with_str_body(self):
+ self._test_create_user_role_on_project()
+
+ def test_create_user_role_on_project_with_bytes_body(self):
+ self._test_create_user_role_on_project(bytes_body=True)
+
+ def test_list_user_roles_on_project_with_str_body(self):
+ self._test_list_user_roles_on_project()
+
+ def test_list_user_roles_on_project_with_bytes_body(self):
+ self._test_list_user_roles_on_project(bytes_body=True)
+
+ def test_delete_role_from_user_on_project(self):
+ self.check_service_client_function(
+ self.client.delete_role_from_user_on_project,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ tenant_id="b344506af7644f6794d9cb316600b020",
+ user_id="123",
+ role_id="1234",
+ status=204)
diff --git a/tempest/tests/lib/services/identity/v2/test_services_client.py b/tempest/tests/lib/services/identity/v2/test_services_client.py
new file mode 100644
index 0000000..bafb6b1
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v2/test_services_client.py
@@ -0,0 +1,97 @@
+# Copyright 2016 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.services.identity.v2 import services_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestServicesClient(base.BaseServiceTest):
+ FAKE_SERVICE_INFO = {
+ "OS-KSADM:service": {
+ "id": "1",
+ "name": "test",
+ "type": "compute",
+ "description": "test_description"
+ }
+ }
+
+ FAKE_LIST_SERVICES = {
+ "OS-KSADM:services": [
+ {
+ "id": "1",
+ "name": "test",
+ "type": "compute",
+ "description": "test_description"
+ },
+ {
+ "id": "2",
+ "name": "test2",
+ "type": "compute",
+ "description": "test2_description"
+ }
+ ]
+ }
+
+ def setUp(self):
+ super(TestServicesClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = services_client.ServicesClient(fake_auth,
+ 'identity', 'regionOne')
+
+ def _test_create_service(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_service,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_SERVICE_INFO,
+ bytes_body,
+ id="1",
+ name="test",
+ type="compute",
+ description="test_description")
+
+ def _test_show_service(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_service,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_SERVICE_INFO,
+ bytes_body,
+ service_id="1")
+
+ def _test_list_services(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_services,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_SERVICES,
+ bytes_body)
+
+ def test_create_service_with_str_body(self):
+ self._test_create_service()
+
+ def test_create_service_with_bytes_body(self):
+ self._test_create_service(bytes_body=True)
+
+ def test_show_service_with_str_body(self):
+ self._test_show_service()
+
+ def test_show_service_with_bytes_body(self):
+ self._test_show_service(bytes_body=True)
+
+ def test_delete_service(self):
+ self.check_service_client_function(
+ self.client.delete_service,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ service_id="1",
+ status=204)
diff --git a/tempest/tests/lib/services/identity/v2/test_tenants_client.py b/tempest/tests/lib/services/identity/v2/test_tenants_client.py
new file mode 100644
index 0000000..ae3d13a
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v2/test_tenants_client.py
@@ -0,0 +1,131 @@
+# Copyright 2016 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.services.identity.v2 import tenants_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestTenantsClient(base.BaseServiceTest):
+ FAKE_TENANT_INFO = {
+ "tenant": {
+ "id": "1",
+ "name": "test",
+ "description": "test_description",
+ "enabled": True
+ }
+ }
+
+ FAKE_LIST_TENANTS = {
+ "tenants": [
+ {
+ "id": "1",
+ "name": "test",
+ "description": "test_description",
+ "enabled": True
+ },
+ {
+ "id": "2",
+ "name": "test2",
+ "description": "test2_description",
+ "enabled": True
+ }
+ ]
+ }
+
+ def setUp(self):
+ super(TestTenantsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = tenants_client.TenantsClient(fake_auth,
+ 'identity', 'regionOne')
+
+ def _test_create_tenant(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_tenant,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_TENANT_INFO,
+ bytes_body,
+ name="test",
+ description="test_description")
+
+ def _test_show_tenant(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_tenant,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_TENANT_INFO,
+ bytes_body,
+ tenant_id="1")
+
+ def _test_update_tenant(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_tenant,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_TENANT_INFO,
+ bytes_body,
+ tenant_id="1",
+ name="test",
+ description="test_description")
+
+ def _test_list_tenants(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_tenants,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_TENANTS,
+ bytes_body)
+
+ def _test_list_tenant_users(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_tenant_users,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_TENANTS,
+ bytes_body,
+ tenant_id="1")
+
+ def test_create_tenant_with_str_body(self):
+ self._test_create_tenant()
+
+ def test_create_tenant_with_bytes_body(self):
+ self._test_create_tenant(bytes_body=True)
+
+ def test_show_tenant_with_str_body(self):
+ self._test_show_tenant()
+
+ def test_show_tenant_with_bytes_body(self):
+ self._test_show_tenant(bytes_body=True)
+
+ def test_update_tenant_with_str_body(self):
+ self._test_update_tenant()
+
+ def test_update_tenant_with_bytes_body(self):
+ self._test_update_tenant(bytes_body=True)
+
+ def test_list_tenants_with_str_body(self):
+ self._test_list_tenants()
+
+ def test_list_tenants_with_bytes_body(self):
+ self._test_list_tenants(bytes_body=True)
+
+ def test_delete_tenant(self):
+ self.check_service_client_function(
+ self.client.delete_tenant,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ tenant_id="1",
+ status=204)
+
+ def test_list_tenant_users_with_str_body(self):
+ self._test_list_tenant_users()
+
+ def test_list_tenant_users_with_bytes_body(self):
+ self._test_list_tenant_users(bytes_body=True)
diff --git a/tempest/tests/lib/services/identity/v2/test_token_client.py b/tempest/tests/lib/services/identity/v2/test_token_client.py
new file mode 100644
index 0000000..dfce9b3
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v2/test_token_client.py
@@ -0,0 +1,104 @@
+# Copyright 2015 Hewlett-Packard Development Company, L.P.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import json
+
+import mock
+
+from tempest.lib.common import rest_client
+from tempest.lib import exceptions
+from tempest.lib.services.identity.v2 import token_client
+from tempest.tests import base
+from tempest.tests.lib import fake_http
+
+
+class TestTokenClientV2(base.TestCase):
+
+ def test_init_without_authurl(self):
+ self.assertRaises(exceptions.IdentityError,
+ token_client.TokenClient, None)
+
+ def test_auth(self):
+ token_client_v2 = token_client.TokenClient('fake_url')
+ response = fake_http.fake_http_response(
+ None, status=200,
+ )
+ body = {'access': {'token': 'fake_token'}}
+
+ with mock.patch.object(token_client_v2, 'post') as post_mock:
+ post_mock.return_value = response, body
+ resp = token_client_v2.auth('fake_user', 'fake_pass')
+
+ self.assertIsInstance(resp, rest_client.ResponseBody)
+ req_dict = json.dumps({
+ 'auth': {
+ 'passwordCredentials': {
+ 'username': 'fake_user',
+ 'password': 'fake_pass',
+ },
+ }
+ }, sort_keys=True)
+ post_mock.assert_called_once_with('fake_url/tokens',
+ body=req_dict)
+
+ def test_auth_with_tenant(self):
+ token_client_v2 = token_client.TokenClient('fake_url')
+ response = fake_http.fake_http_response(
+ None, status=200,
+ )
+ body = {'access': {'token': 'fake_token'}}
+
+ with mock.patch.object(token_client_v2, 'post') as post_mock:
+ post_mock.return_value = response, body
+ resp = token_client_v2.auth('fake_user', 'fake_pass',
+ 'fake_tenant')
+
+ self.assertIsInstance(resp, rest_client.ResponseBody)
+ req_dict = json.dumps({
+ 'auth': {
+ 'tenantName': 'fake_tenant',
+ 'passwordCredentials': {
+ 'username': 'fake_user',
+ 'password': 'fake_pass',
+ },
+ }
+ }, sort_keys=True)
+ post_mock.assert_called_once_with('fake_url/tokens',
+ body=req_dict)
+
+ def test_request_with_str_body(self):
+ token_client_v2 = token_client.TokenClient('fake_url')
+ response = fake_http.fake_http_response(
+ None, status=200,
+ )
+ body = str('{"access": {"token": "fake_token"}}')
+
+ with mock.patch.object(token_client_v2, 'raw_request') as mock_raw_r:
+ mock_raw_r.return_value = response, body
+ resp, body = token_client_v2.request('GET', 'fake_uri')
+ self.assertIsInstance(body, dict)
+
+ def test_request_with_bytes_body(self):
+ token_client_v2 = token_client.TokenClient('fake_url')
+
+ response = fake_http.fake_http_response(
+ None, status=200,
+ )
+ body = b'{"access": {"token": "fake_token"}}'
+
+ with mock.patch.object(token_client_v2, 'raw_request') as mock_raw_r:
+ mock_raw_r.return_value = response, body
+ resp, body = token_client_v2.request('GET', 'fake_uri')
+
+ self.assertIsInstance(body, dict)
diff --git a/tempest/tests/lib/services/identity/v2/test_users_client.py b/tempest/tests/lib/services/identity/v2/test_users_client.py
new file mode 100644
index 0000000..9534e44
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v2/test_users_client.py
@@ -0,0 +1,243 @@
+# Copyright 2016 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.services.identity.v2 import users_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestUsersClient(base.BaseServiceTest):
+ FAKE_USER_INFO = {
+ "user": {
+ "id": "1",
+ "name": "test",
+ "email": "john.smith@example.org",
+ "enabled": True
+ }
+ }
+
+ FAKE_LIST_USERS = {
+ "users": [
+ {
+ "id": "1",
+ "name": "test",
+ "email": "john.smith@example.org",
+ "enabled": True
+ },
+ {
+ "id": "2",
+ "name": "test2",
+ "email": "john.smith@example.org",
+ "enabled": True
+ }
+ ]
+ }
+
+ FAKE_USER_EC2_CREDENTIAL_INFO = {
+ "credential": {
+ 'user_id': '9beb0e12f3e5416db8d7cccfc785db3b',
+ 'access': '79abf59acc77492a86170cbe2f1feafa',
+ 'secret': 'c4e7d3a691fd4563873d381a40320f46',
+ 'trust_id': None,
+ 'tenant_id': '596557269d7b4dd78631a602eb9f151d'
+ }
+ }
+
+ FAKE_LIST_USER_EC2_CREDENTIALS = {
+ "credentials": [
+ {
+ 'user_id': '9beb0e12f3e5416db8d7cccfc785db3b',
+ 'access': '79abf59acc77492a86170cbe2f1feafa',
+ 'secret': 'c4e7d3a691fd4563873d381a40320f46',
+ 'trust_id': None,
+ 'tenant_id': '596557269d7b4dd78631a602eb9f151d'
+ },
+ {
+ 'user_id': '3beb0e12f3e5416db8d7cccfc785de4r',
+ 'access': '45abf59acc77492a86170cbe2f1fesde',
+ 'secret': 'g4e7d3a691fd4563873d381a40320e45',
+ 'trust_id': None,
+ 'tenant_id': '123557269d7b4dd78631a602eb9f112f'
+ }
+ ]
+ }
+
+ def setUp(self):
+ super(TestUsersClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = users_client.UsersClient(fake_auth,
+ 'identity', 'regionOne')
+
+ def _test_create_user(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_user,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_USER_INFO,
+ bytes_body,
+ name="test",
+ email="john.smith@example.org")
+
+ def _test_update_user(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_user,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ self.FAKE_USER_INFO,
+ bytes_body,
+ user_id="1",
+ name="test")
+
+ def _test_show_user(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_user,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_USER_INFO,
+ bytes_body,
+ user_id="1")
+
+ def _test_list_users(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_users,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_USERS,
+ bytes_body)
+
+ def _test_update_user_enabled(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_user_enabled,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ self.FAKE_USER_INFO,
+ bytes_body,
+ user_id="1",
+ enabled=True)
+
+ def _test_update_user_password(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_user_password,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ self.FAKE_USER_INFO,
+ bytes_body,
+ user_id="1",
+ password="pass")
+
+ def _test_update_user_own_password(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_user_own_password,
+ 'tempest.lib.common.rest_client.RestClient.patch',
+ self.FAKE_USER_INFO,
+ bytes_body,
+ user_id="1",
+ password="pass")
+
+ def _test_create_user_ec2_credential(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_user_ec2_credential,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_USER_EC2_CREDENTIAL_INFO,
+ bytes_body,
+ user_id="1",
+ tenant_id="123")
+
+ def _test_show_user_ec2_credential(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_user_ec2_credential,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_USER_EC2_CREDENTIAL_INFO,
+ bytes_body,
+ user_id="1",
+ access="123")
+
+ def _test_list_user_ec2_credentials(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_user_ec2_credentials,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_USER_EC2_CREDENTIALS,
+ bytes_body,
+ user_id="1")
+
+ def test_create_user_with_str_body(self):
+ self._test_create_user()
+
+ def test_create_user_with_bytes_body(self):
+ self._test_create_user(bytes_body=True)
+
+ def test_update_user_with_str_body(self):
+ self._test_update_user()
+
+ def test_update_user_with_bytes_body(self):
+ self._test_update_user(bytes_body=True)
+
+ def test_show_user_with_str_body(self):
+ self._test_show_user()
+
+ def test_show_user_with_bytes_body(self):
+ self._test_show_user(bytes_body=True)
+
+ def test_list_users_with_str_body(self):
+ self._test_list_users()
+
+ def test_list_users_with_bytes_body(self):
+ self._test_list_users(bytes_body=True)
+
+ def test_delete_user(self):
+ self.check_service_client_function(
+ self.client.delete_user,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ user_id="1",
+ status=204)
+
+ def test_update_user_enabled_with_str_body(self):
+ self._test_update_user_enabled()
+
+ def test_update_user_enabled_with_bytes_body(self):
+ self._test_update_user_enabled(bytes_body=True)
+
+ def test_update_user_password_with_str_body(self):
+ self._test_update_user_password()
+
+ def test_update_user_password_with_bytes_body(self):
+ self._test_update_user_password(bytes_body=True)
+
+ def test_update_user_own_password_with_str_body(self):
+ self._test_update_user_own_password()
+
+ def test_update_user_own_password_with_bytes_body(self):
+ self._test_update_user_own_password(bytes_body=True)
+
+ def test_create_user_ec2_credential_with_str_body(self):
+ self._test_create_user_ec2_credential()
+
+ def test_create_user_ec2_credential_with_bytes_body(self):
+ self._test_create_user_ec2_credential(bytes_body=True)
+
+ def test_show_user_ec2_credential_with_str_body(self):
+ self._test_show_user_ec2_credential()
+
+ def test_show_user_ec2_credential_with_bytes_body(self):
+ self._test_show_user_ec2_credential(bytes_body=True)
+
+ def test_list_user_ec2_credentials_with_str_body(self):
+ self._test_list_user_ec2_credentials()
+
+ def test_list_user_ec2_credentials_with_bytes_body(self):
+ self._test_list_user_ec2_credentials(bytes_body=True)
+
+ def test_delete_user_ec2_credential(self):
+ self.check_service_client_function(
+ self.client.delete_user_ec2_credential,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ user_id="123",
+ access="1234",
+ status=204)
diff --git a/tempest/services/identity/v3/__init__.py b/tempest/tests/lib/services/identity/v3/__init__.py
similarity index 100%
rename from tempest/services/identity/v3/__init__.py
rename to tempest/tests/lib/services/identity/v3/__init__.py
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_catalog_client.py b/tempest/tests/lib/services/identity/v3/test_catalog_client.py
new file mode 100644
index 0000000..0ac8fe4
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_catalog_client.py
@@ -0,0 +1,86 @@
+# 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 catalog_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestCatalogClient(base.BaseServiceTest):
+ FAKE_CATALOG_INFO = {
+ 'catalog': [
+ {
+ 'endpoints': [
+ {
+ 'id': '39dc322ce86c4111b4f06c2eeae0841b',
+ 'interface': 'public',
+ 'region': 'RegionOne',
+ 'url': 'http://localhost:5000'
+ },
+ ],
+ 'id': 'ac58672276f848a7b1727850b3ebe826',
+ 'type': 'compute',
+ 'name': 'nova'
+ },
+ {
+ 'endpoints': [
+ {
+ 'id': '39dc322ce86c4111b4f06c2eeae0841b',
+ 'interface': 'public',
+ 'region': 'RegionOne',
+ 'url': 'http://localhost:5000'
+ },
+ ],
+ 'id': 'b7c5ed2b486a46dbb4c221499d22991c',
+ 'type': 'image',
+ 'name': 'glance'
+ },
+ {
+ 'endpoints': [
+ {
+ 'id': '39dc322ce86c4111b4f06c2eeae0841b',
+ 'interface': 'public',
+ 'region': 'RegionOne',
+ 'url': 'http://localhost:5000'
+ },
+ ],
+ 'id': '4363ae44bdf34a3981fde3b823cb9aa2',
+ 'type': 'identity',
+ 'name': 'keystone'
+ }
+
+ ],
+ 'links': {
+ 'self': 'http://localhost/identity/v3/catalog',
+ 'previous': None,
+ 'next': None
+ }
+ }
+
+ def setUp(self):
+ super(TestCatalogClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = catalog_client.CatalogClient(fake_auth, 'identity',
+ 'RegionOne')
+
+ def test_show_catalog_with_bytes_body(self):
+ self._test_show_catalog(bytes_body=True)
+
+ def test_show_catalog_with_str_body(self):
+ self._test_show_catalog()
+
+ def _test_show_catalog(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_catalog,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_CATALOG_INFO,
+ bytes_body)
diff --git a/tempest/tests/lib/services/identity/v3/test_credentials_client.py b/tempest/tests/lib/services/identity/v3/test_credentials_client.py
new file mode 100644
index 0000000..29d7496
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_credentials_client.py
@@ -0,0 +1,179 @@
+# Copyright 2016 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.services.identity.v3 import credentials_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestCredentialsClient(base.BaseServiceTest):
+ FAKE_CREATE_CREDENTIAL = {
+ "credential": {
+ "blob": "{\"access\":\"181920\",\"secret\":\"secretKey\"}",
+ "project_id": "731fc6f265cd486d900f16e84c5cb594",
+ "type": "ec2",
+ "user_id": "bb5476fd12884539b41d5a88f838d773"
+ }
+ }
+
+ FAKE_INFO_CREDENTIAL = {
+ "credential": {
+ "user_id": "bb5476fd12884539b41d5a88f838d773",
+ "links": {
+ "self": "http://example.com/identity/v3/credentials/" +
+ "207e9b76935efc03804d3dd6ab52d22e9b22a0711e4" +
+ "ada4ff8b76165a07311d7"
+ },
+ "blob": "{\"access\": \"a42a27755ce6442596b049bd7dd8a563\"," +
+ " \"secret\": \"71faf1d40bb24c82b479b1c6fbbd9f0c\"}",
+ "project_id": "6e01855f345f4c59812999b5e459137d",
+ "type": "ec2",
+ "id": "207e9b76935efc03804d3dd6ab52d22e9b22a0711e4ada4f"
+ }
+ }
+
+ FAKE_LIST_CREDENTIALS = {
+ "credentials": [
+ {
+ "user_id": "bb5476fd12884539b41d5a88f838d773",
+ "links": {
+ "self": "http://example.com/identity/v3/credentials/" +
+ "207e9b76935efc03804d3dd6ab52d22e9b22a0711e4" +
+ "ada4ff8b76165a07311d7"
+ },
+ "blob": "{\"access\": \"a42a27755ce6442596b049bd7dd8a563\"," +
+ " \"secret\": \"71faf1d40bb24c82b479b1c6fbbd9f0c\"," +
+ " \"trust_id\": null}",
+ "project_id": "6e01855f345f4c59812999b5e459137d",
+ "type": "ec2",
+ "id": "207e9b76935efc03804d3dd6ab52d22e9b22a0711e4ada4f"
+ },
+ {
+ "user_id": "6f556708d04b4ea6bc72d7df2296b71a",
+ "links": {
+ "self": "http://example.com/identity/v3/credentials/" +
+ "2441494e52ab6d594a34d74586075cb299489bdd1e9" +
+ "389e3ab06467a4f460609"
+ },
+ "blob": "{\"access\": \"7da79ff0aa364e1396f067e352b9b79a\"," +
+ " \"secret\": \"7a18d68ba8834b799d396f3ff6f1e98c\"," +
+ " \"trust_id\": null}",
+ "project_id": "1a1d14690f3c4ec5bf5f321c5fde3c16",
+ "type": "ec2",
+ "id": "2441494e52ab6d594a34d74586075cb299489bdd1e9389e3"
+ },
+ {
+ "user_id": "c14107e65d5c4a7f8894fc4b3fc209ff",
+ "links": {
+ "self": "http://example.com/identity/v3/credentials/" +
+ "3397b204b5f04c495bcdc8f34c8a39996f280f91726" +
+ "58241873e15f070ec79d7"
+ },
+ "blob": "{\"access\": \"db9c58a558534a10a070110de4f9f20c\"," +
+ " \"secret\": \"973e790b88db447ba6f93bca02bc745b\"," +
+ " \"trust_id\": null}",
+ "project_id": "7396e43183db40dcbf40dd727637b548",
+ "type": "ec2",
+ "id": "3397b204b5f04c495bcdc8f34c8a39996f280f9172658241"
+ },
+ {
+ "user_id": "bb5476fd12884539b41d5a88f838d773",
+ "links": {
+ "self": "http://example.com/identity/v3/credentials/" +
+ "7ef4faa904ae7b8b4ddc7bad15b05ee359dad7d7a9b" +
+ "82861d4ad92fdbbb2eb4e"
+ },
+ "blob": "{\"access\": \"7d7559359b57419eb5f5f5dcd65ab57d\"," +
+ " \"secret\": \"570652bcf8c2483c86eb29e9734eed3c\"," +
+ " \"trust_id\": null}",
+ "project_id": "731fc6f265cd486d900f16e84c5cb594",
+ "type": "ec2",
+ "id": "7ef4faa904ae7b8b4ddc7bad15b05ee359dad7d7a9b82861"
+ },
+ ],
+ "links": {
+ "self": "http://example.com/identity/v3/credentials",
+ "previous": None,
+ "next": None
+ }
+ }
+
+ def setUp(self):
+ super(TestCredentialsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = credentials_client.CredentialsClient(fake_auth,
+ 'identity',
+ 'regionOne')
+
+ def _test_create_credential(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_credential,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_CREDENTIAL,
+ bytes_body, status=201)
+
+ def _test_show_credential(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_credential,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_INFO_CREDENTIAL,
+ bytes_body,
+ credential_id="207e9b76935efc03804d3dd6ab52d22e9b22a0711e4ada4f")
+
+ def _test_update_credential(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_credential,
+ 'tempest.lib.common.rest_client.RestClient.patch',
+ self.FAKE_INFO_CREDENTIAL,
+ bytes_body,
+ credential_id="207e9b76935efc03804d3dd6ab52d22e9b22a0711e4ada4f")
+
+ def _test_list_credentials(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_credentials,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_CREDENTIALS,
+ bytes_body)
+
+ def test_create_credential_with_str_body(self):
+ self._test_create_credential()
+
+ def test_create_credential_with_bytes_body(self):
+ self._test_create_credential(bytes_body=True)
+
+ def test_show_credential_with_str_body(self):
+ self._test_show_credential()
+
+ def test_show_credential_with_bytes_body(self):
+ self._test_show_credential(bytes_body=True)
+
+ def test_update_credential_with_str_body(self):
+ self._test_update_credential()
+
+ def test_update_credential_with_bytes_body(self):
+ self._test_update_credential(bytes_body=True)
+
+ def test_list_credentials_with_str_body(self):
+ self._test_list_credentials()
+
+ def test_list_credentials_with_bytes_body(self):
+ self._test_list_credentials(bytes_body=True)
+
+ def test_delete_credential(self):
+ self.check_service_client_function(
+ self.client.delete_credential,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ credential_id="207e9b76935efc03804d3dd6ab52d22e9b22a0711e4ada4f",
+ status=204)
diff --git a/tempest/tests/lib/services/identity/v3/test_domain_configuration_client.py b/tempest/tests/lib/services/identity/v3/test_domain_configuration_client.py
new file mode 100644
index 0000000..72e5bd2
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_domain_configuration_client.py
@@ -0,0 +1,217 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# 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 domain_configuration_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestDomainConfigurationClient(base.BaseServiceTest):
+
+ FAKE_CONFIG_SETTINGS = {
+ "config": {
+ "identity": {
+ "driver": "ldap"
+ },
+ "ldap": {
+ "url": "ldap://localhost",
+ "user": "",
+ "suffix": "cn=example,cn=com",
+ }
+ }
+ }
+
+ FAKE_DOMAIN_ID = '07ef7d04-2941-4bee-8551-f79f08a021de'
+
+ def setUp(self):
+ super(TestDomainConfigurationClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = domain_configuration_client.DomainConfigurationClient(
+ fake_auth, 'identity', 'regionOne')
+
+ def _test_show_default_config_settings(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_default_config_settings,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_CONFIG_SETTINGS,
+ bytes_body)
+
+ def _test_show_default_group_config(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_default_group_config,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_CONFIG_SETTINGS['config']['ldap'],
+ bytes_body,
+ group='ldap')
+
+ def _test_show_default_group_option(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_default_group_option,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {'driver': 'ldap'},
+ bytes_body,
+ group='identity',
+ option='driver')
+
+ def _test_show_domain_group_option_config(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_domain_group_option_config,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {'driver': 'ldap'},
+ bytes_body,
+ domain_id=self.FAKE_DOMAIN_ID,
+ group='identity',
+ option='driver')
+
+ def _test_update_domain_group_option_config(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_domain_group_option_config,
+ 'tempest.lib.common.rest_client.RestClient.patch',
+ self.FAKE_CONFIG_SETTINGS,
+ bytes_body,
+ domain_id=self.FAKE_DOMAIN_ID,
+ group='identity',
+ option='driver',
+ url='http://myldap/my_other_root')
+
+ def _test_show_domain_group_config(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_domain_group_config,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_CONFIG_SETTINGS['config']['ldap'],
+ bytes_body,
+ domain_id=self.FAKE_DOMAIN_ID,
+ group='ldap')
+
+ def _test_update_domain_group_config(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_domain_group_config,
+ 'tempest.lib.common.rest_client.RestClient.patch',
+ self.FAKE_CONFIG_SETTINGS['config']['ldap'],
+ bytes_body,
+ domain_id=self.FAKE_DOMAIN_ID,
+ group='ldap',
+ **self.FAKE_CONFIG_SETTINGS['config'])
+
+ def _test_create_domain_config(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_domain_config,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ self.FAKE_CONFIG_SETTINGS,
+ bytes_body,
+ domain_id=self.FAKE_DOMAIN_ID,
+ status=201)
+
+ def _test_show_domain_config(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_domain_config,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_CONFIG_SETTINGS,
+ bytes_body,
+ domain_id=self.FAKE_DOMAIN_ID)
+
+ def _test_update_domain_config(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_domain_config,
+ 'tempest.lib.common.rest_client.RestClient.patch',
+ self.FAKE_CONFIG_SETTINGS,
+ bytes_body,
+ domain_id=self.FAKE_DOMAIN_ID)
+
+ def test_show_default_config_settings_with_str_body(self):
+ self._test_show_default_config_settings()
+
+ def test_show_default_config_settings_with_bytes_body(self):
+ self._test_show_default_config_settings(bytes_body=True)
+
+ def test_show_default_group_config_with_str_body(self):
+ self._test_show_default_group_config()
+
+ def test_show_default_group_config_with_bytes_body(self):
+ self._test_show_default_group_config(bytes_body=True)
+
+ def test_show_default_group_option_with_str_body(self):
+ self._test_show_default_group_option()
+
+ def test_show_default_group_option_with_bytes_body(self):
+ self._test_show_default_group_option(bytes_body=True)
+
+ def test_show_domain_group_option_config_with_str_body(self):
+ self._test_show_domain_group_option_config()
+
+ def test_show_domain_group_option_config_with_bytes_body(self):
+ self._test_show_domain_group_option_config(bytes_body=True)
+
+ def test_update_domain_group_option_config_with_str_body(self):
+ self._test_update_domain_group_option_config()
+
+ def test_update_domain_group_option_config_with_bytes_body(self):
+ self._test_update_domain_group_option_config(bytes_body=True)
+
+ def test_delete_domain_group_option_config(self):
+ self.check_service_client_function(
+ self.client.delete_domain_group_option_config,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ status=204,
+ domain_id=self.FAKE_DOMAIN_ID,
+ group='identity',
+ option='driver')
+
+ def test_show_domain_group_config_with_str_body(self):
+ self._test_show_domain_group_config()
+
+ def test_show_domain_group_config_with_bytes_body(self):
+ self._test_show_domain_group_config(bytes_body=True)
+
+ def test_test_update_domain_group_config_with_str_body(self):
+ self._test_update_domain_group_config()
+
+ def test_update_domain_group_config_with_bytes_body(self):
+ self._test_update_domain_group_config(bytes_body=True)
+
+ def test_delete_domain_group_config(self):
+ self.check_service_client_function(
+ self.client.delete_domain_group_config,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ status=204,
+ domain_id=self.FAKE_DOMAIN_ID,
+ group='identity')
+
+ def test_create_domain_config_with_str_body(self):
+ self._test_create_domain_config()
+
+ def test_create_domain_config_with_bytes_body(self):
+ self._test_create_domain_config(bytes_body=True)
+
+ def test_show_domain_config_with_str_body(self):
+ self._test_show_domain_config()
+
+ def test_show_domain_config_with_bytes_body(self):
+ self._test_show_domain_config(bytes_body=True)
+
+ def test_update_domain_config_with_str_body(self):
+ self._test_update_domain_config()
+
+ def test_update_domain_config_with_bytes_body(self):
+ self._test_update_domain_config(bytes_body=True)
+
+ def test_delete_domain_config(self):
+ self.check_service_client_function(
+ self.client.delete_domain_config,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ status=204,
+ domain_id=self.FAKE_DOMAIN_ID)
diff --git a/tempest/tests/lib/services/identity/v3/test_domains_client.py b/tempest/tests/lib/services/identity/v3/test_domains_client.py
new file mode 100644
index 0000000..f89ced7
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_domains_client.py
@@ -0,0 +1,138 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# 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 domains_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestDomainsClient(base.BaseServiceTest):
+ FAKE_CREATE_DOMAIN = {
+ "domain": {
+ "description": "Domain description",
+ "enabled": True,
+ "name": "myDomain"
+ }
+ }
+
+ FAKE_DOMAIN_INFO = {
+ "domain": {
+ "description": "Used for swift functional testing",
+ "enabled": True,
+ "id": "5a75994a3",
+ "links": {
+ "self": "http://example.com/identity/v3/domains/5a75994a3"
+ },
+ "name": "swift_test"
+ }
+ }
+
+ FAKE_LIST_DOMAINS = {
+ "domains": [
+ {
+ "description": "Used for swift functional testing",
+ "enabled": True,
+ "id": "5a75994a3",
+ "links": {
+ "self": "http://example.com/identity/v3/domains/5a75994a3"
+ },
+ "name": "swift_test"
+ },
+ {
+ "description": "Owns users and tenants available on " +
+ "Identity API",
+ "enabled": True,
+ "id": "default",
+ "links": {
+ "self": "http://example.com/identity/v3/domains/default"
+ },
+ "name": "Default"
+ }
+ ],
+ "links": {
+ "next": None,
+ "previous": None,
+ "self": "http://example.com/identity/v3/domains"
+ }
+ }
+
+ def setUp(self):
+ super(TestDomainsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = domains_client.DomainsClient(fake_auth,
+ 'identity',
+ 'regionOne')
+
+ def _test_create_domain(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_domain,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_DOMAIN,
+ bytes_body,
+ status=201)
+
+ def _test_show_domain(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_domain,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_DOMAIN_INFO,
+ bytes_body,
+ domain_id="5a75994a3")
+
+ def _test_list_domains(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_domains,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_DOMAINS,
+ bytes_body)
+
+ def _test_update_domain(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_domain,
+ 'tempest.lib.common.rest_client.RestClient.patch',
+ self.FAKE_DOMAIN_INFO,
+ bytes_body,
+ domain_id="5a75994a3")
+
+ def test_create_domain_with_str_body(self):
+ self._test_create_domain()
+
+ def test_create_domain_with_bytes_body(self):
+ self._test_create_domain(bytes_body=True)
+
+ def test_show_domain_with_str_body(self):
+ self._test_show_domain()
+
+ def test_show_domain_with_bytes_body(self):
+ self._test_show_domain(bytes_body=True)
+
+ def test_list_domain_with_str_body(self):
+ self._test_list_domains()
+
+ def test_list_domain_with_bytes_body(self):
+ self._test_list_domains(bytes_body=True)
+
+ def test_update_domain_with_str_body(self):
+ self._test_update_domain()
+
+ def test_update_domain_with_bytes_body(self):
+ self._test_update_domain(bytes_body=True)
+
+ def test_delete_domain(self):
+ self.check_service_client_function(
+ self.client.delete_domain,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ domain_id="5a75994a3",
+ status=204)
diff --git a/tempest/tests/lib/services/identity/v3/test_endpoint_filter_client.py b/tempest/tests/lib/services/identity/v3/test_endpoint_filter_client.py
new file mode 100644
index 0000000..7faf6a0
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_endpoint_filter_client.py
@@ -0,0 +1,165 @@
+# Copyright 2017 AT&T 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 tempest.lib.services.identity.v3 import endpoint_filter_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestEndPointsFilterClient(base.BaseServiceTest):
+ FAKE_LIST_PROJECTS_FOR_ENDPOINTS = {
+ "projects": [
+ {
+ "domain_id": "1777c7",
+ "enabled": True,
+ "id": "1234ab1",
+ "type": "compute",
+ "links": {
+ "self": "http://example.com/identity/v3/projects/1234ab1"
+ },
+ "name": "Project 1",
+ "description": "Project 1 description",
+ },
+ {
+ "domain_id": "1777c7",
+ "enabled": True,
+ "id": "5678cd2",
+ "type": "compute",
+ "links": {
+ "self": "http://example.com/identity/v3/projects/5678cd2"
+ },
+ "name": "Project 2",
+ "description": "Project 2 description",
+ }
+ ],
+ "links": {
+ "self": "http://example.com/identity/v3/OS-EP-FILTER/endpoints/\
+ u6ay5u/projects",
+ "previous": None,
+ "next": None
+ }
+ }
+
+ FAKE_LIST_ENDPOINTS_FOR_PROJECTS = {
+ "endpoints": [
+ {
+ "id": "u6ay5u",
+ "interface": "public",
+ "url": "http://example.com/identity/",
+ "region": "north",
+ "links": {
+ "self": "http://example.com/identity/v3/endpoints/u6ay5u"
+ },
+ "service_id": "5um4r",
+ },
+ {
+ "id": "u6ay5u",
+ "interface": "internal",
+ "url": "http://example.com/identity/",
+ "region": "south",
+ "links": {
+ "self": "http://example.com/identity/v3/endpoints/u6ay5u"
+ },
+ "service_id": "5um4r",
+ },
+ ],
+ "links": {
+ "self": "http://example.com/identity/v3/OS-EP-FILTER/projects/\
+ 1234ab1/endpoints",
+ "previous": None,
+ "next": None
+ }
+ }
+
+ def setUp(self):
+ super(TestEndPointsFilterClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = endpoint_filter_client.EndPointsFilterClient(
+ fake_auth, 'identity', 'regionOne')
+
+ def _test_add_endpoint_to_project(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.add_endpoint_to_project,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ {},
+ bytes_body,
+ status=204,
+ project_id=3,
+ endpoint_id=4)
+
+ def _test_check_endpoint_in_project(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.check_endpoint_in_project,
+ 'tempest.lib.common.rest_client.RestClient.head',
+ {},
+ bytes_body,
+ status=204,
+ project_id=3,
+ endpoint_id=4)
+
+ def _test_list_projects_for_endpoint(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_projects_for_endpoint,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_PROJECTS_FOR_ENDPOINTS,
+ bytes_body,
+ status=200,
+ endpoint_id=3)
+
+ def _test_list_endpoints_in_project(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_endpoints_in_project,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_ENDPOINTS_FOR_PROJECTS,
+ bytes_body,
+ status=200,
+ project_id=4)
+
+ def _test_delete_endpoint_from_project(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.delete_endpoint_from_project,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ bytes_body,
+ status=204,
+ project_id=3,
+ endpoint_id=4)
+
+ def test_add_endpoint_to_project_with_str_body(self):
+ self._test_add_endpoint_to_project()
+
+ def test_add_endpoint_to_project_with_bytes_body(self):
+ self._test_add_endpoint_to_project(bytes_body=True)
+
+ def test_check_endpoint_in_project_with_str_body(self):
+ self._test_check_endpoint_in_project()
+
+ def test_check_endpoint_in_project_with_bytes_body(self):
+ self._test_check_endpoint_in_project(bytes_body=True)
+
+ def test_list_projects_for_endpoint_with_str_body(self):
+ self._test_list_projects_for_endpoint()
+
+ def test_list_projects_for_endpoint_with_bytes_body(self):
+ self._test_list_projects_for_endpoint(bytes_body=True)
+
+ def test_list_endpoints_in_project_with_str_body(self):
+ self._test_list_endpoints_in_project()
+
+ def test_list_endpoints_in_project_with_bytes_body(self):
+ self._test_list_endpoints_in_project(bytes_body=True)
+
+ def test_delete_endpoint_from_project(self):
+ self._test_delete_endpoint_from_project()
diff --git a/tempest/tests/lib/services/identity/v3/test_endpoint_groups_client.py b/tempest/tests/lib/services/identity/v3/test_endpoint_groups_client.py
new file mode 100644
index 0000000..c724f0a
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_endpoint_groups_client.py
@@ -0,0 +1,162 @@
+# Copyright 2017 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 tempest.lib.services.identity.v3 import endpoint_groups_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestEndPointGroupsClient(base.BaseServiceTest):
+ FAKE_CREATE_ENDPOINT_GROUP = {
+ "endpoint_group": {
+ "id": 1,
+ "name": "FAKE_ENDPOINT_GROUP",
+ "description": "FAKE SERVICE ENDPOINT GROUP",
+ "filters": {
+ "service_id": 1
+ }
+ }
+ }
+
+ FAKE_ENDPOINT_GROUP_INFO = {
+ "endpoint_group": {
+ "id": 1,
+ "name": "FAKE_ENDPOINT_GROUP",
+ "description": "FAKE SERVICE ENDPOINT GROUP",
+ "links": {
+ "self": "http://example.com/identity/v3/OS-EP-FILTER/" +
+ "endpoint_groups/1"
+ },
+ "filters": {
+ "service_id": 1
+ }
+ }
+ }
+
+ FAKE_LIST_ENDPOINT_GROUPS = {
+ "endpoint_groups": [
+ {
+ "id": 1,
+ "name": "SERVICE_GROUP1",
+ "description": "FAKE SERVICE ENDPOINT GROUP",
+ "links": {
+ "self": "http://example.com/identity/v3/OS-EP-FILTER/" +
+ "endpoint_groups/1"
+ },
+ "filters": {
+ "service_id": 1
+ }
+ },
+ {
+ "id": 2,
+ "name": "SERVICE_GROUP2",
+ "description": "FAKE SERVICE ENDPOINT GROUP",
+ "links": {
+ "self": "http://example.com/identity/v3/OS-EP-FILTER/" +
+ "endpoint_groups/2"
+ },
+ "filters": {
+ "service_id": 2
+ }
+ }
+ ]
+ }
+
+ def setUp(self):
+ super(TestEndPointGroupsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = endpoint_groups_client.EndPointGroupsClient(
+ fake_auth, 'identity', 'regionOne')
+
+ def _test_create_endpoint_group(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_endpoint_group,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_ENDPOINT_GROUP,
+ bytes_body,
+ status=201,
+ name="FAKE_ENDPOINT_GROUP",
+ filters={'service_id': "1"})
+
+ def _test_show_endpoint_group(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_endpoint_group,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_ENDPOINT_GROUP_INFO,
+ bytes_body,
+ endpoint_group_id="1")
+
+ def _test_check_endpoint_group(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.check_endpoint_group,
+ 'tempest.lib.common.rest_client.RestClient.head',
+ {},
+ bytes_body,
+ status=200,
+ endpoint_group_id="1")
+
+ def _test_update_endpoint_group(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_endpoint_group,
+ 'tempest.lib.common.rest_client.RestClient.patch',
+ self.FAKE_ENDPOINT_GROUP_INFO,
+ bytes_body,
+ endpoint_group_id="1",
+ name="NewName")
+
+ def _test_list_endpoint_groups(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_endpoint_groups,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_ENDPOINT_GROUPS,
+ bytes_body)
+
+ def test_create_endpoint_group_with_str_body(self):
+ self._test_create_endpoint_group()
+
+ def test_create_endpoint_group_with_bytes_body(self):
+ self._test_create_endpoint_group(bytes_body=True)
+
+ def test_show_endpoint_group_with_str_body(self):
+ self._test_show_endpoint_group()
+
+ def test_show_endpoint_group_with_bytes_body(self):
+ self._test_show_endpoint_group(bytes_body=True)
+
+ def test_check_endpoint_group_with_str_body(self):
+ self._test_check_endpoint_group()
+
+ def test_check_endpoint_group_with_bytes_body(self):
+ self._test_check_endpoint_group(bytes_body=True)
+
+ def test_list_endpoint_groups_with_str_body(self):
+ self._test_list_endpoint_groups()
+
+ def test_list_endpoint_groups_with_bytes_body(self):
+ self._test_list_endpoint_groups(bytes_body=True)
+
+ def test_update_endpoint_group_with_str_body(self):
+ self._test_update_endpoint_group()
+
+ def test_update_endpoint_group_with_bytes_body(self):
+ self._test_update_endpoint_group(bytes_body=True)
+
+ def test_delete_endpoint_group(self):
+ self.check_service_client_function(
+ self.client.delete_endpoint_group,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ endpoint_group_id="1",
+ status=204)
diff --git a/tempest/tests/lib/services/identity/v3/test_endpoints_client.py b/tempest/tests/lib/services/identity/v3/test_endpoints_client.py
new file mode 100644
index 0000000..ca15dd1
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_endpoints_client.py
@@ -0,0 +1,115 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# 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 endpoints_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestEndpointsClient(base.BaseServiceTest):
+ FAKE_CREATE_ENDPOINT = {
+ "endpoint": {
+ "id": 1,
+ "tenantId": 1,
+ "region": "North",
+ "type": "compute",
+ "publicURL": "https://compute.north.public.com/v1",
+ "internalURL": "https://compute.north.internal.com/v1",
+ "adminURL": "https://compute.north.internal.com/v1"
+ }
+ }
+
+ FAKE_LIST_ENDPOINTS = {
+ "endpoints": [
+ {
+ "id": 1,
+ "tenantId": "1",
+ "region": "North",
+ "type": "compute",
+ "publicURL": "https://compute.north.public.com/v1",
+ "internalURL": "https://compute.north.internal.com/v1",
+ "adminURL": "https://compute.north.internal.com/v1"
+ },
+ {
+ "id": 2,
+ "tenantId": "1",
+ "region": "South",
+ "type": "compute",
+ "publicURL": "https://compute.north.public.com/v1",
+ "internalURL": "https://compute.north.internal.com/v1",
+ "adminURL": "https://compute.north.internal.com/v1"
+ }
+ ]
+ }
+
+ FAKE_SERVICE_ID = "a4dc5060-f757-4662-b658-edd2aefbb41d"
+
+ def setUp(self):
+ super(TestEndpointsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = endpoints_client.EndPointsClient(fake_auth,
+ 'identity', 'regionOne')
+
+ def _test_create_endpoint(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_endpoint,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_ENDPOINT,
+ bytes_body,
+ status=201,
+ service_id="b344506af7644f6794d9cb316600b020",
+ region="region-demo",
+ publicurl="https://compute.north.public.com/v1",
+ adminurl="https://compute.north.internal.com/v1",
+ internalurl="https://compute.north.internal.com/v1")
+
+ def _test_list_endpoints(self, bytes_body=False, mock_args='endpoints',
+ **params):
+ self.check_service_client_function(
+ self.client.list_endpoints,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_ENDPOINTS,
+ bytes_body,
+ mock_args=[mock_args],
+ **params)
+
+ def test_create_endpoint_with_str_body(self):
+ self._test_create_endpoint()
+
+ def test_create_endpoint_with_bytes_body(self):
+ self._test_create_endpoint(bytes_body=True)
+
+ def test_list_endpoints_with_str_body(self):
+ self._test_list_endpoints()
+
+ def test_list_endpoints_with_bytes_body(self):
+ self._test_list_endpoints(bytes_body=True)
+
+ def test_list_endpoints_with_params(self):
+ # Run the test separately for each param, to avoid assertion error
+ # resulting from randomized params order.
+ mock_args = 'endpoints?service_id=%s' % self.FAKE_SERVICE_ID
+ self._test_list_endpoints(mock_args=mock_args,
+ service_id=self.FAKE_SERVICE_ID)
+
+ mock_args = 'endpoints?interface=public'
+ self._test_list_endpoints(mock_args=mock_args, interface='public')
+
+ def test_delete_endpoint(self):
+ self.check_service_client_function(
+ self.client.delete_endpoint,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ endpoint_id="b344506af7644f6794d9cb316600b020",
+ status=204)
diff --git a/tempest/tests/lib/services/identity/v3/test_groups_client.py b/tempest/tests/lib/services/identity/v3/test_groups_client.py
new file mode 100644
index 0000000..38cf3ae
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_groups_client.py
@@ -0,0 +1,213 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# 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 groups_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestGroupsClient(base.BaseServiceTest):
+ FAKE_CREATE_GROUP = {
+ 'group': {
+ 'description': 'Tempest Group Description',
+ 'domain_id': 'TempestDomain',
+ 'name': 'Tempest Group',
+ }
+ }
+
+ FAKE_GROUP_INFO = {
+ 'group': {
+ 'description': 'Tempest Group Description',
+ 'domain_id': 'TempestDomain',
+ 'id': '6e13e2068cf9466e98950595baf6bb35',
+ 'links': {
+ 'self': 'http://example.com/identity/v3/groups/' +
+ '6e13e2068cf9466e98950595baf6bb35'
+ },
+ 'name': 'Tempest Group',
+ }
+ }
+
+ FAKE_GROUP_LIST = {
+ 'links': {
+ 'self': 'http://example.com/identity/v3/groups',
+ 'previous': None,
+ 'next': None,
+ },
+ 'groups': [
+ {
+ 'description': 'Tempest Group One Description',
+ 'domain_id': 'TempestDomain',
+ 'id': '1c92f3453ed34291a074b87493455b8f',
+ 'links': {
+ 'self': 'http://example.com/identity/v3/groups/' +
+ '1c92f3453ed34291a074b87493455b8f'
+ },
+ 'name': 'Tempest Group One',
+ },
+ {
+ 'description': 'Tempest Group Two Description',
+ 'domain_id': 'TempestDomain',
+ 'id': 'ce9e7dafed3b4877a7d4466ed730a9ee',
+ 'links': {
+ 'self': 'http://example.com/identity/v3/groups/' +
+ 'ce9e7dafed3b4877a7d4466ed730a9ee'
+ },
+ 'name': 'Tempest Group Two',
+ },
+ ]
+ }
+
+ FAKE_USER_LIST = {
+ 'links': {
+ 'self': 'http://example.com/identity/v3/groups/' +
+ '6e13e2068cf9466e98950595baf6bb35/users',
+ 'previous': None,
+ 'next': None,
+ },
+ 'users': [
+ {
+ 'domain_id': 'TempestDomain',
+ 'description': 'Tempest Test User One Description',
+ 'enabled': True,
+ 'id': '642688fa65a84217b86cef3c063de2b9',
+ 'name': 'TempestUserOne',
+ 'links': {
+ 'self': 'http://example.com/identity/v3/users/' +
+ '642688fa65a84217b86cef3c063de2b9'
+ }
+ },
+ {
+ 'domain_id': 'TempestDomain',
+ 'description': 'Tempest Test User Two Description',
+ 'enabled': True,
+ 'id': '1048ead6f8ef4a859b44ffbce3ac0b52',
+ 'name': 'TempestUserTwo',
+ 'links': {
+ 'self': 'http://example.com/identity/v3/users/' +
+ '1048ead6f8ef4a859b44ffbce3ac0b52'
+ }
+ },
+ ]
+ }
+
+ def setUp(self):
+ super(TestGroupsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = groups_client.GroupsClient(fake_auth, 'identity',
+ 'regionOne')
+
+ def _test_create_group(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_group,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_GROUP,
+ bytes_body,
+ status=201,
+ )
+
+ def _test_show_group(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_group,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_GROUP_INFO,
+ bytes_body,
+ group_id='6e13e2068cf9466e98950595baf6bb35',
+ )
+
+ def _test_list_groups(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_groups,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_GROUP_LIST,
+ bytes_body,
+ )
+
+ def _test_update_group(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_group,
+ 'tempest.lib.common.rest_client.RestClient.patch',
+ self.FAKE_GROUP_INFO,
+ bytes_body,
+ group_id='6e13e2068cf9466e98950595baf6bb35',
+ name='NewName',
+ )
+
+ def _test_list_users_in_group(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_group_users,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_USER_LIST,
+ bytes_body,
+ group_id='6e13e2068cf9466e98950595baf6bb35',
+ )
+
+ def test_create_group_with_string_body(self):
+ self._test_create_group()
+
+ def test_create_group_with_bytes_body(self):
+ self._test_create_group(bytes_body=True)
+
+ def test_show_group_with_string_body(self):
+ self._test_show_group()
+
+ def test_show_group_with_bytes_body(self):
+ self._test_show_group(bytes_body=True)
+
+ def test_list_groups_with_string_body(self):
+ self._test_list_groups()
+
+ def test_list_groups_with_bytes_body(self):
+ self._test_list_groups(bytes_body=True)
+
+ def test_update_group_with_string_body(self):
+ self._test_update_group()
+
+ def test_update_group_with_bytes_body(self):
+ self._test_update_group(bytes_body=True)
+
+ def test_list_users_in_group_with_string_body(self):
+ self._test_list_users_in_group()
+
+ def test_list_users_in_group_with_bytes_body(self):
+ self._test_list_users_in_group(bytes_body=True)
+
+ def test_delete_group(self):
+ self.check_service_client_function(
+ self.client.delete_group,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ group_id='6e13e2068cf9466e98950595baf6bb35',
+ status=204,
+ )
+
+ def test_add_user_to_group(self):
+ self.check_service_client_function(
+ self.client.add_group_user,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ {},
+ status=204,
+ group_id='6e13e2068cf9466e98950595baf6bb35',
+ user_id='642688fa65a84217b86cef3c063de2b9',
+ )
+
+ def test_check_user_in_group(self):
+ self.check_service_client_function(
+ self.client.check_group_user_existence,
+ 'tempest.lib.common.rest_client.RestClient.head',
+ {},
+ status=204,
+ group_id='6e13e2068cf9466e98950595baf6bb35',
+ user_id='642688fa65a84217b86cef3c063de2b9',
+ )
diff --git a/tempest/tests/lib/services/identity/v3/test_identity_client.py b/tempest/tests/lib/services/identity/v3/test_identity_client.py
new file mode 100644
index 0000000..3739fe6
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_identity_client.py
@@ -0,0 +1,165 @@
+# Copyright 2016 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.services.identity.v3 import identity_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestIdentityClient(base.BaseServiceTest):
+ FAKE_TOKEN = {
+ "tokens": {
+ "id": "cbc36478b0bd8e67e89",
+ "name": "FakeToken",
+ "type": "token",
+ }
+ }
+
+ FAKE_API_INFO = {
+ "name": "API_info",
+ "type": "API",
+ "description": "test_description"
+ }
+
+ FAKE_AUTH_PROJECTS = {
+ "projects": [
+ {
+ "domain_id": "1789d1",
+ "enabled": True,
+ "id": "263fd9",
+ "links": {
+ "self": "https://example.com/identity/v3/projects/263fd9"
+ },
+ "name": "Test Group"
+ },
+ {
+ "domain_id": "1789d1",
+ "enabled": True,
+ "id": "50ef01",
+ "links": {
+ "self": "https://example.com/identity/v3/projects/50ef01"
+ },
+ "name": "Build Group"
+ }
+ ],
+ "links": {
+ "self": "https://example.com/identity/v3/auth/projects",
+ "previous": None,
+ "next": None
+ }
+ }
+
+ FAKE_AUTH_DOMAINS = {
+ "domains": [
+ {
+ "description": "my domain description",
+ "enabled": True,
+ "id": "1789d1",
+ "links": {
+ "self": "https://example.com/identity/v3/domains/1789d1"
+ },
+ "name": "my domain"
+ },
+ {
+ "description": "description of my other domain",
+ "enabled": True,
+ "id": "43e8da",
+ "links": {
+ "self": "https://example.com/identity/v3/domains/43e8da"
+ },
+ "name": "another domain"
+ }
+ ],
+ "links": {
+ "self": "https://example.com/identity/v3/auth/domains",
+ "previous": None,
+ "next": None
+ }
+ }
+
+ def setUp(self):
+ super(TestIdentityClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = identity_client.IdentityClient(fake_auth,
+ 'identity',
+ 'regionOne')
+
+ def _test_show_api_description(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_api_description,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_API_INFO,
+ bytes_body)
+
+ def _test_show_token(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_token,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_TOKEN,
+ bytes_body,
+ resp_token="cbc36478b0bd8e67e89")
+
+ def _test_list_auth_projects(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_auth_projects,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_AUTH_PROJECTS,
+ bytes_body)
+
+ def _test_list_auth_domains(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_auth_domains,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_AUTH_DOMAINS,
+ bytes_body)
+
+ def test_show_api_description_with_str_body(self):
+ self._test_show_api_description()
+
+ def test_show_api_description_with_bytes_body(self):
+ self._test_show_api_description(bytes_body=True)
+
+ def test_show_token_with_str_body(self):
+ self._test_show_token()
+
+ def test_show_token_with_bytes_body(self):
+ self._test_show_token(bytes_body=True)
+
+ def test_delete_token(self):
+ self.check_service_client_function(
+ self.client.delete_token,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ resp_token="cbc36478b0bd8e67e89",
+ status=204)
+
+ def test_check_token_existence(self):
+ self.check_service_client_function(
+ self.client.check_token_existence,
+ 'tempest.lib.common.rest_client.RestClient.head',
+ {},
+ resp_token="cbc36478b0bd8e67e89",
+ status=200)
+
+ def test_list_auth_projects_with_str_body(self):
+ self._test_list_auth_projects()
+
+ def test_list_auth_projects_with_bytes_body(self):
+ self._test_list_auth_projects(bytes_body=True)
+
+ def test_list_auth_domains_with_str_body(self):
+ self._test_list_auth_domains()
+
+ def test_list_auth_domains_with_bytes_body(self):
+ self._test_list_auth_domains(bytes_body=True)
diff --git a/tempest/tests/lib/services/identity/v3/test_inherited_roles_client.py b/tempest/tests/lib/services/identity/v3/test_inherited_roles_client.py
new file mode 100644
index 0000000..9da3cce
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_inherited_roles_client.py
@@ -0,0 +1,220 @@
+# Copyright 2016 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.services.identity.v3 import inherited_roles_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestInheritedRolesClient(base.BaseServiceTest):
+ FAKE_LIST_INHERITED_ROLES = {
+ "roles": [
+ {
+ "id": "1",
+ "name": "test",
+ "links": "example.com"
+ },
+ {
+ "id": "2",
+ "name": "test2",
+ "links": "example.com"
+ }
+ ]
+ }
+
+ def setUp(self):
+ super(TestInheritedRolesClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = inherited_roles_client.InheritedRolesClient(
+ fake_auth, 'identity', 'regionOne')
+
+ def _test_create_inherited_role_on_domains_user(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_inherited_role_on_domains_user,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ {},
+ bytes_body,
+ domain_id="b344506af7644f6794d9cb316600b020",
+ user_id="123",
+ role_id="1234",
+ status=204)
+
+ def _test_list_inherited_project_role_for_user_on_domain(
+ self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_inherited_project_role_for_user_on_domain,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_INHERITED_ROLES,
+ bytes_body,
+ domain_id="b344506af7644f6794d9cb316600b020",
+ user_id="123")
+
+ def _test_create_inherited_role_on_domains_group(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_inherited_role_on_domains_group,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ {},
+ bytes_body,
+ domain_id="b344506af7644f6794d9cb316600b020",
+ group_id="123",
+ role_id="1234",
+ status=204)
+
+ def _test_list_inherited_project_role_for_group_on_domain(
+ self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_inherited_project_role_for_group_on_domain,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_INHERITED_ROLES,
+ bytes_body,
+ domain_id="b344506af7644f6794d9cb316600b020",
+ group_id="123")
+
+ def _test_create_inherited_role_on_projects_user(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_inherited_role_on_projects_user,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ {},
+ bytes_body,
+ project_id="b344506af7644f6794d9cb316600b020",
+ user_id="123",
+ role_id="1234",
+ status=204)
+
+ def _test_create_inherited_role_on_projects_group(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_inherited_role_on_projects_group,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ {},
+ bytes_body,
+ project_id="b344506af7644f6794d9cb316600b020",
+ group_id="123",
+ role_id="1234",
+ status=204)
+
+ def test_create_inherited_role_on_domains_user_with_str_body(self):
+ self._test_create_inherited_role_on_domains_user()
+
+ def test_create_inherited_role_on_domains_user_with_bytes_body(self):
+ self._test_create_inherited_role_on_domains_user(bytes_body=True)
+
+ def test_create_inherited_role_on_domains_group_with_str_body(self):
+ self._test_create_inherited_role_on_domains_group()
+
+ def test_create_inherited_role_on_domains_group_with_bytes_body(self):
+ self._test_create_inherited_role_on_domains_group(bytes_body=True)
+
+ def test_create_inherited_role_on_projects_user_with_str_body(self):
+ self._test_create_inherited_role_on_projects_user()
+
+ def test_create_inherited_role_on_projects_group_with_bytes_body(self):
+ self._test_create_inherited_role_on_projects_group(bytes_body=True)
+
+ def test_list_inherited_project_role_for_user_on_domain_with_str_body(
+ self):
+ self._test_list_inherited_project_role_for_user_on_domain()
+
+ def test_list_inherited_project_role_for_user_on_domain_with_bytes_body(
+ self):
+ self._test_list_inherited_project_role_for_user_on_domain(
+ bytes_body=True)
+
+ def test_list_inherited_project_role_for_group_on_domain_with_str_body(
+ self):
+ self._test_list_inherited_project_role_for_group_on_domain()
+
+ def test_list_inherited_project_role_for_group_on_domain_with_bytes_body(
+ self):
+ self._test_list_inherited_project_role_for_group_on_domain(
+ bytes_body=True)
+
+ def test_delete_inherited_role_from_user_on_domain(self):
+ self.check_service_client_function(
+ self.client.delete_inherited_role_from_user_on_domain,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ domain_id="b344506af7644f6794d9cb316600b020",
+ user_id="123",
+ role_id="1234",
+ status=204)
+
+ def test_check_user_inherited_project_role_on_domain(self):
+ self.check_service_client_function(
+ self.client.check_user_inherited_project_role_on_domain,
+ 'tempest.lib.common.rest_client.RestClient.head',
+ {},
+ domain_id="b344506af7644f6794d9cb316600b020",
+ user_id="123",
+ role_id="1234",
+ status=204)
+
+ def test_delete_inherited_role_from_group_on_domain(self):
+ self.check_service_client_function(
+ self.client.delete_inherited_role_from_group_on_domain,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ domain_id="b344506af7644f6794d9cb316600b020",
+ group_id="123",
+ role_id="1234",
+ status=204)
+
+ def test_check_group_inherited_project_role_on_domain(self):
+ self.check_service_client_function(
+ self.client.check_group_inherited_project_role_on_domain,
+ 'tempest.lib.common.rest_client.RestClient.head',
+ {},
+ domain_id="b344506af7644f6794d9cb316600b020",
+ group_id="123",
+ role_id="1234",
+ status=204)
+
+ def test_delete_inherited_role_from_user_on_project(self):
+ self.check_service_client_function(
+ self.client.delete_inherited_role_from_user_on_project,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ project_id="b344506af7644f6794d9cb316600b020",
+ user_id="123",
+ role_id="1234",
+ status=204)
+
+ def test_check_user_has_flag_on_inherited_to_project(self):
+ self.check_service_client_function(
+ self.client.check_user_has_flag_on_inherited_to_project,
+ 'tempest.lib.common.rest_client.RestClient.head',
+ {},
+ project_id="b344506af7644f6794d9cb316600b020",
+ user_id="123",
+ role_id="1234",
+ status=204)
+
+ def test_delete_inherited_role_from_group_on_project(self):
+ self.check_service_client_function(
+ self.client.delete_inherited_role_from_group_on_project,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ project_id="b344506af7644f6794d9cb316600b020",
+ group_id="123",
+ role_id="1234",
+ status=204)
+
+ def test_check_group_has_flag_on_inherited_to_project(self):
+ self.check_service_client_function(
+ self.client.check_group_has_flag_on_inherited_to_project,
+ 'tempest.lib.common.rest_client.RestClient.head',
+ {},
+ project_id="b344506af7644f6794d9cb316600b020",
+ group_id="123",
+ role_id="1234",
+ status=204)
diff --git a/tempest/tests/lib/services/identity/v3/test_oauth_consumers_client.py b/tempest/tests/lib/services/identity/v3/test_oauth_consumers_client.py
new file mode 100644
index 0000000..8d53792
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_oauth_consumers_client.py
@@ -0,0 +1,160 @@
+# Copyright 2017 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 tempest.lib.services.identity.v3 import oauth_consumers_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestOAUTHConsumerClient(base.BaseServiceTest):
+ FAKE_CREATE_CONSUMER = {
+ "consumer": {
+ 'description': 'A fake description 1'
+ }
+
+ }
+
+ FAKE_CONSUMER_INFO = {
+ "consumer": {
+ 'id': '6392c7d3b7a2062e09a07aa377',
+ 'links': {
+ 'self': 'http://example.com/identity/v3/' +
+ 'OS-OAUTH1/consumers/g6f2l9'
+ },
+ 'description': 'A description that is fake'
+ }
+
+ }
+
+ FAKE_LIST_CONSUMERS = {
+ 'links': {
+ 'self': 'http://example.com/identity/v3/OS-OAUTH1/consumers/',
+ 'next': None,
+ 'previous': None
+ },
+ 'consumers': [
+ {
+ 'id': '6392c7d3b7a2062e09a07aa377',
+ 'links': {
+ 'self': 'http://example.com/identity/v3/' +
+ 'OS-OAUTH1/consumers/6b9f2g5'
+ },
+ 'description': 'A description that is fake'
+ },
+ {
+ 'id': '677a855c9e3eb3a3954b36aca6',
+ 'links': {
+ 'self': 'http://example.com/identity/v3/' +
+ 'OS-OAUTH1/consumers/6a9f2366'
+ },
+ 'description': 'A very fake description 2'
+ },
+ {
+ 'id': '9d3ac57b08d65e07826b5e506',
+ 'links': {
+ 'self': 'http://example.com/identity/v3/' +
+ 'OS-OAUTH1/consumers/626b5e506'
+ },
+ 'description': 'A very fake description 3'
+ },
+ {
+ 'id': 'b522d163b1a18e928aca9y426',
+ 'links': {
+ 'self': 'http://example.com/identity/v3/' +
+ 'OS-OAUTH1/consumers/g7ca9426'
+ },
+ 'description': 'A very fake description 4'
+ },
+ {
+ 'id': 'b7e47321b5ef9051f93c2049e',
+ 'links': {
+ 'self': 'http://example.com/identity/v3/' +
+ 'OS-OAUTH1/consumers/23d82049e'
+ },
+ 'description': 'A very fake description 5'
+ }
+ ]
+ }
+
+ def setUp(self):
+ super(TestOAUTHConsumerClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = oauth_consumers_client.OAUTHConsumerClient(fake_auth,
+ 'identity',
+ 'regionOne')
+
+ def _test_create_consumer(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_consumer,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_CONSUMER,
+ bytes_body,
+ description=self.FAKE_CREATE_CONSUMER["consumer"]["description"],
+ status=201)
+
+ def _test_show_consumer(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_consumer,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_CONSUMER_INFO,
+ bytes_body,
+ consumer_id="6392c7d3b7a2062e09a07aa377")
+
+ def _test_list_consumers(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_consumers,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_CONSUMERS,
+ bytes_body)
+
+ def _test_update_consumer(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_consumer,
+ 'tempest.lib.common.rest_client.RestClient.patch',
+ self.FAKE_CONSUMER_INFO,
+ bytes_body,
+ consumer_id="6392c7d3b7a2062e09a07aa377")
+
+ def test_create_consumer_with_str_body(self):
+ self._test_create_consumer()
+
+ def test_create_consumer_with_bytes_body(self):
+ self._test_create_consumer(bytes_body=True)
+
+ def test_show_consumer_with_str_body(self):
+ self._test_show_consumer()
+
+ def test_show_consumer_with_bytes_body(self):
+ self._test_show_consumer(bytes_body=True)
+
+ def test_list_consumers_with_str_body(self):
+ self._test_list_consumers()
+
+ def test_list_consumers_with_bytes_body(self):
+ self._test_list_consumers(bytes_body=True)
+
+ def test_update_consumer_with_str_body(self):
+ self._test_update_consumer()
+
+ def test_update_consumer_with_bytes_body(self):
+ self._test_update_consumer(bytes_body=True)
+
+ def test_delete_consumer(self):
+ self.check_service_client_function(
+ self.client.delete_consumer,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ consumer_id="6392c7d3b7a2062e09a07aa377",
+ status=204)
diff --git a/tempest/tests/lib/services/identity/v3/test_oauth_token_client.py b/tempest/tests/lib/services/identity/v3/test_oauth_token_client.py
new file mode 100644
index 0000000..420ea5f
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_oauth_token_client.py
@@ -0,0 +1,215 @@
+# Copyright 2017 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 fixtures
+
+from tempest.lib.services.identity.v3 import oauth_token_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib import fake_http
+from tempest.tests.lib.services import base
+
+
+class TestOAUTHTokenClient(base.BaseServiceTest):
+ FAKE_CREATE_REQUEST_TOKEN = {
+ 'oauth_token': '29971f',
+ 'oauth_token_secret': '238eb8',
+ 'oauth_expires_at': '2013-09-11T06:07:51.501805Z'
+ }
+
+ FAKE_AUTHORIZE_REQUEST_TOKEN = {
+ 'token': {
+ 'oauth_verifier': '8171'
+ }
+ }
+
+ FAKE_CREATE_ACCESS_TOKEN = {
+ 'oauth_token': 'accd36',
+ 'oauth_token_secret': 'aa47da',
+ 'oauth_expires_at': '2013-09-11T06:07:51.501805Z'
+ }
+
+ FAKE_ACCESS_TOKEN_INFO = {
+ 'access_token': {
+ 'consumer_id': '7fea2d',
+ 'id': '6be26a',
+ 'expires_at': '2013-09-11T06:07:51.501805Z',
+ 'links': {
+ 'roles': 'http://example.com/identity/v3/' +
+ 'users/ce9e07/OS-OAUTH1/access_tokens/6be26a/roles',
+ 'self': 'http://example.com/identity/v3/' +
+ 'users/ce9e07/OS-OAUTH1/access_tokens/6be26a'
+ },
+ 'project_id': 'b9fca3',
+ 'authorizing_user_id': 'ce9e07'
+ }
+ }
+
+ FAKE_LIST_ACCESS_TOKENS = {
+ 'access_tokens': [
+ {
+ 'consumer_id': '7fea2d',
+ 'id': '6be26a',
+ 'expires_at': '2013-09-11T06:07:51.501805Z',
+ 'links': {
+ 'roles': 'http://example.com/identity/v3/' +
+ 'users/ce9e07/OS-OAUTH1/access_tokens/' +
+ '6be26a/roles',
+ 'self': 'http://example.com/identity/v3/' +
+ 'users/ce9e07/OS-OAUTH1/access_tokens/6be26a'
+ },
+ 'project_id': 'b9fca3',
+ 'authorizing_user_id': 'ce9e07'
+ }
+ ],
+ 'links': {
+ 'next': None,
+ 'previous': None,
+ 'self': 'http://example.com/identity/v3/' +
+ 'users/ce9e07/OS-OAUTH1/access_tokens'
+ }
+ }
+
+ FAKE_LIST_ACCESS_TOKEN_ROLES = {
+ 'roles': [
+ {
+ 'id': '26b860',
+ 'domain_id': 'fake_domain',
+ 'links': {
+ 'self': 'http://example.com/identity/v3/' +
+ 'roles/26b860'
+ },
+ 'name': 'fake_role'
+ }
+ ],
+ 'links': {
+ 'next': None,
+ 'previous': None,
+ 'self': 'http://example.com/identity/v3/' +
+ 'users/ce9e07/OS-OAUTH1/access_tokens/6be26a/roles'
+ }
+ }
+
+ FAKE_ACCESS_TOKEN_ROLE_INFO = {
+ 'role': {
+ 'id': '26b860',
+ 'domain_id': 'fake_domain',
+ 'links': {
+ 'self': 'http://example.com/identity/v3/' +
+ 'roles/26b860'
+ },
+ 'name': 'fake_role'
+ }
+ }
+
+ def setUp(self):
+ super(TestOAUTHTokenClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = oauth_token_client.OAUTHTokenClient(fake_auth,
+ 'identity',
+ 'regionOne')
+
+ def _mock_token_response(self, body):
+ temp_response = [key + '=' + value for key, value in body.items()]
+ return '&'.join(temp_response)
+
+ def _test_authorize_request_token(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.authorize_request_token,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ self.FAKE_AUTHORIZE_REQUEST_TOKEN,
+ bytes_body,
+ request_token_id=self.FAKE_CREATE_REQUEST_TOKEN['oauth_token'],
+ role_ids=['26b860'],
+ status=200)
+
+ def test_create_request_token(self):
+ mock_resp = self._mock_token_response(self.FAKE_CREATE_REQUEST_TOKEN)
+ resp = fake_http.fake_http_response(None, status=201), mock_resp
+ self.useFixture(fixtures.MockPatch(
+ 'tempest.lib.common.rest_client.RestClient.post',
+ return_value=resp))
+
+ resp = self.client.create_request_token(
+ consumer_key='12345',
+ consumer_secret='23456',
+ project_id='c8f58432c6f00162f04d3250f')
+ self.assertEqual(self.FAKE_CREATE_REQUEST_TOKEN, resp)
+
+ def test_authorize_token_request_with_str_body(self):
+ self._test_authorize_request_token()
+
+ def test_authorize_token_request_with_bytes_body(self):
+ self._test_authorize_request_token(bytes_body=True)
+
+ def test_create_access_token(self):
+ mock_resp = self._mock_token_response(self.FAKE_CREATE_ACCESS_TOKEN)
+ req_secret = self.FAKE_CREATE_REQUEST_TOKEN['oauth_token_secret']
+ resp = fake_http.fake_http_response(None, status=201), mock_resp
+ self.useFixture(fixtures.MockPatch(
+ 'tempest.lib.common.rest_client.RestClient.post',
+ return_value=resp))
+
+ resp = self.client.create_access_token(
+ consumer_key='12345',
+ consumer_secret='23456',
+ request_key=self.FAKE_CREATE_REQUEST_TOKEN['oauth_token'],
+ request_secret=req_secret,
+ oauth_verifier='8171')
+ self.assertEqual(self.FAKE_CREATE_ACCESS_TOKEN, resp)
+
+ def test_get_access_token(self):
+ self.check_service_client_function(
+ self.client.get_access_token,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_ACCESS_TOKEN_INFO,
+ user_id='ce9e07',
+ access_token_id=self.FAKE_ACCESS_TOKEN_INFO['access_token']['id'],
+ status=200)
+
+ def test_list_access_tokens(self):
+ self.check_service_client_function(
+ self.client.list_access_tokens,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_ACCESS_TOKENS,
+ user_id='ce9e07',
+ status=200)
+
+ def test_revoke_access_token(self):
+ self.check_service_client_function(
+ self.client.revoke_access_token,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ user_id=self.FAKE_ACCESS_TOKEN_INFO['access_token']['consumer_id'],
+ access_token_id=self.FAKE_ACCESS_TOKEN_INFO['access_token']['id'],
+ status=204)
+
+ def test_list_access_token_roles(self):
+ self.check_service_client_function(
+ self.client.list_access_token_roles,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_ACCESS_TOKEN_ROLES,
+ user_id='ce9e07',
+ access_token_id=self.FAKE_ACCESS_TOKEN_INFO['access_token']['id'],
+ status=200)
+
+ def test_get_access_token_role(self):
+ self.check_service_client_function(
+ self.client.get_access_token_role,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_ACCESS_TOKEN_ROLE_INFO,
+ user_id='ce9e07',
+ access_token_id=self.FAKE_ACCESS_TOKEN_INFO['access_token']['id'],
+ role_id=self.FAKE_ACCESS_TOKEN_ROLE_INFO['role']['id'],
+ status=200)
diff --git a/tempest/tests/lib/services/identity/v3/test_policies_client.py b/tempest/tests/lib/services/identity/v3/test_policies_client.py
new file mode 100644
index 0000000..0237475
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_policies_client.py
@@ -0,0 +1,240 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# 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 policies_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestPoliciesClient(base.BaseServiceTest):
+ FAKE_CREATE_POLICY = {
+ "policy": {
+ "blob": "{'foobar_user': 'role:compute-user'}",
+ "project_id": "0426ac1e48f642ef9544c2251e07e261",
+ "type": "application/json",
+ "user_id": "0ffd248c55b443eaac5253b4e9cbf9b5"
+ }
+ }
+
+ FAKE_POLICY_INFO = {
+ "policy": {
+ "blob": {
+ "foobar_user": [
+ "role:compute-user"
+ ]
+ },
+ "id": "717273",
+ "links": {
+ "self": "http://example.com/identity/v3/policies/717273"
+ },
+ "project_id": "456789",
+ "type": "application/json",
+ "user_id": "616263"
+ }
+ }
+
+ FAKE_LIST_POLICIES = {
+ "links": {
+ "next": None,
+ "previous": None,
+ "self": "http://example.com/identity/v3/policies"
+ },
+ "policies": [
+ {
+ "blob": {
+ "foobar_user": [
+ "role:compute-user"
+ ]
+ },
+ "id": "717273",
+ "links": {
+ "self": "http://example.com/identity/v3/policies/717273"
+ },
+ "project_id": "456789",
+ "type": "application/json",
+ "user_id": "616263"
+ },
+ {
+ "blob": {
+ "foobar_user": [
+ "role:compute-user"
+ ]
+ },
+ "id": "717274",
+ "links": {
+ "self": "http://example.com/identity/v3/policies/717274"
+ },
+ "project_id": "456789",
+ "type": "application/json",
+ "user_id": "616263"
+ }
+ ]
+ }
+ FAKE_ENDPOINT_ID = "234789"
+ FAKE_SERVICE_ID = "556782"
+ FAKE_POLICY_ID = "717273"
+ FAKE_REGION_ID = "73"
+
+ def setUp(self):
+ super(TestPoliciesClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = policies_client.PoliciesClient(fake_auth,
+ 'identity', 'regionOne')
+
+ def _test_create_policy(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_policy,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_POLICY,
+ bytes_body,
+ status=201)
+
+ def _test_show_policy(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_policy,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_POLICY_INFO,
+ bytes_body,
+ policy_id="717273")
+
+ def _test_list_policies(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_policies,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_POLICIES,
+ bytes_body)
+
+ def _test_update_policy(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_policy,
+ 'tempest.lib.common.rest_client.RestClient.patch',
+ self.FAKE_POLICY_INFO,
+ bytes_body,
+ policy_id="717273")
+
+ def test_create_policy_with_str_body(self):
+ self._test_create_policy()
+
+ def test_create_policy_with_bytes_body(self):
+ self._test_create_policy(bytes_body=True)
+
+ def test_show_policy_with_str_body(self):
+ self._test_show_policy()
+
+ def test_show_policy_with_bytes_body(self):
+ self._test_show_policy(bytes_body=True)
+
+ def test_list_policies_with_str_body(self):
+ self._test_list_policies()
+
+ def test_list_policies_with_bytes_body(self):
+ self._test_list_policies(bytes_body=True)
+
+ def test_update_policy_with_str_body(self):
+ self._test_update_policy()
+
+ def test_update_policy_with_bytes_body(self):
+ self._test_update_policy(bytes_body=True)
+
+ def test_delete_policy(self):
+ self.check_service_client_function(
+ self.client.delete_policy,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ policy_id="717273",
+ status=204)
+
+ def test_update_policy_association_for_endpoint(self):
+ self.check_service_client_function(
+ self.client.update_policy_association_for_endpoint,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ {},
+ policy_id=self.FAKE_POLICY_ID,
+ endpoint_id=self.FAKE_ENDPOINT_ID,
+ status=204)
+
+ def test_show_policy_association_for_endpoint(self):
+ self.check_service_client_function(
+ self.client.show_policy_association_for_endpoint,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {},
+ policy_id=self.FAKE_POLICY_ID,
+ endpoint_id=self.FAKE_ENDPOINT_ID,
+ status=204)
+
+ def test_delete_policy_association_for_endpoint(self):
+ self.check_service_client_function(
+ self.client.delete_policy_association_for_endpoint,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ policy_id=self.FAKE_POLICY_ID,
+ endpoint_id=self.FAKE_ENDPOINT_ID,
+ status=204)
+
+ def test_update_policy_association_for_service(self):
+ self.check_service_client_function(
+ self.client.update_policy_association_for_service,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ {},
+ policy_id=self.FAKE_POLICY_ID,
+ service_id=self.FAKE_SERVICE_ID,
+ status=204)
+
+ def test_show_policy_association_for_service(self):
+ self.check_service_client_function(
+ self.client.show_policy_association_for_service,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {},
+ policy_id=self.FAKE_POLICY_ID,
+ service_id=self.FAKE_SERVICE_ID,
+ status=204)
+
+ def test_delete_policy_association_for_service(self):
+ self.check_service_client_function(
+ self.client.delete_policy_association_for_service,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ policy_id=self.FAKE_POLICY_ID,
+ service_id=self.FAKE_SERVICE_ID,
+ status=204)
+
+ def test_update_policy_association_for_region_and_service(self):
+ self.check_service_client_function(
+ self.client.update_policy_association_for_region_and_service,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ {},
+ policy_id=self.FAKE_POLICY_ID,
+ service_id=self.FAKE_SERVICE_ID,
+ region_id=self.FAKE_REGION_ID,
+ status=204)
+
+ def test_show_policy_association_for_region_and_service(self):
+ self.check_service_client_function(
+ self.client.show_policy_association_for_region_and_service,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {},
+ policy_id=self.FAKE_POLICY_ID,
+ service_id=self.FAKE_SERVICE_ID,
+ region_id=self.FAKE_REGION_ID,
+ status=204)
+
+ def test_delete_policy_association_for_region_and_service(self):
+ self.check_service_client_function(
+ self.client.delete_policy_association_for_region_and_service,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ policy_id=self.FAKE_POLICY_ID,
+ service_id=self.FAKE_SERVICE_ID,
+ region_id=self.FAKE_REGION_ID,
+ 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/identity/v3/test_projects_client.py b/tempest/tests/lib/services/identity/v3/test_projects_client.py
new file mode 100644
index 0000000..6ffbcde
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_projects_client.py
@@ -0,0 +1,178 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# 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 projects_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestProjectsClient(base.BaseServiceTest):
+ FAKE_CREATE_PROJECT = {
+ "project": {
+ "description": "My new project",
+ "domain_id": "default",
+ "enabled": True,
+ "is_domain": False,
+ "name": "myNewProject"
+ }
+ }
+
+ FAKE_PROJECT_INFO = {
+ "project": {
+ "is_domain": False,
+ "description": None,
+ "domain_id": "default",
+ "enabled": True,
+ "id": "0c4e939acacf4376bdcd1129f1a054ad",
+ "links": {
+ "self": "http://example.com/identity/v3/projects/0c4e" +
+ "939acacf4376bdcd1129f1a054ad"
+ },
+ "name": "admin",
+ "parent_id": "default"
+ }
+ }
+
+ FAKE_LIST_PROJECTS = {
+ "links": {
+ "next": None,
+ "previous": None,
+ "self": "http://example.com/identity/v3/projects"
+ },
+ "projects": [
+ {
+ "is_domain": False,
+ "description": None,
+ "domain_id": "default",
+ "enabled": True,
+ "id": "0c4e939acacf4376bdcd1129f1a054ad",
+ "links": {
+ "self": "http://example.com/identity/v3/projects" +
+ "/0c4e939acacf4376bdcd1129f1a054ad"
+ },
+ "name": "admin",
+ "parent_id": None
+ },
+ {
+ "is_domain": False,
+ "description": None,
+ "domain_id": "default",
+ "enabled": True,
+ "id": "0cbd49cbf76d405d9c86562e1d579bd3",
+ "links": {
+ "self": "http://example.com/identity/v3/projects" +
+ "/0cbd49cbf76d405d9c86562e1d579bd3"
+ },
+ "name": "demo",
+ "parent_id": None
+ },
+ {
+ "is_domain": False,
+ "description": None,
+ "domain_id": "default",
+ "enabled": True,
+ "id": "2db68fed84324f29bb73130c6c2094fb",
+ "links": {
+ "self": "http://example.com/identity/v3/projects" +
+ "/2db68fed84324f29bb73130c6c2094fb"
+ },
+ "name": "swifttenanttest2",
+ "parent_id": None
+ },
+ {
+ "is_domain": False,
+ "description": None,
+ "domain_id": "default",
+ "enabled": True,
+ "id": "3d594eb0f04741069dbbb521635b21c7",
+ "links": {
+ "self": "http://example.com/identity/v3/projects" +
+ "/3d594eb0f04741069dbbb521635b21c7"
+ },
+ "name": "service",
+ "parent_id": None
+ }
+ ]
+ }
+
+ def setUp(self):
+ super(TestProjectsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = projects_client.ProjectsClient(fake_auth,
+ 'identity',
+ 'regionOne')
+
+ def _test_create_project(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_project,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_PROJECT,
+ bytes_body,
+ name=self.FAKE_CREATE_PROJECT["project"]["name"],
+ status=201)
+
+ def _test_show_project(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_project,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_PROJECT_INFO,
+ bytes_body,
+ project_id="0c4e939acacf4376bdcd1129f1a054ad")
+
+ def _test_list_projects(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_projects,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_PROJECTS,
+ bytes_body)
+
+ def _test_update_project(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_project,
+ 'tempest.lib.common.rest_client.RestClient.patch',
+ self.FAKE_PROJECT_INFO,
+ bytes_body,
+ project_id="0c4e939acacf4376bdcd1129f1a054ad")
+
+ def test_create_project_with_str_body(self):
+ self._test_create_project()
+
+ def test_create_project_with_bytes_body(self):
+ self._test_create_project(bytes_body=True)
+
+ def test_show_project_with_str_body(self):
+ self._test_show_project()
+
+ def test_show_project_with_bytes_body(self):
+ self._test_show_project(bytes_body=True)
+
+ def test_list_projects_with_str_body(self):
+ self._test_list_projects()
+
+ def test_list_projects_with_bytes_body(self):
+ self._test_list_projects(bytes_body=True)
+
+ def test_update_project_with_str_body(self):
+ self._test_update_project()
+
+ def test_update_project_with_bytes_body(self):
+ self._test_update_project(bytes_body=True)
+
+ def test_delete_project(self):
+ self.check_service_client_function(
+ self.client.delete_project,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ project_id="0c4e939acacf4376bdcd1129f1a054ad",
+ status=204)
diff --git a/tempest/tests/lib/services/identity/v3/test_regions_client.py b/tempest/tests/lib/services/identity/v3/test_regions_client.py
new file mode 100644
index 0000000..a2cb86f
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_regions_client.py
@@ -0,0 +1,125 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# 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 regions_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestRegionsClient(base.BaseServiceTest):
+ FAKE_CREATE_REGION = {
+ "region": {
+ "description": "My subregion",
+ "id": "RegionOneSubRegion",
+ "parent_region_id": "RegionOne"
+ }
+ }
+
+ FAKE_REGION_INFO = {
+ "region": {
+ "description": "My subregion 3",
+ "id": "RegionThree",
+ "links": {
+ "self": "http://example.com/identity/v3/regions/RegionThree"
+ },
+ "parent_region_id": "RegionOne"
+ }
+ }
+
+ FAKE_LIST_REGIONS = {
+ "links": {
+ "next": None,
+ "previous": None,
+ "self": "http://example.com/identity/v3/regions"
+ },
+ "regions": [
+ {
+ "description": "",
+ "id": "RegionOne",
+ "links": {
+ "self": "http://example.com/identity/v3/regions/RegionOne"
+ },
+ "parent_region_id": None
+ }
+ ]
+ }
+
+ def setUp(self):
+ super(TestRegionsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = regions_client.RegionsClient(fake_auth, 'identity',
+ 'regionOne')
+
+ def _test_create_region(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_region,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_REGION,
+ bytes_body,
+ status=201)
+
+ def _test_show_region(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_region,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_REGION_INFO,
+ bytes_body,
+ region_id="RegionThree")
+
+ def _test_list_regions(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_regions,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_REGIONS,
+ bytes_body)
+
+ def _test_update_region(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_region,
+ 'tempest.lib.common.rest_client.RestClient.patch',
+ self.FAKE_REGION_INFO,
+ bytes_body,
+ region_id="RegionThree")
+
+ def test_create_region_with_str_body(self):
+ self._test_create_region()
+
+ def test_create_region_with_bytes_body(self):
+ self._test_create_region(bytes_body=True)
+
+ def test_show_region_with_str_body(self):
+ self._test_show_region()
+
+ def test_show_region_with_bytes_body(self):
+ self._test_show_region(bytes_body=True)
+
+ def test_list_regions_with_str_body(self):
+ self._test_list_regions()
+
+ def test_list_regions_with_bytes_body(self):
+ self._test_list_regions(bytes_body=True)
+
+ def test_update_region_with_str_body(self):
+ self._test_update_region()
+
+ def test_update_region_with_bytes_body(self):
+ self._test_update_region(bytes_body=True)
+
+ def test_delete_region(self):
+ self.check_service_client_function(
+ self.client.delete_region,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ region_id="RegionThree",
+ status=204)
diff --git a/tempest/tests/lib/services/identity/v3/test_role_assignments_client.py b/tempest/tests/lib/services/identity/v3/test_role_assignments_client.py
new file mode 100644
index 0000000..7d304c1
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_role_assignments_client.py
@@ -0,0 +1,206 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# 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 role_assignments_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestRoleAssignmentsClient(base.BaseServiceTest):
+
+ FAKE_USER_ID = "313234"
+ FAKE_GROUP_ID = "101112"
+
+ FAKE_ROLE1_ID = "123456"
+ FAKE_ROLE2_ID = "123457"
+
+ FAKE_PROJECT_ID = "456789"
+ FAKE_DOMAIN_ID = "102030"
+
+ FAKE_USER_PROJECT_ASSIGNMENT = {
+ "links": {
+ "assignment": "http://example.com/identity/v3/projects/"
+ "%s/users/%s/roles/%s" % (FAKE_PROJECT_ID,
+ FAKE_USER_ID,
+ FAKE_ROLE2_ID)
+ },
+ "role": {
+ "id": FAKE_ROLE2_ID
+ },
+ "scope": {
+ "project": {
+ "id": FAKE_PROJECT_ID
+ }
+ },
+ "user": {
+ "id": FAKE_USER_ID
+ }
+ }
+
+ FAKE_GROUP_PROJECT_ASSIGNMENT = {
+ "links": {
+ "assignment": "http://example.com/identity/v3/projects/"
+ "%s/groups/%s/roles/%s" % (FAKE_PROJECT_ID,
+ FAKE_GROUP_ID,
+ FAKE_ROLE1_ID)
+ },
+ "role": {
+ "id": FAKE_ROLE1_ID
+ },
+ "scope": {
+ "project": {
+ "id": FAKE_PROJECT_ID
+ }
+ },
+ "group": {
+ "id": FAKE_GROUP_ID
+ }
+ }
+
+ FAKE_USER_PROJECT_EFFECTIVE_ASSIGNMENT = {
+ "links": {
+ "assignment": "http://example.com/identity/v3/projects/"
+ "%s/groups/%s/roles/%s" % (FAKE_PROJECT_ID,
+ FAKE_GROUP_ID,
+ FAKE_ROLE1_ID),
+ "membership": "http://example.com/identity/v3/groups/"
+ "%s/users/%s" % (FAKE_GROUP_ID, FAKE_USER_ID)
+ },
+ "role": {
+ "id": FAKE_ROLE1_ID
+ },
+ "scope": {
+ "project": {
+ "id": FAKE_PROJECT_ID
+ }
+ },
+ "user": {
+ "id": FAKE_USER_ID
+ }
+ }
+
+ FAKE_USER_DOMAIN_ASSIGNMENT = {
+ "links": {
+ "assignment": "http://example.com/identity/v3/domains/"
+ "%s/users/%s/roles/%s" % (FAKE_DOMAIN_ID,
+ FAKE_USER_ID,
+ FAKE_ROLE1_ID)
+ },
+ "role": {
+ "id": FAKE_ROLE1_ID
+ },
+ "scope": {
+ "domain": {
+ "id": FAKE_DOMAIN_ID
+ }
+ },
+ "user": {
+ "id": FAKE_USER_ID
+ }
+ }
+
+ FAKE_GROUP_PROJECT_ASSIGNMENTS = {
+ "role_assignments": [
+ FAKE_GROUP_PROJECT_ASSIGNMENT
+ ],
+ "links": {
+ "self": "http://example.com/identity/v3/role_assignments?"
+ "scope.project.id=%s&group.id=%s&effective" % (
+ FAKE_PROJECT_ID, FAKE_GROUP_ID),
+ "previous": None,
+ "next": None
+ }
+ }
+
+ FAKE_USER_PROJECT_EFFECTIVE_ASSIGNMENTS = {
+ "role_assignments": [
+ FAKE_USER_PROJECT_ASSIGNMENT,
+ FAKE_USER_PROJECT_EFFECTIVE_ASSIGNMENT
+ ],
+ "links": {
+ "self": "http://example.com/identity/v3/role_assignments?"
+ "scope.project.id=%s&user.id=%s&effective" % (
+ FAKE_PROJECT_ID, FAKE_USER_ID),
+ "previous": None,
+ "next": None
+ }
+ }
+
+ FAKE_USER_DOMAIN_ASSIGNMENTS = {
+ "role_assignments": [
+ FAKE_USER_DOMAIN_ASSIGNMENT
+ ],
+ "links": {
+ "self": "http://example.com/identity/v3/role_assignments?"
+ "scope.domain.id=%s&user.id=%s&effective" % (
+ FAKE_DOMAIN_ID, FAKE_USER_ID),
+ "previous": None,
+ "next": None
+ }
+ }
+
+ def setUp(self):
+ super(TestRoleAssignmentsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = role_assignments_client.RoleAssignmentsClient(
+ fake_auth, 'identity', 'regionOne')
+
+ def _test_list_user_project_effective_assignments(self, bytes_body=False):
+ params = {'scope.project.id': self.FAKE_PROJECT_ID,
+ 'user.id': self.FAKE_USER_ID}
+ self.check_service_client_function(
+ self.client.list_role_assignments,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_USER_PROJECT_EFFECTIVE_ASSIGNMENTS,
+ bytes_body,
+ effective=True,
+ **params)
+
+ def test_list_user_project_effective_assignments_with_str_body(self):
+ self._test_list_user_project_effective_assignments()
+
+ def test_list_user_project_effective_assignments_with_bytes_body(self):
+ self._test_list_user_project_effective_assignments(bytes_body=True)
+
+ def _test_list_group_project_assignments(self, bytes_body=False):
+ params = {'scope.project.id': self.FAKE_PROJECT_ID,
+ 'group.id': self.FAKE_GROUP_ID}
+ self.check_service_client_function(
+ self.client.list_role_assignments,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_GROUP_PROJECT_ASSIGNMENTS,
+ bytes_body,
+ **params)
+
+ def test_list_group_project_assignments_with_str_body(self):
+ self._test_list_group_project_assignments()
+
+ def test_list_group_project_assignments_with_bytes_body(self):
+ self._test_list_group_project_assignments(bytes_body=True)
+
+ def _test_list_user_domain_assignments(self, bytes_body=False):
+ params = {'scope.domain.id': self.FAKE_DOMAIN_ID,
+ 'user.id': self.FAKE_USER_ID}
+ self.check_service_client_function(
+ self.client.list_role_assignments,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_GROUP_PROJECT_ASSIGNMENTS,
+ bytes_body,
+ **params)
+
+ def test_list_user_domain_assignments_with_str_body(self):
+ self._test_list_user_domain_assignments()
+
+ def test_list_user_domain_assignments_with_bytes_body(self):
+ self._test_list_user_domain_assignments(bytes_body=True)
diff --git a/tempest/tests/lib/services/identity/v3/test_roles_client.py b/tempest/tests/lib/services/identity/v3/test_roles_client.py
new file mode 100644
index 0000000..8d6bb42
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_roles_client.py
@@ -0,0 +1,508 @@
+# Copyright 2016 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.services.identity.v3 import roles_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestRolesClient(base.BaseServiceTest):
+
+ FAKE_ROLE_ID = "1"
+ FAKE_ROLE_NAME = "test"
+ FAKE_DOMAIN_ID = "1"
+
+ FAKE_ROLE_ID_2 = "2"
+ FAKE_ROLE_NAME_2 = "test2"
+
+ FAKE_ROLE_ID_3 = "3"
+ FAKE_ROLE_NAME_3 = "test3"
+
+ FAKE_ROLE_ID_4 = "4"
+ FAKE_ROLE_NAME_4 = "test4"
+
+ FAKE_ROLE_ID_5 = "5"
+ FAKE_ROLE_NAME_5 = "test5"
+
+ FAKE_ROLE_ID_6 = "6"
+ FAKE_ROLE_NAME_6 = "test6"
+
+ FAKE_ROLE_INFO = {
+ "role": {
+ "domain_id": FAKE_DOMAIN_ID,
+ "id": FAKE_ROLE_ID,
+ "name": FAKE_ROLE_NAME,
+ "links": {
+ "self": "http://example.com/identity/v3/roles/%s" % (
+ FAKE_ROLE_ID)
+ }
+ }
+ }
+
+ FAKE_ROLE_INFO_2 = {
+ "role": {
+ "domain_id": FAKE_DOMAIN_ID,
+ "id": FAKE_ROLE_ID_2,
+ "name": FAKE_ROLE_NAME_2,
+ "links": {
+ "self": "http://example.com/identity/v3/roles/%s" % (
+ FAKE_ROLE_ID_2)
+ }
+ }
+ }
+
+ FAKE_LIST_ROLES = {"roles": [FAKE_ROLE_INFO, FAKE_ROLE_INFO_2]}
+
+ FAKE_ROLE_INFERENCE_RULE = {
+ "role_inference": {
+ "prior_role": {
+ "id": FAKE_ROLE_ID,
+ "name": FAKE_ROLE_NAME,
+ "links": {
+ "self": "http://example.com/identity/v3/roles/%s" % (
+ FAKE_ROLE_ID)
+ }
+ },
+ "implies": {
+ "id": FAKE_ROLE_ID_2,
+ "name": FAKE_ROLE_NAME_2,
+ "links": {
+ "self": "http://example.com/identity/v3/roles/%s" % (
+ FAKE_ROLE_ID_2)
+ }
+ }
+ },
+ "links": {
+ "self": "http://example.com/identity/v3/roles/"
+ "%s/implies/%s" % (FAKE_ROLE_ID, FAKE_ROLE_ID_2)
+ }
+ }
+
+ COMMON_FAKE_LIST_ROLE_INFERENCE_RULES = [
+ {
+ "prior_role": {
+ "id": FAKE_ROLE_ID,
+ "name": FAKE_ROLE_NAME,
+ "links": {
+ "self": "http://example.com/identity/v3/roles/%s" % (
+ FAKE_ROLE_ID)
+ }
+ },
+ "implies": [
+ {
+ "id": FAKE_ROLE_ID_2,
+ "name": FAKE_ROLE_NAME_2,
+ "links": {
+ "self": "http://example.com/identity/v3/roles/%s" % (
+ FAKE_ROLE_ID_2)
+ }
+ },
+ {
+ "id": FAKE_ROLE_ID_3,
+ "name": FAKE_ROLE_NAME_3,
+ "links": {
+ "self": "http://example.com/identity/v3/roles/%s" % (
+ FAKE_ROLE_ID_3)
+ }
+ }
+ ]
+ },
+ {
+ "prior_role": {
+ "id": FAKE_ROLE_ID_4,
+ "name": FAKE_ROLE_NAME_4,
+ "links": {
+ "self": "http://example.com/identity/v3/roles/%s" % (
+ FAKE_ROLE_ID_4)
+ }
+ },
+ "implies": [
+ {
+ "id": FAKE_ROLE_ID_5,
+ "name": FAKE_ROLE_NAME_5,
+ "links": {
+ "self": "http://example.com/identity/v3/roles/%s" % (
+ FAKE_ROLE_ID_5)
+ }
+ },
+ {
+ "id": FAKE_ROLE_ID_6,
+ "name": FAKE_ROLE_NAME_6,
+ "links": {
+ "self": "http://example.com/identity/v3/roles/%s" % (
+ FAKE_ROLE_ID_6)
+ }
+ }
+ ]
+ }
+ ]
+
+ FAKE_LIST_ROLE_INFERENCE_RULES = {
+ "role_inference": COMMON_FAKE_LIST_ROLE_INFERENCE_RULES[0],
+ "links": {
+ "self": "http://example.com/identity/v3/roles/"
+ "%s/implies" % FAKE_ROLE_ID
+ }
+ }
+
+ FAKE_LIST_ALL_ROLE_INFERENCE_RULES = {
+ "role_inferences": COMMON_FAKE_LIST_ROLE_INFERENCE_RULES,
+ "links": {
+ "self": "http://example.com/identity/v3/role_inferences"
+ }
+ }
+
+ def setUp(self):
+ super(TestRolesClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = roles_client.RolesClient(fake_auth,
+ 'identity', 'regionOne')
+
+ def _test_create_role(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_role,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_ROLE_INFO,
+ bytes_body,
+ domain_id=self.FAKE_DOMAIN_ID,
+ name=self.FAKE_ROLE_NAME,
+ status=201)
+
+ def _test_show_role(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_role,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_ROLE_INFO,
+ bytes_body,
+ role_id=self.FAKE_ROLE_ID)
+
+ def _test_list_roles(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_roles,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_ROLES,
+ bytes_body)
+
+ def _test_update_role(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_role,
+ 'tempest.lib.common.rest_client.RestClient.patch',
+ self.FAKE_ROLE_INFO,
+ bytes_body,
+ role_id=self.FAKE_ROLE_ID,
+ name=self.FAKE_ROLE_NAME)
+
+ def _test_create_user_role_on_project(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_user_role_on_project,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ {},
+ bytes_body,
+ project_id="b344506af7644f6794d9cb316600b020",
+ user_id="123",
+ role_id="1234",
+ status=204)
+
+ def _test_create_user_role_on_domain(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_user_role_on_domain,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ {},
+ bytes_body,
+ domain_id="b344506af7644f6794d9cb316600b020",
+ user_id="123",
+ role_id="1234",
+ status=204)
+
+ def _test_list_user_roles_on_project(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_user_roles_on_project,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_ROLES,
+ bytes_body,
+ project_id="b344506af7644f6794d9cb316600b020",
+ user_id="123")
+
+ def _test_list_user_roles_on_domain(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_user_roles_on_domain,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_ROLES,
+ bytes_body,
+ domain_id="b344506af7644f6794d9cb316600b020",
+ user_id="123")
+
+ def _test_create_group_role_on_project(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_group_role_on_project,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ {},
+ bytes_body,
+ project_id="b344506af7644f6794d9cb316600b020",
+ group_id="123",
+ role_id="1234",
+ status=204)
+
+ def _test_create_group_role_on_domain(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_group_role_on_domain,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ {},
+ bytes_body,
+ domain_id="b344506af7644f6794d9cb316600b020",
+ group_id="123",
+ role_id="1234",
+ status=204)
+
+ def _test_list_group_roles_on_project(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_group_roles_on_project,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_ROLES,
+ bytes_body,
+ project_id="b344506af7644f6794d9cb316600b020",
+ group_id="123")
+
+ def _test_list_group_roles_on_domain(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_group_roles_on_domain,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_ROLES,
+ bytes_body,
+ domain_id="b344506af7644f6794d9cb316600b020",
+ group_id="123")
+
+ def _test_create_role_inference_rule(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_role_inference_rule,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ self.FAKE_ROLE_INFERENCE_RULE,
+ bytes_body,
+ status=201,
+ prior_role=self.FAKE_ROLE_ID,
+ implies_role=self.FAKE_ROLE_ID_2)
+
+ def _test_show_role_inference_rule(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_role_inference_rule,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_ROLE_INFERENCE_RULE,
+ bytes_body,
+ prior_role=self.FAKE_ROLE_ID,
+ implies_role=self.FAKE_ROLE_ID_2)
+
+ def _test_list_role_inferences_rules(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_role_inferences_rules,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_ROLE_INFERENCE_RULES,
+ bytes_body,
+ prior_role=self.FAKE_ROLE_ID)
+
+ def _test_list_all_role_inference_rules(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_all_role_inference_rules,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_ALL_ROLE_INFERENCE_RULES,
+ bytes_body)
+
+ def test_create_role_with_str_body(self):
+ self._test_create_role()
+
+ def test_create_role_with_bytes_body(self):
+ self._test_create_role(bytes_body=True)
+
+ def test_show_role_with_str_body(self):
+ self._test_show_role()
+
+ def test_show_role_with_bytes_body(self):
+ self._test_show_role(bytes_body=True)
+
+ def test_list_roles_with_str_body(self):
+ self._test_list_roles()
+
+ def test_list_roles_with_bytes_body(self):
+ self._test_list_roles(bytes_body=True)
+
+ def test_update_role_with_str_body(self):
+ self._test_update_role()
+
+ def test_update_role_with_bytes_body(self):
+ self._test_update_role(bytes_body=True)
+
+ def test_delete_role(self):
+ self.check_service_client_function(
+ self.client.delete_role,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ role_id=self.FAKE_ROLE_ID,
+ status=204)
+
+ def test_create_user_role_on_project_with_str_body(self):
+ self._test_create_user_role_on_project()
+
+ def test_create_user_role_on_project_with_bytes_body(self):
+ self._test_create_user_role_on_project(bytes_body=True)
+
+ def test_create_user_role_on_domain_with_str_body(self):
+ self._test_create_user_role_on_domain()
+
+ def test_create_user_role_on_domain_with_bytes_body(self):
+ self._test_create_user_role_on_domain(bytes_body=True)
+
+ def test_create_group_role_on_domain_with_str_body(self):
+ self._test_create_group_role_on_domain()
+
+ def test_create_group_role_on_domain_with_bytes_body(self):
+ self._test_create_group_role_on_domain(bytes_body=True)
+
+ def test_list_user_roles_on_project_with_str_body(self):
+ self._test_list_user_roles_on_project()
+
+ def test_list_user_roles_on_project_with_bytes_body(self):
+ self._test_list_user_roles_on_project(bytes_body=True)
+
+ def test_list_user_roles_on_domain_with_str_body(self):
+ self._test_list_user_roles_on_domain()
+
+ def test_list_user_roles_on_domain_with_bytes_body(self):
+ self._test_list_user_roles_on_domain(bytes_body=True)
+
+ def test_list_group_roles_on_domain_with_str_body(self):
+ self._test_list_group_roles_on_domain()
+
+ def test_list_group_roles_on_domain_with_bytes_body(self):
+ self._test_list_group_roles_on_domain(bytes_body=True)
+
+ def test_delete_role_from_user_on_project(self):
+ self.check_service_client_function(
+ self.client.delete_role_from_user_on_project,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ project_id="b344506af7644f6794d9cb316600b020",
+ user_id="123",
+ role_id="1234",
+ status=204)
+
+ def test_delete_role_from_user_on_domain(self):
+ self.check_service_client_function(
+ self.client.delete_role_from_user_on_domain,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ domain_id="b344506af7644f6794d9cb316600b020",
+ user_id="123",
+ role_id="1234",
+ status=204)
+
+ def test_delete_role_from_group_on_project(self):
+ self.check_service_client_function(
+ self.client.delete_role_from_group_on_project,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ project_id="b344506af7644f6794d9cb316600b020",
+ group_id="123",
+ role_id="1234",
+ status=204)
+
+ def test_delete_role_from_group_on_domain(self):
+ self.check_service_client_function(
+ self.client.delete_role_from_group_on_domain,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ domain_id="b344506af7644f6794d9cb316600b020",
+ group_id="123",
+ role_id="1234",
+ status=204)
+
+ def test_check_user_role_existence_on_project(self):
+ self.check_service_client_function(
+ self.client.check_user_role_existence_on_project,
+ 'tempest.lib.common.rest_client.RestClient.head',
+ {},
+ project_id="b344506af7644f6794d9cb316600b020",
+ user_id="123",
+ role_id="1234",
+ status=204)
+
+ def test_check_user_role_existence_on_domain(self):
+ self.check_service_client_function(
+ self.client.check_user_role_existence_on_domain,
+ 'tempest.lib.common.rest_client.RestClient.head',
+ {},
+ domain_id="b344506af7644f6794d9cb316600b020",
+ user_id="123",
+ role_id="1234",
+ status=204)
+
+ def test_check_role_from_group_on_project_existence(self):
+ self.check_service_client_function(
+ self.client.check_role_from_group_on_project_existence,
+ 'tempest.lib.common.rest_client.RestClient.head',
+ {},
+ project_id="b344506af7644f6794d9cb316600b020",
+ group_id="123",
+ role_id="1234",
+ status=204)
+
+ def test_check_role_from_group_on_domain_existence(self):
+ self.check_service_client_function(
+ self.client.check_role_from_group_on_domain_existence,
+ 'tempest.lib.common.rest_client.RestClient.head',
+ {},
+ domain_id="b344506af7644f6794d9cb316600b020",
+ group_id="123",
+ role_id="1234",
+ status=204)
+
+ def test_create_role_inference_rule_with_str_body(self):
+ self._test_create_role_inference_rule()
+
+ def test_create_role_inference_rule_with_bytes_body(self):
+ self._test_create_role_inference_rule(bytes_body=True)
+
+ def test_show_role_inference_rule_with_str_body(self):
+ self._test_show_role_inference_rule()
+
+ def test_show_role_inference_rule_with_bytes_body(self):
+ self._test_show_role_inference_rule(bytes_body=True)
+
+ def test_list_role_inferences_rules_with_str_body(self):
+ self._test_list_role_inferences_rules()
+
+ def test_list_role_inferences_rules_with_bytes_body(self):
+ self._test_list_role_inferences_rules(bytes_body=True)
+
+ def test_check_role_inference_rule(self):
+ self.check_service_client_function(
+ self.client.check_role_inference_rule,
+ 'tempest.lib.common.rest_client.RestClient.head',
+ {},
+ status=204,
+ prior_role=self.FAKE_ROLE_ID,
+ implies_role=self.FAKE_ROLE_ID_2)
+
+ def test_delete_role_inference_rule(self):
+ self.check_service_client_function(
+ self.client.delete_role_inference_rule,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ status=204,
+ prior_role=self.FAKE_ROLE_ID,
+ implies_role=self.FAKE_ROLE_ID_2)
+
+ def test_list_all_role_inference_rules_with_str_body(self):
+ self._test_list_all_role_inference_rules()
+
+ def test_list_all_role_inference_rules_with_bytes_body(self):
+ self._test_list_all_role_inference_rules(bytes_body=True)
diff --git a/tempest/tests/lib/services/identity/v3/test_services_client.py b/tempest/tests/lib/services/identity/v3/test_services_client.py
new file mode 100644
index 0000000..b464644
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_services_client.py
@@ -0,0 +1,156 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# 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 services_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestServicesClient(base.BaseServiceTest):
+ FAKE_CREATE_SERVICE = {
+ "service": {
+ "type": "compute",
+ "name": "compute2",
+ "description": "Compute service 2"
+ }
+ }
+
+ FAKE_SERVICE_INFO = {
+ "service": {
+ "description": "Keystone Identity Service",
+ "enabled": True,
+ "id": "686766",
+ "links": {
+ "self": "http://example.com/identity/v3/services/686766"
+ },
+ "name": "keystone",
+ "type": "identity"
+ }
+ }
+
+ FAKE_LIST_SERVICES = {
+ "links": {
+ "next": None,
+ "previous": None,
+ "self": "http://example.com/identity/v3/services"
+ },
+ "services": [
+ {
+ "description": "Nova Compute Service",
+ "enabled": True,
+ "id": "1999c3",
+ "links": {
+ "self": "http://example.com/identity/v3/services/1999c3"
+ },
+ "name": "nova",
+ "type": "compute"
+ },
+ {
+ "description": "Cinder Volume Service V2",
+ "enabled": True,
+ "id": "392166",
+ "links": {
+ "self": "http://example.com/identity/v3/services/392166"
+ },
+ "name": "cinderv2",
+ "type": "volumev2"
+ },
+ {
+ "description": "Neutron Service",
+ "enabled": True,
+ "id": "4fe41a",
+ "links": {
+ "self": "http://example.com/identity/v3/services/4fe41a"
+ },
+ "name": "neutron",
+ "type": "network"
+ }
+ ]
+ }
+
+ def setUp(self):
+ super(TestServicesClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = services_client.ServicesClient(fake_auth, 'identity',
+ 'regionOne')
+
+ def _test_create_service(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_service,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_SERVICE,
+ bytes_body,
+ status=201)
+
+ def _test_show_service(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_service,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_SERVICE_INFO,
+ bytes_body,
+ service_id="686766")
+
+ def _test_list_services(self, bytes_body=False, mock_args='services',
+ **params):
+ self.check_service_client_function(
+ self.client.list_services,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_SERVICES,
+ bytes_body,
+ mock_args=[mock_args],
+ **params)
+
+ def _test_update_service(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_service,
+ 'tempest.lib.common.rest_client.RestClient.patch',
+ self.FAKE_SERVICE_INFO,
+ bytes_body,
+ service_id="686766")
+
+ def test_create_service_with_str_body(self):
+ self._test_create_service()
+
+ def test_create_service_with_bytes_body(self):
+ self._test_create_service(bytes_body=True)
+
+ def test_show_service_with_str_body(self):
+ self._test_show_service()
+
+ def test_show_service_with_bytes_body(self):
+ self._test_show_service(bytes_body=True)
+
+ def test_list_services_with_str_body(self):
+ self._test_list_services()
+
+ def test_list_services_with_bytes_body(self):
+ self._test_list_services(bytes_body=True)
+
+ def test_list_services_with_params(self):
+ self._test_list_services(
+ type='fake-type', mock_args='services?type=fake-type')
+
+ def test_update_service_with_str_body(self):
+ self._test_update_service()
+
+ def test_update_service_with_bytes_body(self):
+ self._test_update_service(bytes_body=True)
+
+ def test_delete_service(self):
+ self.check_service_client_function(
+ self.client.delete_service,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ service_id="686766",
+ status=204)
diff --git a/tempest/tests/lib/services/identity/v3/test_token_client.py b/tempest/tests/lib/services/identity/v3/test_token_client.py
new file mode 100644
index 0000000..38e8c4a
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_token_client.py
@@ -0,0 +1,153 @@
+# Copyright 2015 Hewlett-Packard Development Company, L.P.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import json
+
+import mock
+
+from tempest.lib.common import rest_client
+from tempest.lib import exceptions
+from tempest.lib.services.identity.v3 import token_client
+from tempest.tests import base
+from tempest.tests.lib import fake_identity
+
+
+class TestTokenClientV3(base.TestCase):
+
+ def test_init_without_authurl(self):
+ self.assertRaises(exceptions.IdentityError,
+ token_client.V3TokenClient, None)
+
+ def test_auth(self):
+ token_client_v3 = token_client.V3TokenClient('fake_url')
+ response, body_text = fake_identity._fake_v3_response(None, None)
+ body = json.loads(body_text)
+
+ with mock.patch.object(token_client_v3, 'post') as post_mock:
+ post_mock.return_value = response, body
+ resp = token_client_v3.auth(username='fake_user',
+ password='fake_pass')
+
+ self.assertIsInstance(resp, rest_client.ResponseBody)
+ req_dict = json.dumps({
+ 'auth': {
+ 'identity': {
+ 'methods': ['password'],
+ 'password': {
+ 'user': {
+ 'name': 'fake_user',
+ 'password': 'fake_pass',
+ }
+ }
+ },
+ }
+ }, sort_keys=True)
+ post_mock.assert_called_once_with('fake_url/auth/tokens',
+ body=req_dict)
+
+ def test_auth_with_project_id_and_domain_id(self):
+ token_client_v3 = token_client.V3TokenClient('fake_url')
+ response, body_text = fake_identity._fake_v3_response(None, None)
+ body = json.loads(body_text)
+
+ with mock.patch.object(token_client_v3, 'post') as post_mock:
+ post_mock.return_value = response, body
+ resp = token_client_v3.auth(
+ username='fake_user', password='fake_pass',
+ project_id='fcac2a055a294e4c82d0a9c21c620eb4',
+ user_domain_id='14f4a9a99973404d8c20ba1d2af163ff',
+ project_domain_id='291f63ae9ac54ee292ca09e5f72d9676')
+
+ self.assertIsInstance(resp, rest_client.ResponseBody)
+ req_dict = json.dumps({
+ 'auth': {
+ 'identity': {
+ 'methods': ['password'],
+ 'password': {
+ 'user': {
+ 'name': 'fake_user',
+ 'password': 'fake_pass',
+ 'domain': {
+ 'id': '14f4a9a99973404d8c20ba1d2af163ff'
+ }
+ }
+ }
+ },
+ 'scope': {
+ 'project': {
+ 'id': 'fcac2a055a294e4c82d0a9c21c620eb4',
+ 'domain': {
+ 'id': '291f63ae9ac54ee292ca09e5f72d9676'
+ }
+ }
+ }
+ }
+ }, sort_keys=True)
+ post_mock.assert_called_once_with('fake_url/auth/tokens',
+ body=req_dict)
+
+ def test_auth_with_tenant(self):
+ token_client_v3 = token_client.V3TokenClient('fake_url')
+ response, body_text = fake_identity._fake_v3_response(None, None)
+ body = json.loads(body_text)
+
+ with mock.patch.object(token_client_v3, 'post') as post_mock:
+ post_mock.return_value = response, body
+ resp = token_client_v3.auth(username='fake_user',
+ password='fake_pass',
+ project_name='fake_tenant')
+
+ self.assertIsInstance(resp, rest_client.ResponseBody)
+ req_dict = json.dumps({
+ 'auth': {
+ 'identity': {
+ 'methods': ['password'],
+ 'password': {
+ 'user': {
+ 'name': 'fake_user',
+ 'password': 'fake_pass',
+ }
+ }},
+ 'scope': {
+ 'project': {
+ 'name': 'fake_tenant'
+ }
+ },
+ }
+ }, sort_keys=True)
+
+ post_mock.assert_called_once_with('fake_url/auth/tokens',
+ body=req_dict)
+
+ def test_request_with_str_body(self):
+ token_client_v3 = token_client.V3TokenClient('fake_url')
+
+ with mock.patch.object(token_client_v3, 'raw_request') as mock_raw_r:
+ mock_raw_r.return_value = (
+ fake_identity._fake_v3_response(None, None))
+ resp, body = token_client_v3.request('GET', 'fake_uri')
+
+ self.assertIsInstance(body, dict)
+
+ def test_request_with_bytes_body(self):
+ token_client_v3 = token_client.V3TokenClient('fake_url')
+
+ response, body_text = fake_identity._fake_v3_response(None, None)
+ body = body_text.encode('utf-8')
+
+ with mock.patch.object(token_client_v3, 'raw_request') as mock_raw_r:
+ mock_raw_r.return_value = response, body
+ resp, body = token_client_v3.request('GET', 'fake_uri')
+
+ self.assertIsInstance(body, dict)
diff --git a/tempest/tests/lib/services/identity/v3/test_trusts_client.py b/tempest/tests/lib/services/identity/v3/test_trusts_client.py
new file mode 100644
index 0000000..a1ca020
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_trusts_client.py
@@ -0,0 +1,150 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# 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 trusts_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestTrustsClient(base.BaseServiceTest):
+ FAKE_CREATE_TRUST = {
+ "trust": {
+ "expires_at": "2013-02-27T18:30:59.999999Z",
+ "impersonation": True,
+ "allow_redelegation": True,
+ "project_id": "ddef321",
+ "roles": [
+ {
+ "name": "member"
+ }
+ ],
+ "trustee_user_id": "86c0d5",
+ "trustor_user_id": "a0fdfd"
+ }
+ }
+
+ FAKE_LIST_TRUSTS = {
+ "trusts": [
+ {
+ "id": "1ff900",
+ "expires_at":
+ "2013-02-27T18:30:59.999999Z",
+ "impersonation": True,
+ "links": {
+ "self":
+ "http://example.com/identity/v3/OS-TRUST/trusts/1ff900"
+ },
+ "project_id": "0f1233",
+ "trustee_user_id": "86c0d5",
+ "trustor_user_id": "a0fdfd"
+ },
+ {
+ "id": "f4513a",
+ "impersonation": False,
+ "links": {
+ "self":
+ "http://example.com/identity/v3/OS-TRUST/trusts/f45513a"
+ },
+ "project_id": "0f1233",
+ "trustee_user_id": "86c0d5",
+ "trustor_user_id": "3cd2ce"
+ }
+ ]
+ }
+
+ FAKE_TRUST_INFO = {
+ "trust": {
+ "id": "987fe8",
+ "expires_at": "2013-02-27T18:30:59.999999Z",
+ "impersonation": True,
+ "links": {
+ "self":
+ "http://example.com/identity/v3/OS-TRUST/trusts/987fe8"
+ },
+ "roles": [
+ {
+ "id": "ed7b78",
+ "links": {
+ "self":
+ "http://example.com/identity/v3/roles/ed7b78"
+ },
+ "name": "member"
+ }
+ ],
+ "roles_links": {
+ "next": None,
+ "previous": None,
+ "self":
+ "http://example.com/identity/v3/OS-TRUST/trusts/1ff900/roles"
+ },
+ "project_id": "0f1233",
+ "trustee_user_id": "be34d1",
+ "trustor_user_id": "56ae32"
+ }
+ }
+
+ def setUp(self):
+ super(TestTrustsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = trusts_client.TrustsClient(fake_auth, 'identity',
+ 'regionOne')
+
+ def _test_create_trust(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_trust,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_TRUST,
+ bytes_body,
+ status=201)
+
+ def _test_show_trust(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_trust,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_TRUST_INFO,
+ bytes_body,
+ trust_id="1ff900")
+
+ def _test_list_trusts(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_trusts,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_TRUSTS,
+ bytes_body)
+
+ def test_create_trust_with_str_body(self):
+ self._test_create_trust()
+
+ def test_create_trust_with_bytes_body(self):
+ self._test_create_trust(bytes_body=True)
+
+ def test_show_trust_with_str_body(self):
+ self._test_show_trust()
+
+ def test_show_trust_with_bytes_body(self):
+ self._test_show_trust(bytes_body=True)
+
+ def test_list_trusts_with_str_body(self):
+ self._test_list_trusts()
+
+ def test_list_trusts_with_bytes_body(self):
+ self._test_list_trusts(bytes_body=True)
+
+ def test_delete_trust(self):
+ self.check_service_client_function(
+ self.client.delete_trust,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ trust_id="1ff900",
+ status=204)
diff --git a/tempest/tests/lib/services/identity/v3/test_users_client.py b/tempest/tests/lib/services/identity/v3/test_users_client.py
new file mode 100644
index 0000000..5b572f5
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_users_client.py
@@ -0,0 +1,205 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# 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 users_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestUsersClient(base.BaseServiceTest):
+ FAKE_CREATE_USER = {
+ 'user': {
+ 'default_project_id': '95f8c3f8e7b54409a418fc30717f9ae0',
+ 'domain_id': '8347b31afc3545c4b311cb4cce788a08',
+ 'enabled': True,
+ 'name': 'Tempest User',
+ 'password': 'TempestPassword',
+ }
+ }
+
+ FAKE_USER_INFO = {
+ 'user': {
+ 'default_project_id': '95f8c3f8e7b54409a418fc30717f9ae0',
+ 'domain_id': '8347b31afc3545c4b311cb4cce788a08',
+ 'enabled': True,
+ 'id': '817fb3c23fd7465ba6d7fe1b1320121d',
+ 'links': {
+ 'self': 'http://example.com/identity',
+ },
+ 'name': 'Tempest User',
+ 'password_expires_at': '2016-11-06T15:32:17.000000',
+ }
+ }
+
+ FAKE_USER_LIST = {
+ 'links': {
+ 'next': None,
+ 'previous': None,
+ 'self': 'http://example.com/identity/v3/users',
+ },
+ 'users': [
+ {
+ 'domain_id': 'TempestDomain',
+ 'enabled': True,
+ 'id': '817fb3c23fd7465ba6d7fe1b1320121d',
+ 'links': {
+ 'self': 'http://example.com/identity/v3/users/' +
+ '817fb3c23fd7465ba6d7fe1b1320121d',
+ },
+ 'name': 'Tempest User',
+ 'password_expires_at': '2016-11-06T15:32:17.000000',
+ },
+ {
+ 'domain_id': 'TempestDomain',
+ 'enabled': True,
+ 'id': 'bdbfb1e2f1344be197e90a778379cca1',
+ 'links': {
+ 'self': 'http://example.com/identity/v3/users/' +
+ 'bdbfb1e2f1344be197e90a778379cca1',
+ },
+ 'name': 'Tempest User',
+ 'password_expires_at': None,
+ },
+ ]
+ }
+
+ FAKE_GROUP_LIST = {
+ 'links': {
+ 'self': 'http://example.com/identity/v3/groups',
+ 'previous': None,
+ 'next': None,
+ },
+ 'groups': [
+ {
+ 'description': 'Tempest Group One Description',
+ 'domain_id': 'TempestDomain',
+ 'id': '1c92f3453ed34291a074b87493455b8f',
+ 'links': {
+ 'self': 'http://example.com/identity/v3/groups/' +
+ '1c92f3453ed34291a074b87493455b8f'
+ },
+ 'name': 'Tempest Group One',
+ },
+ {
+ 'description': 'Tempest Group Two Description',
+ 'domain_id': 'TempestDomain',
+ 'id': 'ce9e7dafed3b4877a7d4466ed730a9ee',
+ 'links': {
+ 'self': 'http://example.com/identity/v3/groups/' +
+ 'ce9e7dafed3b4877a7d4466ed730a9ee'
+ },
+ 'name': 'Tempest Group Two',
+ },
+ ]
+ }
+
+ def setUp(self):
+ super(TestUsersClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = users_client.UsersClient(fake_auth, 'identity',
+ 'regionOne')
+
+ def _test_create_user(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_user,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_USER,
+ bytes_body,
+ status=201,
+ )
+
+ def _test_show_user(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_user,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_USER_INFO,
+ bytes_body,
+ user_id='817fb3c23fd7465ba6d7fe1b1320121d',
+ )
+
+ def _test_list_users(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_users,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_USER_LIST,
+ bytes_body,
+ )
+
+ def _test_update_user(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_user,
+ 'tempest.lib.common.rest_client.RestClient.patch',
+ self.FAKE_USER_INFO,
+ bytes_body,
+ user_id='817fb3c23fd7465ba6d7fe1b1320121d',
+ name='NewName',
+ )
+
+ def _test_list_user_groups(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_user_groups,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_GROUP_LIST,
+ bytes_body,
+ user_id='817fb3c23fd7465ba6d7fe1b1320121d',
+ )
+
+ def test_create_user_with_string_body(self):
+ self._test_create_user()
+
+ def test_create_user_with_bytes_body(self):
+ self._test_create_user(bytes_body=True)
+
+ def test_show_user_with_string_body(self):
+ self._test_show_user()
+
+ def test_show_user_with_bytes_body(self):
+ self._test_show_user(bytes_body=True)
+
+ def test_list_users_with_string_body(self):
+ self._test_list_users()
+
+ def test_list_users_with_bytes_body(self):
+ self._test_list_users(bytes_body=True)
+
+ def test_update_user_with_string_body(self):
+ self._test_update_user()
+
+ def test_update_user_with_bytes_body(self):
+ self._test_update_user(bytes_body=True)
+
+ def test_list_user_groups_with_string_body(self):
+ self._test_list_user_groups()
+
+ def test_list_user_groups_with_bytes_body(self):
+ self._test_list_user_groups(bytes_body=True)
+
+ def test_delete_user(self):
+ self.check_service_client_function(
+ self.client.delete_user,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ user_id='817fb3c23fd7465ba6d7fe1b1320121d',
+ status=204,
+ )
+
+ def test_change_user_password(self):
+ self.check_service_client_function(
+ self.client.update_user_password,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ status=204,
+ user_id='817fb3c23fd7465ba6d7fe1b1320121d',
+ password='NewTempestPassword',
+ original_password='OldTempestPassword')
diff --git a/tempest/tests/lib/services/identity/v3/test_versions_client.py b/tempest/tests/lib/services/identity/v3/test_versions_client.py
new file mode 100644
index 0000000..3bfaf1e
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_versions_client.py
@@ -0,0 +1,70 @@
+# Copyright 2017 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.services.identity.v3 import versions_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestIdentityClient(base.BaseServiceTest):
+
+ FAKE_VERSIONS_INFO = {
+ "versions": {
+ "values": [
+ {"status": "stable", "updated": "2017-02-22T00:00:00Z",
+ "media-types": [
+ {"base": "application/json", "type":
+ "application/vnd.openstack.identity-v3+json"}
+ ],
+ "id": "v3.8",
+ "links": [
+ {"href": "https://15.184.67.226/identity_admin/v3/",
+ "rel": "self"}
+ ]},
+ {"status": "deprecated", "updated": "2016-08-04T00:00:00Z",
+ "media-types": [
+ {"base": "application/json",
+ "type": "application/vnd.openstack.identity-v2.0+json"}
+ ],
+ "id": "v2.0",
+ "links": [
+ {"href": "https://15.184.67.226/identity_admin/v2.0/",
+ "rel": "self"},
+ {"href": "https://docs.openstack.org/",
+ "type": "text/html", "rel": "describedby"}
+ ]}
+ ]
+ }
+ }
+
+ def setUp(self):
+ super(TestIdentityClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = versions_client.VersionsClient(fake_auth,
+ 'identity',
+ 'regionOne')
+
+ def _test_list_versions(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_versions,
+ 'tempest.lib.common.rest_client.RestClient.raw_request',
+ self.FAKE_VERSIONS_INFO,
+ bytes_body,
+ 300)
+
+ def test_list_versions_with_str_body(self):
+ self._test_list_versions()
+
+ def test_list_versions_with_bytes_body(self):
+ self._test_list_versions(bytes_body=True)
diff --git a/tempest/services/image/__init__.py b/tempest/tests/lib/services/image/__init__.py
similarity index 100%
rename from tempest/services/image/__init__.py
rename to tempest/tests/lib/services/image/__init__.py
diff --git a/tempest/services/image/v1/__init__.py b/tempest/tests/lib/services/image/v1/__init__.py
similarity index 100%
rename from tempest/services/image/v1/__init__.py
rename to tempest/tests/lib/services/image/v1/__init__.py
diff --git a/tempest/tests/lib/services/image/v1/test_image_members_client.py b/tempest/tests/lib/services/image/v1/test_image_members_client.py
new file mode 100644
index 0000000..a5a6128
--- /dev/null
+++ b/tempest/tests/lib/services/image/v1/test_image_members_client.py
@@ -0,0 +1,84 @@
+# Copyright 2016 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.services.image.v1 import image_members_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestImageMembersClient(base.BaseServiceTest):
+ FAKE_LIST_IMAGE_MEMBERS = {
+ "members": [
+ {
+ "created_at": "2013-10-07T17:58:03Z",
+ "image_id": "dbc999e3-c52f-4200-bedd-3b18fe7f87fe",
+ "member_id": "123456789",
+ "status": "pending",
+ "updated_at": "2013-10-07T17:58:03Z"
+ },
+ {
+ "created_at": "2013-10-07T17:58:55Z",
+ "image_id": "dbc999e3-c52f-4200-bedd-3b18fe7f87fe",
+ "member_id": "987654321",
+ "status": "accepted",
+ "updated_at": "2013-10-08T12:08:55Z"
+ }
+ ]
+ }
+
+ def setUp(self):
+ super(TestImageMembersClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = image_members_client.ImageMembersClient(fake_auth,
+ 'image',
+ 'regionOne')
+
+ def _test_list_image_members(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_image_members,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_IMAGE_MEMBERS,
+ bytes_body,
+ image_id="0ae74cc5-5147-4239-9ce2-b0c580f7067e")
+
+ def _test_create_image_member(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_image_member,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ {},
+ bytes_body,
+ image_id="0ae74cc5-5147-4239-9ce2-b0c580f7067e",
+ member_id="8989447062e04a818baf9e073fd04fa7",
+ status=204)
+
+ def test_list_image_members_with_str_body(self):
+ self._test_list_image_members()
+
+ def test_list_image_members_with_bytes_body(self):
+ self._test_list_image_members(bytes_body=True)
+
+ def test_create_image_member_with_str_body(self):
+ self._test_create_image_member()
+
+ def test_create_image_member_with_bytes_body(self):
+ self._test_create_image_member(bytes_body=True)
+
+ def test_delete_image_member(self):
+ self.check_service_client_function(
+ self.client.delete_image_member,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ image_id="0ae74cc5-5147-4239-9ce2-b0c580f7067e",
+ member_id="8989447062e04a818baf9e073fd04fa7",
+ status=204)
diff --git a/tempest/services/image/v2/__init__.py b/tempest/tests/lib/services/image/v2/__init__.py
similarity index 100%
rename from tempest/services/image/v2/__init__.py
rename to tempest/tests/lib/services/image/v2/__init__.py
diff --git a/tempest/tests/lib/services/image/v2/test_image_members_client.py b/tempest/tests/lib/services/image/v2/test_image_members_client.py
new file mode 100644
index 0000000..703b6e1
--- /dev/null
+++ b/tempest/tests/lib/services/image/v2/test_image_members_client.py
@@ -0,0 +1,90 @@
+# Copyright 2016 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.services.image.v2 import image_members_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestImageMembersClient(base.BaseServiceTest):
+ FAKE_CREATE_SHOW_UPDATE_IMAGE_MEMBER = {
+ "status": "pending",
+ "created_at": "2013-11-26T07:21:21Z",
+ "updated_at": "2013-11-26T07:21:21Z",
+ "image_id": "0ae74cc5-5147-4239-9ce2-b0c580f7067e",
+ "member_id": "8989447062e04a818baf9e073fd04fa7",
+ "schema": "/v2/schemas/member"
+ }
+
+ def setUp(self):
+ super(TestImageMembersClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = image_members_client.ImageMembersClient(fake_auth,
+ 'image',
+ 'regionOne')
+
+ def _test_show_image_member(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_image_member,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_CREATE_SHOW_UPDATE_IMAGE_MEMBER,
+ bytes_body,
+ image_id="0ae74cc5-5147-4239-9ce2-b0c580f7067e",
+ member_id="8989447062e04a818baf9e073fd04fa7")
+
+ def _test_create_image_member(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_image_member,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_SHOW_UPDATE_IMAGE_MEMBER,
+ bytes_body,
+ image_id="0ae74cc5-5147-4239-9ce2-b0c580f7067e",
+ member_id="8989447062e04a818baf9e073fd04fa7")
+
+ def _test_update_image_member(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_image_member,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ self.FAKE_CREATE_SHOW_UPDATE_IMAGE_MEMBER,
+ bytes_body,
+ image_id="0ae74cc5-5147-4239-9ce2-b0c580f7067e",
+ member_id="8989447062e04a818baf9e073fd04fa7",
+ schema="/v2/schemas/member2")
+
+ def test_show_image_member_with_str_body(self):
+ self._test_show_image_member()
+
+ def test_show_image_member_with_bytes_body(self):
+ self._test_show_image_member(bytes_body=True)
+
+ def test_create_image_member_with_str_body(self):
+ self._test_create_image_member()
+
+ def test_create_image_member_with_bytes_body(self):
+ self._test_create_image_member(bytes_body=True)
+
+ def test_delete_image_member(self):
+ self.check_service_client_function(
+ self.client.delete_image_member,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ image_id="0ae74cc5-5147-4239-9ce2-b0c580f7067e",
+ member_id="8989447062e04a818baf9e073fd04fa7",
+ status=204)
+
+ def test_update_image_member_with_str_body(self):
+ self._test_update_image_member()
+
+ def test_update_image_member_with_bytes_body(self):
+ self._test_update_image_member(bytes_body=True)
diff --git a/tempest/tests/lib/services/image/v2/test_images_client.py b/tempest/tests/lib/services/image/v2/test_images_client.py
new file mode 100644
index 0000000..ee4d4cb
--- /dev/null
+++ b/tempest/tests/lib/services/image/v2/test_images_client.py
@@ -0,0 +1,221 @@
+# Copyright 2016 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.
+
+import six
+
+from tempest.lib.common.utils import data_utils
+from tempest.lib.services.image.v2 import images_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestImagesClient(base.BaseServiceTest):
+ FAKE_CREATE_UPDATE_SHOW_IMAGE = {
+ "id": "e485aab9-0907-4973-921c-bb6da8a8fcf8",
+ "name": u"\u2740(*\xb4\u25e2`*)\u2740",
+ "status": "active",
+ "visibility": "public",
+ "size": 2254249,
+ "checksum": "2cec138d7dae2aa59038ef8c9aec2390",
+ "tags": [
+ "fedora",
+ "beefy"
+ ],
+ "created_at": "2012-08-10T19:23:50Z",
+ "updated_at": "2012-08-12T11:11:33Z",
+ "self": "/v2/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea",
+ "file": "/v2/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea/file",
+ "schema": "/v2/schemas/image",
+ "owner": None,
+ "min_ram": None,
+ "min_disk": None,
+ "disk_format": None,
+ "virtual_size": None,
+ "container_format": None
+ }
+
+ FAKE_LIST_IMAGES = {
+ "images": [
+ {
+ "status": "active",
+ "name": "cirros-0.3.2-x86_64-disk",
+ "tags": [],
+ "container_format": "bare",
+ "created_at": "2014-11-07T17:07:06Z",
+ "disk_format": "qcow2",
+ "updated_at": "2014-11-07T17:19:09Z",
+ "visibility": "public",
+ "self": "/v2/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27",
+ "min_disk": 0,
+ "protected": False,
+ "id": "1bea47ed-f6a9-463b-b423-14b9cca9ad27",
+ "file": "/v2/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27/file",
+ "checksum": "64d7c1cd2b6f60c92c14662941cb7913",
+ "owner": "5ef70662f8b34079a6eddb8da9d75fe8",
+ "size": 13167616,
+ "min_ram": 0,
+ "schema": "/v2/schemas/image",
+ "virtual_size": None
+ },
+ {
+ "status": "active",
+ "name": "F17-x86_64-cfntools",
+ "tags": [],
+ "container_format": "bare",
+ "created_at": "2014-10-30T08:23:39Z",
+ "disk_format": "qcow2",
+ "updated_at": "2014-11-03T16:40:10Z",
+ "visibility": "public",
+ "self": "/v2/images/781b3762-9469-4cec-b58d-3349e5de4e9c",
+ "min_disk": 0,
+ "protected": False,
+ "id": "781b3762-9469-4cec-b58d-3349e5de4e9c",
+ "file": "/v2/images/781b3762-9469-4cec-b58d-3349e5de4e9c/file",
+ "checksum": "afab0f79bac770d61d24b4d0560b5f70",
+ "owner": "5ef70662f8b34079a6eddb8da9d75fe8",
+ "size": 476704768,
+ "min_ram": 0,
+ "schema": "/v2/schemas/image",
+ "virtual_size": None
+ }
+ ],
+ "schema": "/v2/schemas/images",
+ "first": "/v2/images"
+ }
+
+ FAKE_TAG_NAME = "fake tag"
+
+ def setUp(self):
+ super(TestImagesClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = images_client.ImagesClient(fake_auth,
+ 'image', 'regionOne')
+
+ def _test_update_image(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_image,
+ 'tempest.lib.common.rest_client.RestClient.patch',
+ self.FAKE_CREATE_UPDATE_SHOW_IMAGE,
+ bytes_body,
+ image_id="e485aab9-0907-4973-921c-bb6da8a8fcf8",
+ patch=[{"op": "add", "path": "/a/b/c", "value": ["foo", "bar"]}])
+
+ def _test_create_image(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_image,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_UPDATE_SHOW_IMAGE,
+ bytes_body,
+ name="virtual machine image",
+ status=201)
+
+ def _test_show_image(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_image,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_CREATE_UPDATE_SHOW_IMAGE,
+ bytes_body,
+ image_id="e485aab9-0907-4973-921c-bb6da8a8fcf8")
+
+ def _test_list_images(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_images,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_IMAGES,
+ bytes_body,
+ mock_args=['images'])
+
+ def test_create_image_with_str_body(self):
+ self._test_create_image()
+
+ def test_create_image_with_bytes_body(self):
+ self._test_create_image(bytes_body=True)
+
+ def test_update_image_with_str_body(self):
+ self._test_update_image()
+
+ def test_update_image_with_bytes_body(self):
+ self._test_update_image(bytes_body=True)
+
+ def test_deactivate_image(self):
+ self.check_service_client_function(
+ self.client.deactivate_image,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {}, image_id="e485aab9-0907-4973-921c-bb6da8a8fcf8", status=204)
+
+ def test_reactivate_image(self):
+ self.check_service_client_function(
+ self.client.reactivate_image,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {}, image_id="e485aab9-0907-4973-921c-bb6da8a8fcf8", status=204)
+
+ def test_delete_image(self):
+ self.check_service_client_function(
+ self.client.delete_image,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {}, image_id="e485aab9-0907-4973-921c-bb6da8a8fcf8", status=204)
+
+ def test_store_image_file(self):
+ data = six.BytesIO(data_utils.random_bytes())
+
+ self.check_service_client_function(
+ self.client.store_image_file,
+ 'tempest.lib.common.rest_client.RestClient.raw_request',
+ {},
+ image_id=self.FAKE_CREATE_UPDATE_SHOW_IMAGE["id"],
+ status=204,
+ data=data)
+
+ def test_show_image_file(self):
+ # NOTE: The response for this API returns raw binary data, but an error
+ # is thrown if random bytes are used for the resp body since
+ # ``create_response`` then calls ``json.dumps``.
+ self.check_service_client_function(
+ self.client.show_image_file,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {},
+ resp_as_string=True,
+ image_id=self.FAKE_CREATE_UPDATE_SHOW_IMAGE["id"],
+ headers={'Content-Type': 'application/octet-stream'},
+ status=200)
+
+ def test_add_image_tag(self):
+ self.check_service_client_function(
+ self.client.add_image_tag,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ {},
+ image_id=self.FAKE_CREATE_UPDATE_SHOW_IMAGE["id"],
+ status=204,
+ tag=self.FAKE_TAG_NAME)
+
+ def test_delete_image_tag(self):
+ self.check_service_client_function(
+ self.client.delete_image_tag,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ image_id=self.FAKE_CREATE_UPDATE_SHOW_IMAGE["id"],
+ status=204,
+ tag=self.FAKE_TAG_NAME)
+
+ def test_show_image_with_str_body(self):
+ self._test_show_image()
+
+ def test_show_image_with_bytes_body(self):
+ self._test_show_image(bytes_body=True)
+
+ def test_list_images_with_str_body(self):
+ self._test_list_images()
+
+ def test_list_images_with_bytes_body(self):
+ self._test_list_images(bytes_body=True)
diff --git a/tempest/tests/lib/services/image/v2/test_namespace_object_client.py b/tempest/tests/lib/services/image/v2/test_namespace_object_client.py
new file mode 100644
index 0000000..8d29660
--- /dev/null
+++ b/tempest/tests/lib/services/image/v2/test_namespace_object_client.py
@@ -0,0 +1,210 @@
+# Copyright 2016 EasyStack. 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.services.image.v2 import namespace_objects_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestNamespaceObjectClient(base.BaseServiceTest):
+ FAKE_CREATE_SHOW_OBJECTS = {
+ "created_at": "2016-09-19T18:20:56Z",
+ "description": "You can configure the CPU limits.",
+ "name": "CPU Limits",
+ "properties": {
+ "quota:cpu_period": {
+ "description": "Specifies the enforcement interval",
+ "maximum": 1000000,
+ "minimum": 1000,
+ "title": "Quota: CPU Period",
+ "type": "integer"
+ },
+ "quota:cpu_quota": {
+ "description": "Specifies the maximum allowed bandwidth ",
+ "title": "Quota: CPU Quota",
+ "type": "integer"
+ },
+ "quota:cpu_shares": {
+ "description": "Specifies the proportional weighted share.",
+ "title": "Quota: CPU Shares",
+ "type": "integer"
+ }
+ },
+ "required": [],
+ "schema": "/v2/schemas/metadefs/object",
+ "self": "/v2/metadefs/namespaces/OS::Compute::Quota/objects/CPU",
+ "updated_at": "2016-09-19T18:20:56Z"
+ }
+
+ FAKE_LIST_OBJECTS = {
+ "objects": [
+ {
+ "created_at": "2016-09-18T18:16:35Z",
+ "description": "You can configure the CPU limits.",
+ "name": "CPU Limits",
+ "properties": {
+ "quota:cpu_period": {
+ "description": "Specifies the enforcement interval ",
+ "maximum": 1000000,
+ "minimum": 1000,
+ "title": "Quota: CPU Period",
+ "type": "integer"
+ },
+ "quota:cpu_quota": {
+ "description": "Specifies the maximum.",
+ "title": "Quota: CPU Quota",
+ "type": "integer"
+ },
+ "quota:cpu_shares": {
+ "description": " Desc.",
+ "title": "Quota: CPU Shares",
+ "type": "integer"
+ }
+ },
+ "required": [],
+ "schema": "/v2/schemas/metadefs/object",
+ "self":
+ "/v2/metadefs/namespaces/OS::Compute::Quota/objects/CPU"
+ },
+ {
+ "created_at": "2016-09-18T18:16:35Z",
+ "description": "Using disk I/O quotas.",
+ "name": "Disk QoS",
+ "properties": {
+ "quota:disk_read_bytes_sec": {
+ "description": "Sets disk I/O quota.",
+ "title": "Quota: Disk read bytes / sec",
+ "type": "integer"
+ },
+ "quota:disk_read_iops_sec": {
+ "description": "Sets disk I/O quota",
+ "title": "Quota: Disk read IOPS / sec",
+ "type": "integer"
+ },
+ "quota:disk_total_bytes_sec": {
+ "description": "Sets disk I/O quota.",
+ "title": "Quota: Disk Total Bytes / sec",
+ "type": "integer"
+ },
+ "quota:disk_total_iops_sec": {
+ "description": "Sets disk I/O quota.",
+ "title": "Quota: Disk Total IOPS / sec",
+ "type": "integer"
+ },
+ "quota:disk_write_bytes_sec": {
+ "description": "Sets disk I/O quota.",
+ "title": "Quota: Disk Write Bytes / sec",
+ "type": "integer"
+ },
+ "quota:disk_write_iops_sec": {
+ "description": "Sets disk I/O quota.",
+ "title": "Quota: Disk Write IOPS / sec",
+ "type": "integer"
+ }
+ },
+ "required": [],
+ "schema": "/v2/schemas/metadefs/object",
+ "self":
+ "/v2/metadefs/namespaces/OS::Compute::Quota/objects/Disk QoS"
+ },
+ ],
+ "schema": "v2/schemas/metadefs/objects"
+ }
+
+ FAKE_UPDATE_OBJECTS = {
+ "description": "You can configure the CPU limits.",
+ "name": "CPU",
+ "properties": {
+ "quota:cpu_shares": {
+ "description": "Specify.",
+ "title": "Quota: CPU Shares",
+ "type": "integer"
+ }
+ },
+ "required": []
+ }
+
+ def setUp(self):
+ super(TestNamespaceObjectClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = namespace_objects_client.NamespaceObjectsClient(
+ fake_auth, 'image', 'regionOne')
+
+ def _test_create_namespace_objects(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_namespace_object,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_SHOW_OBJECTS,
+ bytes_body, status=201,
+ namespace="OS::Compute::Hypervisor",
+ object_name="OS::Glance::Image")
+
+ def _test_list_namespace_objects(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_namespace_objects,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_OBJECTS,
+ bytes_body,
+ namespace="OS::Compute::Hypervisor")
+
+ def _test_show_namespace_objects(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_namespace_object,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_CREATE_SHOW_OBJECTS,
+ bytes_body,
+ namespace="OS::Compute::Hypervisor",
+ object_name="OS::Glance::Image")
+
+ def _test_update_namespace_objects(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_namespace_object,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ self.FAKE_UPDATE_OBJECTS,
+ bytes_body,
+ namespace="OS::Compute::Hypervisor",
+ object_name="OS::Glance::Image",
+ name="CPU")
+
+ def test_create_namespace_object_with_str_body(self):
+ self._test_create_namespace_objects()
+
+ def test_create_namespace_object_with_bytes_body(self):
+ self._test_create_namespace_objects(bytes_body=True)
+
+ def test_list_namespace_object_with_str_body(self):
+ self._test_list_namespace_objects()
+
+ def test_list_namespace_object_with_bytes_body(self):
+ self._test_list_namespace_objects(bytes_body=True)
+
+ def test_show_namespace_object_with_str_body(self):
+ self._test_show_namespace_objects()
+
+ def test_show_namespace_object_with_bytes_body(self):
+ self._test_show_namespace_objects(bytes_body=True)
+
+ def test_update_namespace_object_with_str_body(self):
+ self._test_update_namespace_objects()
+
+ def test_update_namespace_object_with_bytes_body(self):
+ self._test_update_namespace_objects(bytes_body=True)
+
+ def test_delete_namespace_objects(self):
+ self.check_service_client_function(
+ self.client.delete_namespace_object,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {}, namespace="OS::Compute::Hypervisor",
+ object_name="OS::Glance::Image",
+ status=204)
diff --git a/tempest/tests/lib/services/image/v2/test_namespace_properties_client.py b/tempest/tests/lib/services/image/v2/test_namespace_properties_client.py
new file mode 100644
index 0000000..1d56db6
--- /dev/null
+++ b/tempest/tests/lib/services/image/v2/test_namespace_properties_client.py
@@ -0,0 +1,191 @@
+# Copyright 2016 EasyStack. 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.services.image.v2 import namespace_properties_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestNamespacePropertiesClient(base.BaseServiceTest):
+ FAKE_CREATE_SHOW_NAMESPACE_PROPERTY = {
+ "description": "property",
+ "enum": ["xen", "qemu", "kvm", "lxc", "uml", "vmware", "hyperv"],
+ "name": "OS::Glance::Image",
+ "title": "Hypervisor Type",
+ "type": "string"
+ }
+
+ FAKE_LIST_NAMESPACE_PROPERTY = {
+ "properties": {
+ "hw_disk_bus": {
+ "description": "property.",
+ "enum": ["scsi", "virtio", "uml", "xen", "ide", "usb"],
+ "title": "Disk Bus",
+ "type": "string"
+ },
+ "hw_machine_type": {
+ "description": "desc.",
+ "title": "Machine Type",
+ "type": "string"
+ },
+ "hw_qemu_guest_agent": {
+ "description": "desc.",
+ "enum": [
+ "yes",
+ "no"
+ ],
+ "title": "QEMU Guest Agent",
+ "type": "string"
+ },
+ "hw_rng_model": {
+ "default": "virtio",
+ "description": "desc",
+ "title": "Random Number Generator Device",
+ "type": "string"
+ },
+ "hw_scsi_model": {
+ "default": "virtio-scsi",
+ "description": "desc.",
+ "title": "SCSI Model",
+ "type": "string"
+ },
+ "hw_video_model": {
+ "description": "The video image driver used.",
+ "enum": [
+ "vga",
+ "cirrus",
+ "vmvga",
+ "xen",
+ "qxl"
+ ],
+ "title": "Video Model",
+ "type": "string"
+ },
+ "hw_video_ram": {
+ "description": "desc.",
+ "title": "Max Video Ram",
+ "type": "integer"
+ },
+ "hw_vif_model": {
+ "description": "desc.",
+ "enum": ["e1000",
+ "ne2k_pci",
+ "pcnet",
+ "rtl8139",
+ "virtio",
+ "e1000",
+ "e1000e",
+ "VirtualE1000",
+ "VirtualE1000e",
+ "VirtualPCNet32",
+ "VirtualSriovEthernetCard",
+ "VirtualVmxnet",
+ "netfront",
+ "ne2k_pci"
+ ],
+ "title": "Virtual Network Interface",
+ "type": "string"
+ },
+ "os_command_line": {
+ "description": "desc.",
+ "title": "Kernel Command Line",
+ "type": "string"
+ }
+ }
+ }
+
+ FAKE_UPDATE_NAMESPACE_PROPERTY = {
+ "description": "property",
+ "enum": ["xen", "qemu", "kvm", "lxc", "uml", "vmware", "hyperv"],
+ "name": "OS::Glance::Image",
+ "title": "update Hypervisor Type",
+ "type": "string"
+ }
+
+ def setUp(self):
+ super(TestNamespacePropertiesClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = namespace_properties_client.NamespacePropertiesClient(
+ fake_auth, 'image', 'regionOne')
+
+ def _test_create_namespace_property(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_namespace_property,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_SHOW_NAMESPACE_PROPERTY,
+ bytes_body, status=201,
+ namespace="OS::Compute::Hypervisor",
+ title="Hypervisor Type", name="OS::Glance::Image",
+ type="string",
+ enum=["xen", "qemu", "kvm", "lxc", "uml", "vmware", "hyperv"])
+
+ def _test_list_namespace_property(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_namespace_properties,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_NAMESPACE_PROPERTY,
+ bytes_body,
+ namespace="OS::Compute::Hypervisor")
+
+ def _test_show_namespace_property(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_namespace_properties,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_CREATE_SHOW_NAMESPACE_PROPERTY,
+ bytes_body,
+ namespace="OS::Compute::Hypervisor",
+ property_name="OS::Glance::Image")
+
+ def _test_update_namespace_property(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_namespace_properties,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ self.FAKE_UPDATE_NAMESPACE_PROPERTY,
+ bytes_body,
+ namespace="OS::Compute::Hypervisor",
+ property_name="OS::Glance::Image",
+ title="update Hypervisor Type", type="string",
+ enum=["xen", "qemu", "kvm", "lxc", "uml", "vmware", "hyperv"],
+ name="OS::Glance::Image")
+
+ def test_create_namespace_property_with_str_body(self):
+ self._test_create_namespace_property()
+
+ def test_create_namespace_property_with_bytes_body(self):
+ self._test_create_namespace_property(bytes_body=True)
+
+ def test_list_namespace_property_with_str_body(self):
+ self._test_list_namespace_property()
+
+ def test_list_namespace_property_with_bytes_body(self):
+ self._test_list_namespace_property(bytes_body=True)
+
+ def test_show_namespace_property_with_str_body(self):
+ self._test_show_namespace_property()
+
+ def test_show_namespace_property_with_bytes_body(self):
+ self._test_show_namespace_property(bytes_body=True)
+
+ def test_update_namespace_property_with_str_body(self):
+ self._test_update_namespace_property()
+
+ def test_update_namespace_property_with_bytes_body(self):
+ self._test_update_namespace_property(bytes_body=True)
+
+ def test_delete_namespace(self):
+ self.check_service_client_function(
+ self.client.delete_namespace_property,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {}, namespace="OS::Compute::Hypervisor",
+ property_name="OS::Glance::Image", status=204)
diff --git a/tempest/tests/lib/services/image/v2/test_namespace_tags_client.py b/tempest/tests/lib/services/image/v2/test_namespace_tags_client.py
new file mode 100644
index 0000000..2faa5be
--- /dev/null
+++ b/tempest/tests/lib/services/image/v2/test_namespace_tags_client.py
@@ -0,0 +1,126 @@
+# Copyright 2016 EasyStack. 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.services.image.v2 import namespace_tags_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestNamespaceTagsClient(base.BaseServiceTest):
+ FAKE_CREATE_SHOW_TAGS = {
+ "created_at": "2015-05-09T01:12:31Z",
+ "name": "added-sample-tag",
+ "updated_at": "2015-05-09T01:12:31Z"
+ }
+
+ FAKE_LIST_TAGS = {
+ "tags": [
+ {
+ "name": "sample-tag1"
+ },
+ {
+ "name": "sample-tag2"
+ },
+ {
+ "name": "sample-tag3"
+ }
+ ]
+ }
+
+ FAKE_UPDATE_TAGS = {"name": "new-tag-name"}
+
+ def setUp(self):
+ super(TestNamespaceTagsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = namespace_tags_client.NamespaceTagsClient(
+ fake_auth, 'image', 'regionOne')
+
+ def _test_create_namespace_tags(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_namespace_tags,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_SHOW_TAGS,
+ bytes_body, status=201,
+ namespace="OS::Compute::Hypervisor",
+ tags=[{"name": "sample-tag1"},
+ {"name": "sample-tag2"},
+ {"name": "sample-tag3"}])
+
+ def _test_list_namespace_tags(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_namespace_tags,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_TAGS,
+ bytes_body,
+ namespace="OS::Compute::Hypervisor")
+
+ def _test_create_namespace_tag_definition(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_namespace_tag,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_SHOW_TAGS,
+ bytes_body,
+ status=201,
+ namespace="OS::Compute::Hypervisor",
+ tag_name="added-sample-tag")
+
+ def _test_show_namespace_tag_definition(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_namespace_tag,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_CREATE_SHOW_TAGS,
+ bytes_body,
+ namespace="OS::Compute::Hypervisor",
+ tag_name="added-sample-tag")
+
+ def _test_update_namespace_tag_definition(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_namespace_tag,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ self.FAKE_UPDATE_OBJECTS,
+ bytes_body,
+ namespace="OS::Compute::Hypervisor",
+ tag_name="added-sample-tag",
+ name="new-tag-name")
+
+ def test_create_namespace_tags_with_str_body(self):
+ self._test_create_namespace_tags()
+
+ def test_create_namespace_tags_with_bytes_body(self):
+ self._test_create_namespace_tags(bytes_body=True)
+
+ def test_list_namespace_tags_with_str_body(self):
+ self._test_list_namespace_tags()
+
+ def test_list_namespace_tags_with_bytes_body(self):
+ self._test_list_namespace_tags(bytes_body=True)
+
+ def test_create_namespace_tag_with_str_body(self):
+ self._test_create_namespace_tag_definition()
+
+ def test_create_namespace_tag_with_bytes_body(self):
+ self._test_create_namespace_tag_definition(bytes_body=True)
+
+ def test_show_namespace_tag_with_str_body(self):
+ self._test_show_namespace_tag_definition()
+
+ def test_show_namespace_tag_with_bytes_body(self):
+ self._test_show_namespace_tag_definition(bytes_body=True)
+
+ def test_delete_all_namespace_tags(self):
+ self.check_service_client_function(
+ self.client.delete_namespace_tags,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {}, status=200,
+ namespace="OS::Compute::Hypervisor")
diff --git a/tempest/tests/lib/services/image/v2/test_namespaces_client.py b/tempest/tests/lib/services/image/v2/test_namespaces_client.py
new file mode 100644
index 0000000..4cb9d01
--- /dev/null
+++ b/tempest/tests/lib/services/image/v2/test_namespaces_client.py
@@ -0,0 +1,93 @@
+# Copyright 2016 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.services.image.v2 import namespaces_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestNamespacesClient(base.BaseServiceTest):
+ FAKE_CREATE_SHOW_NAMESPACE = {
+ "namespace": "OS::Compute::Hypervisor",
+ "visibility": "public",
+ "description": "Tempest",
+ "display_name": u"\u2740(*\xb4\u25e1`*)\u2740",
+ "protected": True
+ }
+
+ FAKE_UPDATE_NAMESPACE = {
+ "namespace": "OS::Compute::Hypervisor",
+ "visibility": "public",
+ "description": "Tempest",
+ "display_name": u"\u2740(*\xb4\u25e2`*)\u2740",
+ "protected": True
+ }
+
+ def setUp(self):
+ super(TestNamespacesClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = namespaces_client.NamespacesClient(fake_auth,
+ 'image', 'regionOne')
+
+ def _test_show_namespace(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_namespace,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_CREATE_SHOW_NAMESPACE,
+ bytes_body,
+ namespace="OS::Compute::Hypervisor")
+
+ def _test_create_namespace(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_namespace,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_SHOW_NAMESPACE,
+ bytes_body,
+ namespace="OS::Compute::Hypervisor",
+ visibility="public", description="Tempest",
+ display_name=u"\u2740(*\xb4\u25e1`*)\u2740", protected=True,
+ status=201)
+
+ def _test_update_namespace(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_namespace,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ self.FAKE_UPDATE_NAMESPACE,
+ bytes_body,
+ namespace="OS::Compute::Hypervisor",
+ display_name=u"\u2740(*\xb4\u25e2`*)\u2740", protected=True)
+
+ def test_show_namespace_with_str_body(self):
+ self._test_show_namespace()
+
+ def test_show_namespace_with_bytes_body(self):
+ self._test_show_namespace(bytes_body=True)
+
+ def test_create_namespace_with_str_body(self):
+ self._test_create_namespace()
+
+ def test_create_namespace_with_bytes_body(self):
+ self._test_create_namespace(bytes_body=True)
+
+ def test_delete_namespace(self):
+ self.check_service_client_function(
+ self.client.delete_namespace,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {}, namespace="OS::Compute::Hypervisor", status=204)
+
+ def test_update_namespace_with_str_body(self):
+ self._test_update_namespace()
+
+ def test_update_namespace_with_bytes_body(self):
+ self._test_update_namespace(bytes_body=True)
diff --git a/tempest/tests/lib/services/image/v2/test_resource_types_client.py b/tempest/tests/lib/services/image/v2/test_resource_types_client.py
new file mode 100644
index 0000000..2e3b117
--- /dev/null
+++ b/tempest/tests/lib/services/image/v2/test_resource_types_client.py
@@ -0,0 +1,69 @@
+# Copyright 2016 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.services.image.v2 import resource_types_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestResouceTypesClient(base.BaseServiceTest):
+ FAKE_LIST_RESOURCETYPES = {
+ "resource_types": [
+ {
+ "created_at": "2014-08-28T18:13:04Z",
+ "name": "OS::Glance::Image",
+ "updated_at": "2014-08-28T18:13:04Z"
+ },
+ {
+ "created_at": "2014-08-28T18:13:04Z",
+ "name": "OS::Cinder::Volume",
+ "updated_at": "2014-08-28T18:13:04Z"
+ },
+ {
+ "created_at": "2014-08-28T18:13:04Z",
+ "name": "OS::Nova::Flavor",
+ "updated_at": "2014-08-28T18:13:04Z"
+ },
+ {
+ "created_at": "2014-08-28T18:13:04Z",
+ "name": "OS::Nova::Aggregate",
+ "updated_at": "2014-08-28T18:13:04Z"
+ },
+ {
+ "created_at": "2014-08-28T18:13:04Z",
+ "name": u"\u2740(*\xb4\u25e1`*)\u2740",
+ "updated_at": "2014-08-28T18:13:04Z"
+ }
+ ]
+ }
+
+ def setUp(self):
+ super(TestResouceTypesClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = resource_types_client.ResourceTypesClient(fake_auth,
+ 'image',
+ 'regionOne')
+
+ def _test_list_resouce_types(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_resource_types,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_RESOURCETYPES,
+ bytes_body)
+
+ def test_list_resouce_types_with_str_body(self):
+ self._test_list_resouce_types()
+
+ def test_list_resouce_types_with_bytes_body(self):
+ self._test_list_resouce_types(bytes_body=True)
diff --git a/tempest/tests/lib/services/image/v2/test_schemas_client.py b/tempest/tests/lib/services/image/v2/test_schemas_client.py
new file mode 100644
index 0000000..4c4b86a
--- /dev/null
+++ b/tempest/tests/lib/services/image/v2/test_schemas_client.py
@@ -0,0 +1,96 @@
+# Copyright 2016 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.services.image.v2 import schemas_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestSchemasClient(base.BaseServiceTest):
+ FAKE_SHOW_SCHEMA = {
+ "links": [
+ {
+ "href": "{schema}",
+ "rel": "describedby"
+ }
+ ],
+ "name": "members",
+ "properties": {
+ "members": {
+ "items": {
+ "name": "member",
+ "properties": {
+ "created_at": {
+ "description": ("Date and time of image member"
+ " creation"),
+ "type": "string"
+ },
+ "image_id": {
+ "description": "An identifier for the image",
+ "pattern": ("^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}"
+ "-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}"
+ "-([0-9a-fA-F]){12}$"),
+ "type": "string"
+ },
+ "member_id": {
+ "description": ("An identifier for the image"
+ " member (tenantId)"),
+ "type": "string"
+ },
+ "schema": {
+ "type": "string"
+ },
+ "status": {
+ "description": "The status of this image member",
+ "enum": [
+ "pending",
+ "accepted",
+ "rejected"
+ ],
+ "type": "string"
+ },
+ "updated_at": {
+ "description": ("Date and time of last"
+ " modification of image member"),
+ "type": "string"
+ }
+ }
+ },
+ "type": "array"
+ },
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+
+ def setUp(self):
+ super(TestSchemasClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = schemas_client.SchemasClient(fake_auth,
+ 'image', 'regionOne')
+
+ def _test_show_schema(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_schema,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_SHOW_SCHEMA,
+ bytes_body,
+ schema="member")
+
+ def test_show_schema_with_str_body(self):
+ self._test_show_schema()
+
+ def test_show_schema_with_bytes_body(self):
+ self._test_show_schema(bytes_body=True)
diff --git a/tempest/tests/lib/services/image/v2/test_versions_client.py b/tempest/tests/lib/services/image/v2/test_versions_client.py
new file mode 100644
index 0000000..6234b06
--- /dev/null
+++ b/tempest/tests/lib/services/image/v2/test_versions_client.py
@@ -0,0 +1,94 @@
+# Copyright 2017 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.services.image.v2 import versions_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestVersionsClient(base.BaseServiceTest):
+
+ FAKE_VERSIONS_INFO = {
+ "versions": [
+ {
+ "status": "CURRENT", "id": "v2.5",
+ "links": [
+ {"href": "https://10.220.1.21:9292/v2/", "rel": "self"}
+ ]
+ },
+ {
+ "status": "SUPPORTED", "id": "v2.4",
+ "links": [
+ {"href": "https://10.220.1.21:9292/v2/", "rel": "self"}
+ ]
+ },
+ {
+ "status": "SUPPORTED", "id": "v2.3",
+ "links": [
+ {"href": "https://10.220.1.21:9292/v2/", "rel": "self"}
+ ]
+ },
+ {
+ "status": "SUPPORTED", "id": "v2.2",
+ "links": [
+ {"href": "https://10.220.1.21:9292/v2/", "rel": "self"}
+ ]
+ },
+ {
+ "status": "SUPPORTED", "id": "v2.1",
+ "links": [
+ {"href": "https://10.220.1.21:9292/v2/", "rel": "self"}
+ ]
+ },
+ {
+ "status": "SUPPORTED", "id": "v2.0",
+ "links": [
+ {"href": "https://10.220.1.21:9292/v2/", "rel": "self"}
+ ]
+ },
+ {
+ "status": "DEPRECATED", "id": "v1.1",
+ "links": [
+ {"href": "https://10.220.1.21:9292/v1/", "rel": "self"}
+ ]
+ },
+ {
+ "status": "DEPRECATED", "id": "v1.0",
+ "links": [
+ {"href": "https://10.220.1.21:9292/v1/", "rel": "self"}
+ ]
+ }
+ ]
+ }
+
+ def setUp(self):
+ super(TestVersionsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = versions_client.VersionsClient(fake_auth,
+ 'image',
+ 'regionOne')
+
+ def _test_list_versions(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_versions,
+ 'tempest.lib.common.rest_client.RestClient.raw_request',
+ self.FAKE_VERSIONS_INFO,
+ bytes_body,
+ 300)
+
+ def test_list_versions_with_str_body(self):
+ self._test_list_versions()
+
+ def test_list_versions_with_bytes_body(self):
+ self._test_list_versions(bytes_body=True)
diff --git a/tempest/services/network/__init__.py b/tempest/tests/lib/services/network/__init__.py
similarity index 100%
rename from tempest/services/network/__init__.py
rename to tempest/tests/lib/services/network/__init__.py
diff --git a/tempest/tests/lib/services/network/test_base_network_client.py b/tempest/tests/lib/services/network/test_base_network_client.py
new file mode 100644
index 0000000..e121cec
--- /dev/null
+++ b/tempest/tests/lib/services/network/test_base_network_client.py
@@ -0,0 +1,96 @@
+# Copyright 2017 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 mock
+
+from tempest.lib.services.network import base as base_network_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib import fake_http
+from tempest.tests.lib.services import base
+
+
+class TestBaseNetworkClient(base.BaseServiceTest):
+
+ def setUp(self):
+ super(TestBaseNetworkClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = base_network_client.BaseNetworkClient(
+ fake_auth, 'compute', 'regionOne')
+
+ self.mock_expected_success = mock.patch.object(
+ self.client, 'expected_success').start()
+
+ def _assert_empty(self, resp):
+ self.assertEqual([], list(resp.keys()))
+
+ @mock.patch('tempest.lib.common.rest_client.RestClient.post')
+ def test_create_resource(self, mock_post):
+ response = fake_http.fake_http_response(headers=None, status=201)
+ mock_post.return_value = response, '{"baz": "qux"}'
+
+ post_data = {'foo': 'bar'}
+ resp = self.client.create_resource('/fake_url', post_data)
+
+ self.assertEqual({'status': '201'}, resp.response)
+ self.assertEqual("qux", resp["baz"])
+ mock_post.assert_called_once_with('v2.0/fake_url', '{"foo": "bar"}')
+ self.mock_expected_success.assert_called_once_with(
+ 201, 201)
+
+ @mock.patch('tempest.lib.common.rest_client.RestClient.post')
+ def test_create_resource_expect_different_values(self, mock_post):
+ response = fake_http.fake_http_response(headers=None, status=200)
+ mock_post.return_value = response, '{}'
+
+ post_data = {'foo': 'bar'}
+ resp = self.client.create_resource('/fake_url', post_data,
+ expect_response_code=200,
+ expect_empty_body=True)
+
+ self.assertEqual({'status': '200'}, resp.response)
+ self._assert_empty(resp)
+ mock_post.assert_called_once_with('v2.0/fake_url', '{"foo": "bar"}')
+ self.mock_expected_success.assert_called_once_with(
+ 200, 200)
+
+ @mock.patch('tempest.lib.common.rest_client.RestClient.put')
+ def test_update_resource(self, mock_put):
+ response = fake_http.fake_http_response(headers=None, status=200)
+ mock_put.return_value = response, '{"baz": "qux"}'
+
+ put_data = {'foo': 'bar'}
+ resp = self.client.update_resource('/fake_url', put_data)
+
+ self.assertEqual({'status': '200'}, resp.response)
+ self.assertEqual("qux", resp["baz"])
+ mock_put.assert_called_once_with('v2.0/fake_url', '{"foo": "bar"}')
+ self.mock_expected_success.assert_called_once_with(
+ 200, 200)
+
+ @mock.patch('tempest.lib.common.rest_client.RestClient.put')
+ def test_update_resource_expect_different_values(self, mock_put):
+ response = fake_http.fake_http_response(headers=None, status=201)
+ mock_put.return_value = response, '{}'
+
+ put_data = {'foo': 'bar'}
+ resp = self.client.update_resource('/fake_url', put_data,
+ expect_response_code=201,
+ expect_empty_body=True)
+
+ self.assertEqual({'status': '201'}, resp.response)
+ self._assert_empty(resp)
+ mock_put.assert_called_once_with('v2.0/fake_url', '{"foo": "bar"}')
+ self.mock_expected_success.assert_called_once_with(
+ 201, 201)
diff --git a/tempest/tests/lib/services/network/test_extensions_client.py b/tempest/tests/lib/services/network/test_extensions_client.py
new file mode 100644
index 0000000..27eb485
--- /dev/null
+++ b/tempest/tests/lib/services/network/test_extensions_client.py
@@ -0,0 +1,201 @@
+# Copyright 2017 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 tempest.lib.services.network import extensions_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestExtensionsClient(base.BaseServiceTest):
+
+ FAKE_EXTENSIONS = {
+ "extensions": [
+ {
+ "updated": "2013-01-20T00:00:00-00:00",
+ "name": "Neutron Service Type Management",
+ "links": [],
+ "alias": "service-type",
+ "description": "API for retrieving service providers for"
+ " Neutron advanced services"
+ },
+ {
+ "updated": "2012-10-05T10:00:00-00:00",
+ "name": "security-group",
+ "links": [],
+ "alias": "security-group",
+ "description": "The security groups extension."
+ },
+ {
+ "updated": "2013-02-07T10:00:00-00:00",
+ "name": "L3 Agent Scheduler",
+ "links": [],
+ "alias": "l3_agent_scheduler",
+ "description": "Schedule routers among l3 agents"
+ },
+ {
+ "updated": "2013-02-07T10:00:00-00:00",
+ "name": "Loadbalancer Agent Scheduler",
+ "links": [],
+ "alias": "lbaas_agent_scheduler",
+ "description": "Schedule pools among lbaas agents"
+ },
+ {
+ "updated": "2013-03-28T10:00:00-00:00",
+ "name": "Neutron L3 Configurable external gateway mode",
+ "links": [],
+ "alias": "ext-gw-mode",
+ "description":
+ "Extension of the router abstraction for specifying whether"
+ " SNAT should occur on the external gateway"
+ },
+ {
+ "updated": "2014-02-03T10:00:00-00:00",
+ "name": "Port Binding",
+ "links": [],
+ "alias": "binding",
+ "description": "Expose port bindings of a virtual port to"
+ " external application"
+ },
+ {
+ "updated": "2012-09-07T10:00:00-00:00",
+ "name": "Provider Network",
+ "links": [],
+ "alias": "provider",
+ "description": "Expose mapping of virtual networks to"
+ " physical networks"
+ },
+ {
+ "updated": "2013-02-03T10:00:00-00:00",
+ "name": "agent",
+ "links": [],
+ "alias": "agent",
+ "description": "The agent management extension."
+ },
+ {
+ "updated": "2012-07-29T10:00:00-00:00",
+ "name": "Quota management support",
+ "links": [],
+ "alias": "quotas",
+ "description": "Expose functions for quotas management per"
+ " tenant"
+ },
+ {
+ "updated": "2013-02-07T10:00:00-00:00",
+ "name": "DHCP Agent Scheduler",
+ "links": [],
+ "alias": "dhcp_agent_scheduler",
+ "description": "Schedule networks among dhcp agents"
+ },
+ {
+ "updated": "2013-06-27T10:00:00-00:00",
+ "name": "Multi Provider Network",
+ "links": [],
+ "alias": "multi-provider",
+ "description": "Expose mapping of virtual networks to"
+ " multiple physical networks"
+ },
+ {
+ "updated": "2013-01-14T10:00:00-00:00",
+ "name": "Neutron external network",
+ "links": [],
+ "alias": "external-net",
+ "description": "Adds external network attribute to network"
+ " resource."
+ },
+ {
+ "updated": "2012-07-20T10:00:00-00:00",
+ "name": "Neutron L3 Router",
+ "links": [],
+ "alias": "router",
+ "description": "Router abstraction for basic L3 forwarding"
+ " between L2 Neutron networks and access to external"
+ " networks via a NAT gateway."
+ },
+ {
+ "updated": "2013-07-23T10:00:00-00:00",
+ "name": "Allowed Address Pairs",
+ "links": [],
+ "alias": "allowed-address-pairs",
+ "description": "Provides allowed address pairs"
+ },
+ {
+ "updated": "2013-03-17T12:00:00-00:00",
+ "name": "Neutron Extra DHCP opts",
+ "links": [],
+ "alias": "extra_dhcp_opt",
+ "description": "Extra options configuration for DHCP. For"
+ " example PXE boot options to DHCP clients can be specified"
+ " (e.g. tftp-server, server-ip-address, bootfile-name)"
+ },
+ {
+ "updated": "2012-10-07T10:00:00-00:00",
+ "name": "LoadBalancing service",
+ "links": [],
+ "alias": "lbaas",
+ "description": "Extension for LoadBalancing service"
+ },
+ {
+ "updated": "2013-02-01T10:00:00-00:00",
+ "name": "Neutron Extra Route",
+ "links": [],
+ "alias": "extraroute",
+ "description": "Extra routes configuration for L3 router"
+ },
+ {
+ "updated": "2016-01-24T10:00:00-00:00",
+ "name": "Neutron Port Data Plane Status",
+ "links": [],
+ "alias": "data-plane-status",
+ "description": "Status of the underlying data plane."
+ }
+ ]
+ }
+
+ FAKE_EXTENSION_ALIAS = "service-type"
+
+ def setUp(self):
+ super(TestExtensionsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.extensions_client = extensions_client.ExtensionsClient(
+ fake_auth, "network", "regionOne")
+
+ def _test_list_extensions(self, bytes_body=False):
+ self.check_service_client_function(
+ self.extensions_client.list_extensions,
+ "tempest.lib.common.rest_client.RestClient.get",
+ self.FAKE_EXTENSIONS,
+ bytes_body,
+ 200)
+
+ def _test_show_extension(self, bytes_body=False):
+ self.check_service_client_function(
+ self.extensions_client.show_extension,
+ "tempest.lib.common.rest_client.RestClient.get",
+ {"extension": self.FAKE_EXTENSIONS["extensions"][0]},
+ bytes_body,
+ 200,
+ ext_alias=self.FAKE_EXTENSION_ALIAS)
+
+ def test_list_extensions_with_str_body(self):
+ self._test_list_extensions()
+
+ def test_list_extensions_with_bytes_body(self):
+ self._test_list_extensions(bytes_body=True)
+
+ def test_show_extension_with_str_body(self):
+ self._test_show_extension()
+
+ def test_show_extension_with_bytes_body(self):
+ self._test_show_extension(bytes_body=True)
diff --git a/tempest/tests/lib/services/network/test_floating_ips_client.py b/tempest/tests/lib/services/network/test_floating_ips_client.py
new file mode 100644
index 0000000..c5b1845
--- /dev/null
+++ b/tempest/tests/lib/services/network/test_floating_ips_client.py
@@ -0,0 +1,145 @@
+# Copyright 2017 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 copy
+
+from tempest.lib.services.network import floating_ips_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestFloatingIPsClient(base.BaseServiceTest):
+
+ FAKE_FLOATING_IPS = {
+ "floatingips": [
+ {
+ "router_id": "d23abc8d-2991-4a55-ba98-2aaea84cc72f",
+ "description": "for test",
+ "created_at": "2016-12-21T10:55:50Z",
+ "updated_at": "2016-12-21T10:55:53Z",
+ "revision_number": 1,
+ "project_id": "4969c491a3c74ee4af974e6d800c62de",
+ "tenant_id": "4969c491a3c74ee4af974e6d800c62de",
+ "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57",
+ "fixed_ip_address": "10.0.0.3",
+ "floating_ip_address": "172.24.4.228",
+ "port_id": "ce705c24-c1ef-408a-bda3-7bbd946164ab",
+ "id": "2f245a7b-796b-4f26-9cf9-9e82d248fda7",
+ "status": "ACTIVE"
+ },
+ {
+ "router_id": None,
+ "description": "for test",
+ "created_at": "2016-12-21T11:55:50Z",
+ "updated_at": "2016-12-21T11:55:53Z",
+ "revision_number": 2,
+ "project_id": "4969c491a3c74ee4af974e6d800c62de",
+ "tenant_id": "4969c491a3c74ee4af974e6d800c62de",
+ "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57",
+ "fixed_ip_address": None,
+ "floating_ip_address": "172.24.4.227",
+ "port_id": None,
+ "id": "61cea855-49cb-4846-997d-801b70c71bdd",
+ "status": "DOWN"
+ }
+ ]
+ }
+
+ FAKE_FLOATING_IP_ID = "2f245a7b-796b-4f26-9cf9-9e82d248fda7"
+
+ def setUp(self):
+ super(TestFloatingIPsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.floating_ips_client = floating_ips_client.FloatingIPsClient(
+ fake_auth, "compute", "regionOne")
+
+ def _test_list_floatingips(self, bytes_body=False):
+ self.check_service_client_function(
+ self.floating_ips_client.list_floatingips,
+ "tempest.lib.common.rest_client.RestClient.get",
+ self.FAKE_FLOATING_IPS,
+ bytes_body,
+ 200)
+
+ def _test_create_floatingip(self, bytes_body=False):
+ self.check_service_client_function(
+ self.floating_ips_client.create_floatingip,
+ "tempest.lib.common.rest_client.RestClient.post",
+ {"floatingip": self.FAKE_FLOATING_IPS["floatingips"][1]},
+ bytes_body,
+ 201,
+ floating_network_id="172.24.4.228")
+
+ def _test_show_floatingip(self, bytes_body=False):
+ self.check_service_client_function(
+ self.floating_ips_client.show_floatingip,
+ "tempest.lib.common.rest_client.RestClient.get",
+ {"floatingip": self.FAKE_FLOATING_IPS["floatingips"][0]},
+ bytes_body,
+ 200,
+ floatingip_id=self.FAKE_FLOATING_IP_ID)
+
+ def _test_update_floatingip(self, bytes_body=False):
+ update_kwargs = {
+ "port_id": "fc861431-0e6c-4842-a0ed-e2363f9bc3a8"
+ }
+
+ resp_body = {
+ "floatingip": copy.deepcopy(
+ self.FAKE_FLOATING_IPS["floatingips"][0]
+ )
+ }
+ resp_body["floatingip"].update(update_kwargs)
+
+ self.check_service_client_function(
+ self.floating_ips_client.update_floatingip,
+ "tempest.lib.common.rest_client.RestClient.put",
+ resp_body,
+ bytes_body,
+ 200,
+ floatingip_id=self.FAKE_FLOATING_IP_ID,
+ **update_kwargs)
+
+ def test_list_floatingips_with_str_body(self):
+ self._test_list_floatingips()
+
+ def test_list_floatingips_with_bytes_body(self):
+ self._test_list_floatingips(bytes_body=True)
+
+ def test_create_floatingip_with_str_body(self):
+ self._test_create_floatingip()
+
+ def test_create_floatingip_with_bytes_body(self):
+ self._test_create_floatingip(bytes_body=True)
+
+ def test_show_floatingips_with_str_body(self):
+ self._test_show_floatingip()
+
+ def test_show_floatingips_with_bytes_body(self):
+ self._test_show_floatingip(bytes_body=True)
+
+ def test_update_floatingip_with_str_body(self):
+ self._test_update_floatingip()
+
+ def test_update_floatingip_with_bytes_body(self):
+ self._test_update_floatingip(bytes_body=True)
+
+ def test_delete_floatingip(self):
+ self.check_service_client_function(
+ self.floating_ips_client.delete_floatingip,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ status=204,
+ floatingip_id=self.FAKE_FLOATING_IP_ID)
diff --git a/tempest/tests/lib/services/network/test_metering_label_rules_client.py b/tempest/tests/lib/services/network/test_metering_label_rules_client.py
new file mode 100644
index 0000000..047c34f
--- /dev/null
+++ b/tempest/tests/lib/services/network/test_metering_label_rules_client.py
@@ -0,0 +1,110 @@
+# Copyright 2017 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 tempest.lib.services.network import metering_label_rules_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestMeteringLabelRulesClient(base.BaseServiceTest):
+
+ FAKE_METERING_LABEL_RULES = {
+ "metering_label_rules": [
+ {
+ "remote_ip_prefix": "20.0.0.0/24",
+ "direction": "ingress",
+ "metering_label_id": "e131d186-b02d-4c0b-83d5-0c0725c4f812",
+ "id": "9536641a-7d14-4dc5-afaf-93a973ce0eb8",
+ "excluded": False
+ },
+ {
+ "remote_ip_prefix": "10.0.0.0/24",
+ "direction": "ingress",
+ "metering_label_id": "e131d186-b02d-4c0b-83d5-0c0725c4f812",
+ "id": "ffc6fd15-40de-4e7d-b617-34d3f7a93aec",
+ "excluded": False
+ }
+ ]
+ }
+
+ FAKE_METERING_LABEL_RULE = {
+ "remote_ip_prefix": "20.0.0.0/24",
+ "direction": "ingress",
+ "metering_label_id": "e131d186-b02d-4c0b-83d5-0c0725c4f812"
+ }
+
+ FAKE_METERING_LABEL_ID = "e131d186-b02d-4c0b-83d5-0c0725c4f812"
+ FAKE_METERING_LABEL_RULE_ID = "9536641a-7d14-4dc5-afaf-93a973ce0eb8"
+
+ def setUp(self):
+ super(TestMeteringLabelRulesClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.metering_label_rules_client = \
+ metering_label_rules_client.MeteringLabelRulesClient(
+ fake_auth, "network", "regionOne")
+
+ def _test_list_metering_label_rules(self, bytes_body=False):
+ self.check_service_client_function(
+ self.metering_label_rules_client.list_metering_label_rules,
+ "tempest.lib.common.rest_client.RestClient.get",
+ self.FAKE_METERING_LABEL_RULES,
+ bytes_body,
+ 200)
+
+ def _test_create_metering_label_rule(self, bytes_body=False):
+ self.check_service_client_function(
+ self.metering_label_rules_client.create_metering_label_rule,
+ "tempest.lib.common.rest_client.RestClient.post",
+ {"metering_label_rule": self.FAKE_METERING_LABEL_RULES[
+ "metering_label_rules"][0]},
+ bytes_body,
+ 201,
+ **self.FAKE_METERING_LABEL_RULE)
+
+ def _test_show_metering_label_rule(self, bytes_body=False):
+ self.check_service_client_function(
+ self.metering_label_rules_client.show_metering_label_rule,
+ "tempest.lib.common.rest_client.RestClient.get",
+ {"metering_label_rule": self.FAKE_METERING_LABEL_RULES[
+ "metering_label_rules"][0]},
+ bytes_body,
+ 200,
+ metering_label_rule_id=self.FAKE_METERING_LABEL_RULE_ID)
+
+ def test_delete_metering_label_rule(self):
+ self.check_service_client_function(
+ self.metering_label_rules_client.delete_metering_label_rule,
+ "tempest.lib.common.rest_client.RestClient.delete",
+ {},
+ status=204,
+ metering_label_rule_id=self.FAKE_METERING_LABEL_RULE_ID)
+
+ def test_list_metering_label_rules_with_str_body(self):
+ self._test_list_metering_label_rules()
+
+ def test_list_metering_label_rules_with_bytes_body(self):
+ self._test_list_metering_label_rules(bytes_body=True)
+
+ def test_create_metering_label_rule_with_str_body(self):
+ self._test_create_metering_label_rule()
+
+ def test_create_metering_label_rule_with_bytes_body(self):
+ self._test_create_metering_label_rule(bytes_body=True)
+
+ def test_show_metering_label_rule_with_str_body(self):
+ self._test_show_metering_label_rule()
+
+ def test_show_metering_label_rule_with_bytes_body(self):
+ self._test_show_metering_label_rule(bytes_body=True)
diff --git a/tempest/tests/lib/services/network/test_metering_labels_client.py b/tempest/tests/lib/services/network/test_metering_labels_client.py
new file mode 100644
index 0000000..a048326
--- /dev/null
+++ b/tempest/tests/lib/services/network/test_metering_labels_client.py
@@ -0,0 +1,107 @@
+# Copyright 2017 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 tempest.lib.services.network import metering_labels_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestMeteringLabelsClient(base.BaseServiceTest):
+
+ FAKE_METERING_LABELS = {
+ "metering_labels": [
+ {
+ "project_id": "45345b0ee1ea477fac0f541b2cb79cd4",
+ "tenant_id": "45345b0ee1ea477fac0f541b2cb79cd4",
+ "description": "label1 description",
+ "name": "label1",
+ "id": "a6700594-5b7a-4105-8bfe-723b346ce866",
+ "shared": False
+ },
+ {
+ "project_id": "45345b0ee1ea477fac0f541b2cb79cd4",
+ "tenant_id": "45345b0ee1ea477fac0f541b2cb79cd4",
+ "description": "label2 description",
+ "name": "label2",
+ "id": "e131d186-b02d-4c0b-83d5-0c0725c4f812",
+ "shared": False
+ }
+ ]
+ }
+
+ FAKE_METERING_LABEL_ID = "a6700594-5b7a-4105-8bfe-723b346ce866"
+
+ def setUp(self):
+ super(TestMeteringLabelsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.metering_labels_client = \
+ metering_labels_client.MeteringLabelsClient(
+ fake_auth, "network", "regionOne")
+
+ def _test_list_metering_labels(self, bytes_body=False):
+ self.check_service_client_function(
+ self.metering_labels_client.list_metering_labels,
+ "tempest.lib.common.rest_client.RestClient.get",
+ self.FAKE_METERING_LABELS,
+ bytes_body,
+ 200)
+
+ def _test_create_metering_label(self, bytes_body=False):
+ self.check_service_client_function(
+ self.metering_labels_client.create_metering_label,
+ "tempest.lib.common.rest_client.RestClient.post",
+ {"metering_label": self.FAKE_METERING_LABELS[
+ "metering_labels"][1]},
+ bytes_body,
+ 201,
+ name="label1",
+ description="label1 description",
+ shared=False)
+
+ def _test_show_metering_label(self, bytes_body=False):
+ self.check_service_client_function(
+ self.metering_labels_client.show_metering_label,
+ "tempest.lib.common.rest_client.RestClient.get",
+ {"metering_label": self.FAKE_METERING_LABELS[
+ "metering_labels"][0]},
+ bytes_body,
+ 200,
+ metering_label_id=self.FAKE_METERING_LABEL_ID)
+
+ def test_delete_metering_label(self):
+ self.check_service_client_function(
+ self.metering_labels_client.delete_metering_label,
+ "tempest.lib.common.rest_client.RestClient.delete",
+ {},
+ status=204,
+ metering_label_id=self.FAKE_METERING_LABEL_ID)
+
+ def test_list_metering_labels_with_str_body(self):
+ self._test_list_metering_labels()
+
+ def test_list_metering_labels_with_bytes_body(self):
+ self._test_list_metering_labels(bytes_body=True)
+
+ def test_create_metering_label_with_str_body(self):
+ self._test_create_metering_label()
+
+ def test_create_metering_label_with_bytes_body(self):
+ self._test_create_metering_label(bytes_body=True)
+
+ def test_show_metering_label_with_str_body(self):
+ self._test_show_metering_label()
+
+ def test_show_metering_label_with_bytes_body(self):
+ self._test_show_metering_label(bytes_body=True)
diff --git a/tempest/tests/lib/services/network/test_networks_client.py b/tempest/tests/lib/services/network/test_networks_client.py
new file mode 100644
index 0000000..078f4b0
--- /dev/null
+++ b/tempest/tests/lib/services/network/test_networks_client.py
@@ -0,0 +1,242 @@
+# Copyright 2017 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 copy
+
+from tempest.lib.services.network import networks_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestNetworksClient(base.BaseServiceTest):
+
+ FAKE_NETWORKS = {
+ "networks": [
+ {
+ "admin_state_up": True,
+ "availability_zone_hints": [],
+ "availability_zones": [
+ "nova"
+ ],
+ "created_at": "2016-03-08T20:19:41",
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+ "mtu": 0,
+ "name": "net1",
+ "port_security_enabled": True,
+ "project_id": "4fd44f30292945e481c7b8a0c8908869",
+ "qos_policy_id": "6a8454ade84346f59e8d40665f878b2e",
+ "router:external": False,
+ "shared": False,
+ "status": "ACTIVE",
+ "subnets": [
+ "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+ ],
+ "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+ "updated_at": "2016-03-08T20:19:41",
+ "vlan_transparent": True,
+ "description": ""
+ },
+ {
+ "admin_state_up": True,
+ "availability_zone_hints": [],
+ "availability_zones": [
+ "nova"
+ ],
+ "id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+ "mtu": 0,
+ "name": "net2",
+ "port_security_enabled": True,
+ "project_id": "26a7980765d0414dbc1fc1f88cdb7e6e",
+ "qos_policy_id": "bfdb6c39f71e4d44b1dfbda245c50819",
+ "router:external": False,
+ "shared": False,
+ "status": "ACTIVE",
+ "subnets": [
+ "08eae331-0402-425a-923c-34f7cfe39c1b"
+ ],
+ "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e",
+ "updated_at": "2016-03-08T20:19:41",
+ "vlan_transparent": False,
+ "description": ""
+ }
+ ]
+ }
+
+ FAKE_NETWORK_ID = "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+
+ FAKE_NETWORK1 = {
+ "name": "net1",
+ "admin_state_up": True,
+ "qos_policy_id": "6a8454ade84346f59e8d40665f878b2e"
+ }
+
+ FAKE_NETWORK2 = {
+ "name": "net2",
+ "admin_state_up": True,
+ "qos_policy_id": "bfdb6c39f71e4d44b1dfbda245c50819"
+ }
+
+ FAKE_NETWORKS_REQ = {
+ "networks": [
+ FAKE_NETWORK1,
+ FAKE_NETWORK2
+ ]
+ }
+
+ FAKE_DHCP_AGENT_NETWORK_ID = "80515c45-651f-4f9a-b82b-2ca8a7301a8d"
+
+ FAKE_DHCP_AGENTS = {
+ "agents": [
+ {
+ "binary": "neutron-dhcp-agent",
+ "description": None,
+ "admin_state_up": True,
+ "heartbeat_timestamp": "2017-06-22 18:29:50",
+ "availability_zone": "nova",
+ "alive": True,
+ "topic": "dhcp_agent",
+ "host": "osboxes",
+ "agent_type": "DHCP agent",
+ "resource_versions": {},
+ "created_at": "2017-06-19 21:39:51",
+ "started_at": "2017-06-19 21:39:51",
+ "id": "b6cfb7a1-6ac4-4980-993c-9d295d37062e",
+ "configurations": {
+ "subnets": 2,
+ "dhcp_lease_duration": 86400,
+ "dhcp_driver": "neutron.agent.linux.dhcp.Dnsmasq",
+ "networks": 1,
+ "log_agent_heartbeats": False,
+ "ports": 3
+ }
+ }
+ ]
+ }
+
+ def setUp(self):
+ super(TestNetworksClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.networks_client = networks_client.NetworksClient(
+ fake_auth, "network", "regionOne")
+
+ def _test_list_networks(self, bytes_body=False):
+ self.check_service_client_function(
+ self.networks_client.list_networks,
+ "tempest.lib.common.rest_client.RestClient.get",
+ self.FAKE_NETWORKS,
+ bytes_body,
+ 200)
+
+ def _test_create_network(self, bytes_body=False):
+ self.check_service_client_function(
+ self.networks_client.create_network,
+ "tempest.lib.common.rest_client.RestClient.post",
+ {"network": self.FAKE_NETWORKS["networks"][0]},
+ bytes_body,
+ 201,
+ **self.FAKE_NETWORK1)
+
+ def _test_create_bulk_networks(self, bytes_body=False):
+ self.check_service_client_function(
+ self.networks_client.create_bulk_networks,
+ "tempest.lib.common.rest_client.RestClient.post",
+ self.FAKE_NETWORKS,
+ bytes_body,
+ 201,
+ networks=self.FAKE_NETWORKS_REQ)
+
+ def _test_show_network(self, bytes_body=False):
+ self.check_service_client_function(
+ self.networks_client.show_network,
+ "tempest.lib.common.rest_client.RestClient.get",
+ {"network": self.FAKE_NETWORKS["networks"][0]},
+ bytes_body,
+ 200,
+ network_id=self.FAKE_NETWORK_ID)
+
+ def _test_update_network(self, bytes_body=False):
+ update_kwargs = {
+ "name": "sample_network_5_updated",
+ "qos_policy_id": "6a8454ade84346f59e8d40665f878b2e"
+ }
+
+ resp_body = {
+ "network": copy.deepcopy(
+ self.FAKE_NETWORKS["networks"][0]
+ )
+ }
+ resp_body["network"].update(update_kwargs)
+
+ self.check_service_client_function(
+ self.networks_client.update_network,
+ "tempest.lib.common.rest_client.RestClient.put",
+ resp_body,
+ bytes_body,
+ 200,
+ network_id=self.FAKE_NETWORK_ID,
+ **update_kwargs)
+
+ def _test_list_dhcp_agents_on_hosting_network(self, bytes_body=False):
+ self.check_service_client_function(
+ self.networks_client.list_dhcp_agents_on_hosting_network,
+ "tempest.lib.common.rest_client.RestClient.get",
+ self.FAKE_DHCP_AGENTS,
+ bytes_body,
+ 200,
+ network_id=self.FAKE_DHCP_AGENT_NETWORK_ID)
+
+ def test_delete_network(self):
+ self.check_service_client_function(
+ self.networks_client.delete_network,
+ "tempest.lib.common.rest_client.RestClient.delete",
+ {},
+ status=204,
+ network_id=self.FAKE_NETWORK_ID)
+
+ def test_list_networks_with_str_body(self):
+ self._test_list_networks()
+
+ def test_list_networks_with_bytes_body(self):
+ self._test_list_networks(bytes_body=True)
+
+ def test_create_network_with_str_body(self):
+ self._test_create_network()
+
+ def test_create_network_with_bytes_body(self):
+ self._test_create_network(bytes_body=True)
+
+ def test_create_bulk_network_with_str_body(self):
+ self._test_create_bulk_networks()
+
+ def test_create_bulk_network_with_bytes_body(self):
+ self._test_create_bulk_networks(bytes_body=True)
+
+ def test_show_network_with_str_body(self):
+ self._test_show_network()
+
+ def test_show_network_with_bytes_body(self):
+ self._test_show_network(bytes_body=True)
+
+ def test_update_network_with_str_body(self):
+ self._test_update_network()
+
+ def test_update_network_with_bytes_body(self):
+ self._test_update_network(bytes_body=True)
+
+ def test_list_dhcp_agents_on_hosting_network_with_str_body(self):
+ self._test_list_dhcp_agents_on_hosting_network()
+
+ def test_list_dhcp_agents_on_hosting_network_with_bytes_body(self):
+ self._test_list_dhcp_agents_on_hosting_network(bytes_body=True)
diff --git a/tempest/tests/lib/services/network/test_ports_client.py b/tempest/tests/lib/services/network/test_ports_client.py
new file mode 100644
index 0000000..20ef3f1
--- /dev/null
+++ b/tempest/tests/lib/services/network/test_ports_client.py
@@ -0,0 +1,198 @@
+# Copyright 2017 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 copy
+
+from tempest.lib.services.network import ports_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestPortsClient(base.BaseServiceTest):
+
+ FAKE_PORTS = {
+ "ports": [
+ {
+ "admin_state_up": True,
+ "allowed_address_pairs": [],
+ "data_plane_status": None,
+ "description": "",
+ "device_id": "9ae135f4-b6e0-4dad-9e91-3c223e385824",
+ "device_owner": "network:router_gateway",
+ "extra_dhcp_opts": [],
+ "fixed_ips": [
+ {
+ "ip_address": "172.24.4.2",
+ "subnet_id": "008ba151-0b8c-4a67-98b5-0d2b87666062"
+ }
+ ],
+ "id": "d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b",
+ "mac_address": "fa:16:3e:58:42:ed",
+ "name": "",
+ "network_id": "70c1db1f-b701-45bd-96e0-a313ee3430b3",
+ "project_id": "",
+ "security_groups": [],
+ "status": "ACTIVE",
+ "tenant_id": ""
+ },
+ {
+ "admin_state_up": True,
+ "allowed_address_pairs": [],
+ "data_plane_status": None,
+ "description": "",
+ "device_id": "9ae135f4-b6e0-4dad-9e91-3c223e385824",
+ "device_owner": "network:router_interface",
+ "extra_dhcp_opts": [],
+ "fixed_ips": [
+ {
+ "ip_address": "10.0.0.1",
+ "subnet_id": "288bf4a1-51ba-43b6-9d0a-520e9005db17"
+ }
+ ],
+ "id": "f71a6703-d6de-4be1-a91a-a570ede1d159",
+ "mac_address": "fa:16:3e:bb:3c:e4",
+ "name": "",
+ "network_id": "f27aa545-cbdd-4907-b0c6-c9e8b039dcc2",
+ "project_id": "d397de8a63f341818f198abb0966f6f3",
+ "security_groups": [],
+ "status": "ACTIVE",
+ "tenant_id": "d397de8a63f341818f198abb0966f6f3"
+ }
+ ]
+ }
+
+ FAKE_PORT_ID = "d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b"
+
+ FAKE_PORT1 = {
+ "admin_state_up": True,
+ "name": "",
+ "network_id": "70c1db1f-b701-45bd-96e0-a313ee3430b3"
+ }
+
+ FAKE_PORT2 = {
+ "admin_state_up": True,
+ "name": "",
+ "network_id": "f27aa545-cbdd-4907-b0c6-c9e8b039dcc2"
+ }
+
+ FAKE_PORTS_REQ = {
+ "ports": [
+ FAKE_PORT1,
+ FAKE_PORT2
+ ]
+ }
+
+ def setUp(self):
+ super(TestPortsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.ports_client = ports_client.PortsClient(
+ fake_auth, "network", "regionOne")
+
+ def _test_list_ports(self, bytes_body=False):
+ self.check_service_client_function(
+ self.ports_client.list_ports,
+ "tempest.lib.common.rest_client.RestClient.get",
+ self.FAKE_PORTS,
+ bytes_body,
+ 200)
+
+ def _test_create_port(self, bytes_body=False):
+ self.check_service_client_function(
+ self.ports_client.create_port,
+ "tempest.lib.common.rest_client.RestClient.post",
+ {"port": self.FAKE_PORTS["ports"][0]},
+ bytes_body,
+ 201,
+ **self.FAKE_PORT1)
+
+ def _test_create_bulk_ports(self, bytes_body=False):
+ self.check_service_client_function(
+ self.ports_client.create_bulk_ports,
+ "tempest.lib.common.rest_client.RestClient.post",
+ self.FAKE_PORTS,
+ bytes_body,
+ 201,
+ ports=self.FAKE_PORTS_REQ)
+
+ def _test_show_port(self, bytes_body=False):
+ self.check_service_client_function(
+ self.ports_client.show_port,
+ "tempest.lib.common.rest_client.RestClient.get",
+ {"port": self.FAKE_PORTS["ports"][0]},
+ bytes_body,
+ 200,
+ port_id=self.FAKE_PORT_ID)
+
+ def _test_update_port(self, bytes_body=False):
+ update_kwargs = {
+ "admin_state_up": True,
+ "device_id": "d90a13da-be41-461f-9f99-1dbcf438fdf2",
+ "device_owner": "compute:nova",
+ "name": "test-for-port-update"
+ }
+
+ resp_body = {
+ "port": copy.deepcopy(
+ self.FAKE_PORTS["ports"][0]
+ )
+ }
+ resp_body["port"].update(update_kwargs)
+
+ self.check_service_client_function(
+ self.ports_client.update_port,
+ "tempest.lib.common.rest_client.RestClient.put",
+ resp_body,
+ bytes_body,
+ 200,
+ port_id=self.FAKE_PORT_ID,
+ **update_kwargs)
+
+ def test_delete_port(self):
+ self.check_service_client_function(
+ self.ports_client.delete_port,
+ "tempest.lib.common.rest_client.RestClient.delete",
+ {},
+ status=204,
+ port_id=self.FAKE_PORT_ID)
+
+ def test_list_ports_with_str_body(self):
+ self._test_list_ports()
+
+ def test_list_ports_with_bytes_body(self):
+ self._test_list_ports(bytes_body=True)
+
+ def test_create_port_with_str_body(self):
+ self._test_create_port()
+
+ def test_create_port_with_bytes_body(self):
+ self._test_create_port(bytes_body=True)
+
+ def test_create_bulk_port_with_str_body(self):
+ self._test_create_bulk_ports()
+
+ def test_create_bulk_port_with_bytes_body(self):
+ self._test_create_bulk_ports(bytes_body=True)
+
+ def test_show_port_with_str_body(self):
+ self._test_show_port()
+
+ def test_show_port_with_bytes_body(self):
+ self._test_show_port(bytes_body=True)
+
+ def test_update_port_with_str_body(self):
+ self._test_update_port()
+
+ def test_update_port_with_bytes_body(self):
+ self._test_update_port(bytes_body=True)
diff --git a/tempest/tests/lib/services/network/test_quotas_client.py b/tempest/tests/lib/services/network/test_quotas_client.py
new file mode 100644
index 0000000..aa6c1a1
--- /dev/null
+++ b/tempest/tests/lib/services/network/test_quotas_client.py
@@ -0,0 +1,183 @@
+# Copyright 2017 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 tempest.lib.services.network import quotas_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestQuotasClient(base.BaseServiceTest):
+
+ FAKE_QUOTAS = {
+ "quotas": [
+ {
+ "floatingip": 50,
+ "network": 15,
+ "port": 50,
+ "project_id": "bab7d5c60cd041a0a36f7c4b6e1dd978",
+ "rbac_policy": -1,
+ "router": 10,
+ "security_group": 10,
+ "security_group_rule": 100,
+ "subnet": 10,
+ "subnetpool": -1,
+ "tenant_id": "bab7d5c60cd041a0a36f7c4b6e1dd978"
+ }
+ ]
+ }
+
+ FAKE_PROJECT_QUOTAS = {
+ "quota": {
+ "floatingip": 50,
+ "network": 10,
+ "port": 50,
+ "rbac_policy": -1,
+ "router": 10,
+ "security_group": 10,
+ "security_group_rule": 100,
+ "subnet": 10,
+ "subnetpool": -1
+ }
+ }
+
+ FAKE_QUOTA_TENANT_ID = "bab7d5c60cd041a0a36f7c4b6e1dd978"
+
+ FAKE_QUOTA_DETAILS = {
+ "quota": {
+ "rbac_policy": {
+ "used": 4,
+ "limit": 10,
+ "reserved": 0
+ },
+ "subnetpool": {
+ "used": 2,
+ "limit": -1,
+ "reserved": 0
+ },
+ "security_group_rule": {
+ "used": 10,
+ "limit": 100,
+ "reserved": 1
+ },
+ "security_group": {
+ "used": 3,
+ "limit": 10,
+ "reserved": 0
+ },
+ "subnet": {
+ "used": 3,
+ "limit": 100,
+ "reserved": 0
+ },
+ "port": {
+ "used": 21,
+ "limit": 500,
+ "reserved": 3
+ },
+ "network": {
+ "used": 9,
+ "limit": 100,
+ "reserved": 2
+ }
+ }
+ }
+
+ def setUp(self):
+ super(TestQuotasClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.quotas_client = quotas_client.QuotasClient(
+ fake_auth, "network", "regionOne")
+
+ def _test_list_quotas(self, bytes_body=False):
+ self.check_service_client_function(
+ self.quotas_client.list_quotas,
+ "tempest.lib.common.rest_client.RestClient.get",
+ self.FAKE_QUOTAS,
+ bytes_body,
+ 200)
+
+ def _test_show_quotas(self, bytes_body=False):
+ self.check_service_client_function(
+ self.quotas_client.show_quotas,
+ "tempest.lib.common.rest_client.RestClient.get",
+ self.FAKE_PROJECT_QUOTAS,
+ bytes_body,
+ 200,
+ tenant_id=self.FAKE_QUOTA_TENANT_ID)
+
+ def _test_show_default_quotas(self, bytes_body=False):
+ self.check_service_client_function(
+ self.quotas_client.show_default_quotas,
+ "tempest.lib.common.rest_client.RestClient.get",
+ self.FAKE_PROJECT_QUOTAS,
+ bytes_body,
+ 200,
+ tenant_id=self.FAKE_QUOTA_TENANT_ID)
+
+ def _test_update_quotas(self, bytes_body=False):
+ self.check_service_client_function(
+ self.quotas_client.update_quotas,
+ "tempest.lib.common.rest_client.RestClient.put",
+ self.FAKE_PROJECT_QUOTAS,
+ bytes_body,
+ 200,
+ tenant_id=self.FAKE_QUOTA_TENANT_ID)
+
+ def _test_show_quota_details(self, bytes_body=False):
+ self.check_service_client_function(
+ self.quotas_client.show_quota_details,
+ "tempest.lib.common.rest_client.RestClient.get",
+ self.FAKE_QUOTA_DETAILS,
+ bytes_body,
+ 200,
+ tenant_id=self.FAKE_QUOTA_TENANT_ID)
+
+ def test_reset_quotas(self):
+ self.check_service_client_function(
+ self.quotas_client.reset_quotas,
+ "tempest.lib.common.rest_client.RestClient.delete",
+ {},
+ status=204,
+ tenant_id=self.FAKE_QUOTA_TENANT_ID)
+
+ def test_list_quotas_with_str_body(self):
+ self._test_list_quotas()
+
+ def test_list_quotas_with_bytes_body(self):
+ self._test_list_quotas(bytes_body=True)
+
+ def test_show_quotas_with_str_body(self):
+ self._test_show_quotas()
+
+ def test_show_quotas_with_bytes_body(self):
+ self._test_show_quotas(bytes_body=True)
+
+ def test_show_default_quotas_with_str_body(self):
+ self._test_show_default_quotas()
+
+ def test_show_default_quotas_with_bytes_body(self):
+ self._test_show_default_quotas(bytes_body=True)
+
+ def test_update_quotas_with_str_body(self):
+ self._test_update_quotas()
+
+ def test_update_quotas_with_bytes_body(self):
+ self._test_update_quotas(bytes_body=True)
+
+ def test_show_quota_details_with_str_body(self):
+ self._test_show_quota_details()
+
+ def test_show_quota_details_with_bytes_body(self):
+ self._test_show_quota_details(bytes_body=True)
diff --git a/tempest/tests/lib/services/network/test_routers_client.py b/tempest/tests/lib/services/network/test_routers_client.py
new file mode 100644
index 0000000..2fa5993
--- /dev/null
+++ b/tempest/tests/lib/services/network/test_routers_client.py
@@ -0,0 +1,109 @@
+# Copyright 2016 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.services.network import routers_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestRoutersClient(base.BaseServiceTest):
+ FAKE_CREATE_ROUTER = {
+ "router": {
+ "name": u'\u2740(*\xb4\u25e1`*)\u2740',
+ "external_gateway_info": {
+ "network_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b",
+ "enable_snat": True,
+ "external_fixed_ips": [
+ {
+ "subnet_id": "255.255.255.0",
+ "ip": "192.168.10.1"
+ }
+ ]
+ },
+ "admin_state_up": True,
+ "id": "8604a0de-7f6b-409a-a47c-a1cc7bc77b2e"
+ }
+ }
+
+ FAKE_UPDATE_ROUTER = {
+ "router": {
+ "name": u'\u2740(*\xb4\u25e1`*)\u2740',
+ "external_gateway_info": {
+ "network_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b",
+ "enable_snat": True,
+ "external_fixed_ips": [
+ {
+ "subnet_id": "255.255.255.0",
+ "ip": "192.168.10.1"
+ }
+ ]
+ },
+ "admin_state_up": False,
+ "id": "8604a0de-7f6b-409a-a47c-a1cc7bc77b2e"
+ }
+ }
+
+ def setUp(self):
+ super(TestRoutersClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = routers_client.RoutersClient(fake_auth,
+ 'network', 'regionOne')
+
+ def _test_list_routers(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_routers,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {"routers": []},
+ bytes_body)
+
+ def _test_create_router(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_router,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_ROUTER,
+ bytes_body,
+ name="another_router", admin_state_up="true", status=201)
+
+ def _test_update_router(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_router,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ self.FAKE_UPDATE_ROUTER,
+ bytes_body,
+ router_id="8604a0de-7f6b-409a-a47c-a1cc7bc77b2e",
+ admin_state_up=False)
+
+ def test_list_routers_with_str_body(self):
+ self._test_list_routers()
+
+ def test_list_routers_with_bytes_body(self):
+ self._test_list_routers(bytes_body=True)
+
+ def test_create_router_with_str_body(self):
+ self._test_create_router()
+
+ def test_create_router_with_bytes_body(self):
+ self._test_create_router(bytes_body=True)
+
+ def test_delete_router(self):
+ self.check_service_client_function(
+ self.client.delete_router,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {}, router_id="1", status=204)
+
+ def test_update_router_with_str_body(self):
+ self._test_update_router()
+
+ def test_update_router_with_bytes_body(self):
+ self._test_update_router(bytes_body=True)
diff --git a/tempest/tests/lib/services/network/test_security_group_rules_client.py b/tempest/tests/lib/services/network/test_security_group_rules_client.py
new file mode 100644
index 0000000..b9c17a1
--- /dev/null
+++ b/tempest/tests/lib/services/network/test_security_group_rules_client.py
@@ -0,0 +1,138 @@
+# Copyright 2017 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 copy
+
+import mock
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.services.network import base as network_base
+from tempest.lib.services.network import security_group_rules_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestSecurityGroupsClient(base.BaseServiceTest):
+
+ FAKE_SEC_GROUP_RULE_ID = "3c0e45ff-adaf-4124-b083-bf390e5482ff"
+
+ FAKE_SECURITY_GROUP_RULES = {
+ "security_group_rules": [
+ {
+ "direction": "egress",
+ "ethertype": "IPv6",
+ "id": "3c0e45ff-adaf-4124-b083-bf390e5482ff",
+ "port_range_max": None,
+ "port_range_min": None,
+ "protocol": None,
+ "remote_group_id": None,
+ "remote_ip_prefix": None,
+ "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+ "project_id": "e4f50856753b4dc6afee5fa6b9b6c550",
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550",
+ "description": ""
+ },
+ {
+ "direction": "egress",
+ "ethertype": "IPv4",
+ "id": "93aa42e5-80db-4581-9391-3a608bd0e448",
+ "port_range_max": None,
+ "port_range_min": None,
+ "protocol": None,
+ "remote_group_id": None,
+ "remote_ip_prefix": None,
+ "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+ "project_id": "e4f50856753b4dc6afee5fa6b9b6c550",
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550",
+ "description": ""
+ }
+ ]
+ }
+
+ FAKE_SECURITY_GROUP_RULE = copy.copy(
+ FAKE_SECURITY_GROUP_RULES['security_group_rules'][0])
+
+ def setUp(self):
+ super(TestSecurityGroupsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = security_group_rules_client.SecurityGroupRulesClient(
+ fake_auth, 'network', 'regionOne')
+
+ def _test_list_security_group_rules(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_security_group_rules,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_SECURITY_GROUP_RULES,
+ bytes_body,
+ mock_args='v2.0/security-group-rules')
+
+ def _test_create_security_group_rule(self, bytes_body=False):
+ kwargs = {'direction': 'egress',
+ 'security_group_id': '85cc3048-abc3-43cc-89b3-377341426ac5',
+ 'remote_ip_prefix': None}
+ payload = json.dumps({"security_group_rule": kwargs}, sort_keys=True)
+ json_dumps = json.dumps
+
+ # 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(network_base.json, 'dumps') as mock_dumps:
+ mock_dumps.side_effect = lambda d: json_dumps(d, sort_keys=True)
+
+ self.check_service_client_function(
+ self.client.create_security_group_rule,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_SECURITY_GROUP_RULE,
+ bytes_body,
+ status=201,
+ mock_args=['v2.0/security-group-rules', payload],
+ **kwargs)
+
+ def _test_show_security_group_rule(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_security_group_rule,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_SECURITY_GROUP_RULE,
+ bytes_body,
+ security_group_rule_id=self.FAKE_SEC_GROUP_RULE_ID,
+ mock_args='v2.0/security-group-rules/%s'
+ % self.FAKE_SEC_GROUP_RULE_ID)
+
+ def test_list_security_group_rules_with_str_body(self):
+ self._test_list_security_group_rules()
+
+ def test_list_security_group_rules_with_bytes_body(self):
+ self._test_list_security_group_rules(bytes_body=True)
+
+ def test_create_security_group_rule_with_str_body(self):
+ self._test_create_security_group_rule()
+
+ def test_create_security_group_rule_with_bytes_body(self):
+ self._test_create_security_group_rule(bytes_body=True)
+
+ def test_show_security_group_rule_with_str_body(self):
+ self._test_show_security_group_rule()
+
+ def test_show_security_group_rule_with_bytes_body(self):
+ self._test_show_security_group_rule(bytes_body=True)
+
+ def test_delete_security_group_rule(self):
+ self.check_service_client_function(
+ self.client.delete_security_group_rule,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ status=204,
+ security_group_rule_id=self.FAKE_SEC_GROUP_RULE_ID,
+ mock_args='v2.0/security-group-rules/%s'
+ % self.FAKE_SEC_GROUP_RULE_ID)
diff --git a/tempest/tests/lib/services/network/test_security_groups_client.py b/tempest/tests/lib/services/network/test_security_groups_client.py
new file mode 100644
index 0000000..f96805f
--- /dev/null
+++ b/tempest/tests/lib/services/network/test_security_groups_client.py
@@ -0,0 +1,174 @@
+# Copyright 2017 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 copy
+
+import mock
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.services.network import base as network_base
+from tempest.lib.services.network import security_groups_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestSecurityGroupsClient(base.BaseServiceTest):
+
+ FAKE_SEC_GROUP_ID = "85cc3048-abc3-43cc-89b3-377341426ac5"
+
+ FAKE_SECURITY_GROUPS = {
+ "security_groups": [
+ {
+ "description": "default",
+ "id": FAKE_SEC_GROUP_ID,
+ "name": "fake-security-group-name",
+ "security_group_rules": [
+ {
+ "direction": "egress",
+ "ethertype": "IPv4",
+ "id": "38ce2d8e-e8f1-48bd-83c2-d33cb9f50c3d",
+ "port_range_max": None,
+ "port_range_min": None,
+ "protocol": None,
+ "remote_group_id": None,
+ "remote_ip_prefix": None,
+ "security_group_id": FAKE_SEC_GROUP_ID,
+ "project_id": "e4f50856753b4dc6afee5fa6b9b6c550",
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550",
+ "description": ""
+ },
+ {
+ "direction": "egress",
+ "ethertype": "IPv6",
+ "id": "565b9502-12de-4ffd-91e9-68885cff6ae1",
+ "port_range_max": None,
+ "port_range_min": None,
+ "protocol": None,
+ "remote_group_id": None,
+ "remote_ip_prefix": None,
+ "security_group_id": FAKE_SEC_GROUP_ID,
+ "project_id": "e4f50856753b4dc6afee5fa6b9b6c550",
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550",
+ "description": ""
+ }
+ ],
+ "project_id": "e4f50856753b4dc6afee5fa6b9b6c550",
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+ }
+ ]
+ }
+
+ FAKE_SECURITY_GROUP = {
+ "security_group": copy.deepcopy(
+ FAKE_SECURITY_GROUPS["security_groups"][0])
+ }
+
+ def setUp(self):
+ super(TestSecurityGroupsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = security_groups_client.SecurityGroupsClient(
+ fake_auth, 'network', 'regionOne')
+
+ def _test_list_security_groups(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_security_groups,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_SECURITY_GROUPS,
+ bytes_body,
+ mock_args='v2.0/security-groups')
+
+ def _test_create_security_group(self, bytes_body=False):
+ kwargs = {'name': 'fake-security-group-name'}
+ payload = json.dumps({"security_group": kwargs}, sort_keys=True)
+ json_dumps = json.dumps
+
+ # 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(network_base.json, 'dumps') as mock_dumps:
+ mock_dumps.side_effect = lambda d: json_dumps(d, sort_keys=True)
+
+ self.check_service_client_function(
+ self.client.create_security_group,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_SECURITY_GROUP,
+ bytes_body,
+ status=201,
+ mock_args=['v2.0/security-groups', payload],
+ **kwargs)
+
+ def _test_show_security_group(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_security_group,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_SECURITY_GROUP,
+ bytes_body,
+ security_group_id=self.FAKE_SEC_GROUP_ID,
+ mock_args='v2.0/security-groups/%s' % self.FAKE_SEC_GROUP_ID)
+
+ def _test_update_security_group(self, bytes_body=False):
+ kwargs = {'name': 'updated-security-group-name'}
+ resp_body = copy.deepcopy(self.FAKE_SECURITY_GROUP)
+ resp_body["security_group"]["name"] = 'updated-security-group-name'
+
+ payload = json.dumps({'security_group': kwargs}, sort_keys=True)
+ json_dumps = json.dumps
+
+ # 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(network_base.json, 'dumps') as mock_dumps:
+ mock_dumps.side_effect = lambda d: json_dumps(d, sort_keys=True)
+
+ self.check_service_client_function(
+ self.client.update_security_group,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ resp_body,
+ bytes_body,
+ security_group_id=self.FAKE_SEC_GROUP_ID,
+ mock_args=['v2.0/security-groups/%s' % self.FAKE_SEC_GROUP_ID,
+ payload],
+ **kwargs)
+
+ def test_list_security_groups_with_str_body(self):
+ self._test_list_security_groups()
+
+ def test_list_security_groups_with_bytes_body(self):
+ self._test_list_security_groups(bytes_body=True)
+
+ def test_create_security_group_with_str_body(self):
+ self._test_create_security_group()
+
+ def test_create_security_group_with_bytes_body(self):
+ self._test_create_security_group(bytes_body=True)
+
+ def test_show_security_group_with_str_body(self):
+ self._test_show_security_group()
+
+ def test_show_security_group_with_bytes_body(self):
+ self._test_show_security_group(bytes_body=True)
+
+ def test_update_security_group_with_str_body(self):
+ self._test_update_security_group()
+
+ def test_update_security_group_with_bytes_body(self):
+ self._test_update_security_group(bytes_body=True)
+
+ def test_delete_security_group(self):
+ self.check_service_client_function(
+ self.client.delete_security_group,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ status=204,
+ security_group_id=self.FAKE_SEC_GROUP_ID,
+ mock_args='v2.0/security-groups/%s' % self.FAKE_SEC_GROUP_ID)
diff --git a/tempest/tests/lib/services/network/test_service_providers_client.py b/tempest/tests/lib/services/network/test_service_providers_client.py
new file mode 100644
index 0000000..ae11ef0
--- /dev/null
+++ b/tempest/tests/lib/services/network/test_service_providers_client.py
@@ -0,0 +1,37 @@
+# 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.network import service_providers_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestServiceProvidersClient(base.BaseServiceTest):
+ def setUp(self):
+ super(TestServiceProvidersClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = service_providers_client.ServiceProvidersClient(
+ fake_auth, 'network', 'regionOne')
+
+ def _test_list_service_providers(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_service_providers,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {"service_providers": []},
+ bytes_body)
+
+ def test_list_service_providers_with_str_body(self):
+ self._test_list_service_providers()
+
+ def test_list_service_providers_with_bytes_body(self):
+ self._test_list_service_providers(bytes_body=True)
diff --git a/tempest/tests/lib/services/network/test_subnetpools_client.py b/tempest/tests/lib/services/network/test_subnetpools_client.py
new file mode 100644
index 0000000..3abb438
--- /dev/null
+++ b/tempest/tests/lib/services/network/test_subnetpools_client.py
@@ -0,0 +1,163 @@
+# Copyright 2017 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 copy
+
+from tempest.lib.services.network import subnetpools_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestSubnetsClient(base.BaseServiceTest):
+
+ FAKE_SUBNETPOOLS = {
+ "subnetpools": [
+ {
+ "min_prefixlen": "64",
+ "address_scope_id": None,
+ "default_prefixlen": "64",
+ "id": "03f761e6-eee0-43fc-a921-8acf64c14988",
+ "max_prefixlen": "64",
+ "name": "my-subnet-pool-ipv6",
+ "default_quota": None,
+ "is_default": False,
+ "project_id": "9fadcee8aa7c40cdb2114fff7d569c08",
+ "tenant_id": "9fadcee8aa7c40cdb2114fff7d569c08",
+ "prefixes": [
+ "2001:db8:0:2::/64",
+ "2001:db8::/63"
+ ],
+ "ip_version": 6,
+ "shared": False,
+ "description": "",
+ "revision_number": 2
+ },
+ {
+ "min_prefixlen": "24",
+ "address_scope_id": None,
+ "default_prefixlen": "25",
+ "id": "f49a1319-423a-4ee6-ba54-1d95a4f6cc68",
+ "max_prefixlen": "30",
+ "name": "my-subnet-pool-ipv4",
+ "default_quota": None,
+ "is_default": False,
+ "project_id": "9fadcee8aa7c40cdb2114fff7d569c08",
+ "tenant_id": "9fadcee8aa7c40cdb2114fff7d569c08",
+ "prefixes": [
+ "10.10.0.0/21",
+ "192.168.0.0/16"
+ ],
+ "ip_version": 4,
+ "shared": False,
+ "description": "",
+ "revision_number": 2
+ }
+ ]
+ }
+
+ FAKE_SUBNETPOOL_ID = "03f761e6-eee0-43fc-a921-8acf64c14988"
+
+ def setUp(self):
+ super(TestSubnetsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.subnetpools_client = subnetpools_client.SubnetpoolsClient(
+ fake_auth, 'compute', 'regionOne')
+
+ def _test_list_subnetpools(self, bytes_body=False):
+ self.check_service_client_function(
+ self.subnetpools_client.list_subnetpools,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_SUBNETPOOLS,
+ bytes_body,
+ 200)
+
+ def _test_create_subnetpool(self, bytes_body=False):
+ self.check_service_client_function(
+ self.subnetpools_client.create_subnetpool,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {'subnetpool': self.FAKE_SUBNETPOOLS['subnetpools'][1]},
+ bytes_body,
+ 201,
+ name="my-subnet-pool-ipv4",
+ prefixes=["192.168.0.0/16", "10.10.0.0/21"])
+
+ def _test_show_subnetpool(self, bytes_body=False):
+ self.check_service_client_function(
+ self.subnetpools_client.show_subnetpool,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {'subnetpool': self.FAKE_SUBNETPOOLS['subnetpools'][0]},
+ bytes_body,
+ 200,
+ subnetpool_id=self.FAKE_SUBNETPOOL_ID)
+
+ def _test_update_subnetpool(self, bytes_body=False):
+ update_kwargs = {
+ "name": "my-new-subnetpool-name",
+ "prefixes": [
+ "2001:db8::/64",
+ "2001:db8:0:1::/64",
+ "2001:db8:0:2::/64"
+ ],
+ "min_prefixlen": 64,
+ "default_prefixlen": 64,
+ "max_prefixlen": 64
+ }
+
+ resp_body = {
+ 'subnetpool': copy.deepcopy(
+ self.FAKE_SUBNETPOOLS['subnetpools'][0])
+ }
+ resp_body['subnetpool'].update(update_kwargs)
+
+ self.check_service_client_function(
+ self.subnetpools_client.update_subnetpool,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ resp_body,
+ bytes_body,
+ 200,
+ subnetpool_id=self.FAKE_SUBNETPOOL_ID,
+ **update_kwargs)
+
+ def test_list_subnetpools_with_str_body(self):
+ self._test_list_subnetpools()
+
+ def test_list_subnetpools_with_bytes_body(self):
+ self._test_list_subnetpools(bytes_body=True)
+
+ def test_create_subnetpool_with_str_body(self):
+ self._test_create_subnetpool()
+
+ def test_create_subnetpool_with_bytes_body(self):
+ self._test_create_subnetpool(bytes_body=True)
+
+ def test_show_subnetpool_with_str_body(self):
+ self._test_show_subnetpool()
+
+ def test_show_subnetpool_with_bytes_body(self):
+ self._test_show_subnetpool(bytes_body=True)
+
+ def test_update_subnet_with_str_body(self):
+ self._test_update_subnetpool()
+
+ def test_update_subnet_with_bytes_body(self):
+ self._test_update_subnetpool(bytes_body=True)
+
+ def test_delete_subnetpool(self):
+ self.check_service_client_function(
+ self.subnetpools_client.delete_subnetpool,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ status=204,
+ subnetpool_id=self.FAKE_SUBNETPOOL_ID)
diff --git a/tempest/tests/lib/services/network/test_subnets_client.py b/tempest/tests/lib/services/network/test_subnets_client.py
new file mode 100644
index 0000000..0aadf54
--- /dev/null
+++ b/tempest/tests/lib/services/network/test_subnets_client.py
@@ -0,0 +1,250 @@
+# Copyright 2017 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 copy
+
+from tempest.lib.services.network import subnets_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestSubnetsClient(base.BaseServiceTest):
+
+ FAKE_SUBNET = {
+ "subnet": {
+ "name": "",
+ "enable_dhcp": True,
+ "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+ "segment_id": None,
+ "project_id": "4fd44f30292945e481c7b8a0c8908869",
+ "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+ "dns_nameservers": [],
+ "allocation_pools": [
+ {
+ "start": "192.168.199.2",
+ "end": "192.168.199.254"
+ }
+ ],
+ "host_routes": [],
+ "ip_version": 4,
+ "gateway_ip": "192.168.199.1",
+ "cidr": "192.168.199.0/24",
+ "id": "3b80198d-4f7b-4f77-9ef5-774d54e17126",
+ "created_at": "2016-10-10T14:35:47Z",
+ "description": "",
+ "ipv6_address_mode": None,
+ "ipv6_ra_mode": None,
+ "revision_number": 2,
+ "service_types": [],
+ "subnetpool_id": None,
+ "updated_at": "2016-10-10T14:35:47Z"
+ }
+ }
+
+ FAKE_UPDATED_SUBNET = {
+ "subnet": {
+ "name": "my_subnet",
+ "enable_dhcp": True,
+ "network_id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+ "segment_id": None,
+ "project_id": "26a7980765d0414dbc1fc1f88cdb7e6e",
+ "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e",
+ "dns_nameservers": [],
+ "allocation_pools": [
+ {
+ "start": "10.0.0.2",
+ "end": "10.0.0.254"
+ }
+ ],
+ "host_routes": [],
+ "ip_version": 4,
+ "gateway_ip": "10.0.0.1",
+ "cidr": "10.0.0.0/24",
+ "id": "08eae331-0402-425a-923c-34f7cfe39c1b",
+ "description": ""
+ }
+ }
+
+ FAKE_SUBNETS = {
+ "subnets": [
+ {
+ "name": "private-subnet",
+ "enable_dhcp": True,
+ "network_id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+ "segment_id": None,
+ "project_id": "26a7980765d0414dbc1fc1f88cdb7e6e",
+ "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e",
+ "dns_nameservers": [],
+ "allocation_pools": [
+ {
+ "start": "10.0.0.2",
+ "end": "10.0.0.254"
+ }
+ ],
+ "host_routes": [],
+ "ip_version": 4,
+ "gateway_ip": "10.0.0.1",
+ "cidr": "10.0.0.0/24",
+ "id": "08eae331-0402-425a-923c-34f7cfe39c1b",
+ "created_at": "2016-10-10T14:35:34Z",
+ "description": "",
+ "ipv6_address_mode": None,
+ "ipv6_ra_mode": None,
+ "revision_number": 2,
+ "service_types": [],
+ "subnetpool_id": None,
+ "updated_at": "2016-10-10T14:35:34Z"
+ },
+ {
+ "name": "my_subnet",
+ "enable_dhcp": True,
+ "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+ "segment_id": None,
+ "project_id": "4fd44f30292945e481c7b8a0c8908869",
+ "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+ "dns_nameservers": [],
+ "allocation_pools": [
+ {
+ "start": "192.0.0.2",
+ "end": "192.255.255.254"
+ }
+ ],
+ "host_routes": [],
+ "ip_version": 4,
+ "gateway_ip": "192.0.0.1",
+ "cidr": "192.0.0.0/8",
+ "id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0b",
+ "created_at": "2016-10-10T14:35:47Z",
+ "description": "",
+ "ipv6_address_mode": None,
+ "ipv6_ra_mode": None,
+ "revision_number": 2,
+ "service_types": [],
+ "subnetpool_id": None,
+ "updated_at": "2016-10-10T14:35:47Z"
+ }
+ ]
+ }
+
+ FAKE_BULK_SUBNETS = copy.deepcopy(FAKE_SUBNETS)
+
+ FAKE_SUBNET_ID = "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+
+ FAKE_NETWORK_ID = "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+
+ def setUp(self):
+ super(TestSubnetsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.subnets_client = subnets_client.SubnetsClient(
+ fake_auth, 'compute', 'regionOne')
+
+ def _test_create_subnet(self, bytes_body=False):
+ self.check_service_client_function(
+ self.subnets_client.create_subnet,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_SUBNET,
+ bytes_body,
+ 201,
+ network_id=self.FAKE_NETWORK_ID,
+ ip_version=4,
+ cidr="192.168.199.0/24")
+
+ def _test_update_subnet(self, bytes_body=False):
+ self.check_service_client_function(
+ self.subnets_client.update_subnet,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ self.FAKE_UPDATED_SUBNET,
+ bytes_body,
+ 200,
+ subnet_id=self.FAKE_SUBNET_ID,
+ name="fake_updated_subnet_name")
+
+ def _test_show_subnet(self, bytes_body=False):
+ self.check_service_client_function(
+ self.subnets_client.show_subnet,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_SUBNET,
+ bytes_body,
+ 200,
+ subnet_id=self.FAKE_SUBNET_ID)
+
+ def _test_list_subnets(self, bytes_body=False):
+ self.check_service_client_function(
+ self.subnets_client.list_subnets,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_SUBNETS,
+ bytes_body,
+ 200)
+
+ def _test_create_bulk_subnets(self, bytes_body=False):
+ kwargs = {
+ "subnets": [
+ {
+ "cidr": "192.168.199.0/24",
+ "ip_version": 4,
+ "network_id": "e6031bc2-901a-4c66-82da-f4c32ed89406"
+ },
+ {
+ "cidr": "10.56.4.0/22",
+ "ip_version": 4,
+ "network_id": "64239a54-dcc4-4b39-920b-b37c2144effa"
+ }
+ ]
+ }
+ self.check_service_client_function(
+ self.subnets_client.create_bulk_subnets,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_SUBNETS,
+ bytes_body,
+ 201,
+ **kwargs)
+
+ def test_create_subnet_with_str_body(self):
+ self._test_create_subnet()
+
+ def test_create_subnet_with_bytes_body(self):
+ self._test_create_subnet(bytes_body=True)
+
+ def test_update_subnet_with_str_body(self):
+ self._test_update_subnet()
+
+ def test_update_subnet_with_bytes_body(self):
+ self._test_update_subnet(bytes_body=True)
+
+ def test_show_subnet_with_str_body(self):
+ self._test_show_subnet()
+
+ def test_show_subnet_with_bytes_body(self):
+ self._test_show_subnet(bytes_body=True)
+
+ def test_list_subnets_with_str_body(self):
+ self._test_list_subnets()
+
+ def test_list_subnets_with_bytes_body(self):
+ self._test_list_subnets(bytes_body=True)
+
+ def test_delete_subnet(self):
+ self.check_service_client_function(
+ self.subnets_client.delete_subnet,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ status=204,
+ subnet_id=self.FAKE_SUBNET_ID)
+
+ def test_create_bulk_subnets_with_str_body(self):
+ self._test_create_bulk_subnets()
+
+ def test_create_bulk_subnets_with_bytes_body(self):
+ self._test_create_bulk_subnets(bytes_body=True)
diff --git a/tempest/tests/lib/services/network/test_tags_client.py b/tempest/tests/lib/services/network/test_tags_client.py
new file mode 100644
index 0000000..dbe50a0
--- /dev/null
+++ b/tempest/tests/lib/services/network/test_tags_client.py
@@ -0,0 +1,123 @@
+# Copyright 2017 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 tempest.lib.services.network import tags_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestTagsClient(base.BaseServiceTest):
+
+ FAKE_TAGS = {
+ "tags": [
+ "red",
+ "blue"
+ ]
+ }
+
+ FAKE_RESOURCE_TYPE = 'network'
+
+ FAKE_RESOURCE_ID = '7a8f904b-c1ed-4446-a87d-60440c02934b'
+
+ def setUp(self):
+ super(TestTagsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = tags_client.TagsClient(
+ fake_auth, 'network', 'regionOne')
+
+ def _test_update_all_tags(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_all_tags,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ self.FAKE_TAGS,
+ bytes_body,
+ resource_type=self.FAKE_RESOURCE_TYPE,
+ resource_id=self.FAKE_RESOURCE_ID,
+ tags=self.FAKE_TAGS)
+
+ def _test_check_tag_existence(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.check_tag_existence,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {},
+ bytes_body,
+ resource_type=self.FAKE_RESOURCE_TYPE,
+ resource_id=self.FAKE_RESOURCE_ID,
+ tag=self.FAKE_TAGS['tags'][0],
+ status=204)
+
+ def _test_create_tag(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_tag,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ {},
+ bytes_body,
+ resource_type=self.FAKE_RESOURCE_TYPE,
+ resource_id=self.FAKE_RESOURCE_ID,
+ tag=self.FAKE_TAGS['tags'][0],
+ status=201)
+
+ def _test_list_tags(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_tags,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_TAGS,
+ bytes_body,
+ resource_type=self.FAKE_RESOURCE_TYPE,
+ resource_id=self.FAKE_RESOURCE_ID)
+
+ def test_update_all_tags_with_str_body(self):
+ self._test_update_all_tags()
+
+ def test_update_all_tags_with_bytes_body(self):
+ self._test_update_all_tags(bytes_body=True)
+
+ def test_delete_all_tags(self):
+ self.check_service_client_function(
+ self.client.delete_all_tags,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ resource_type=self.FAKE_RESOURCE_TYPE,
+ resource_id=self.FAKE_RESOURCE_ID,
+ status=204)
+
+ def test_check_tag_existence_with_str_body(self):
+ self._test_check_tag_existence()
+
+ def test_check_tag_existence_with_bytes_body(self):
+ self._test_check_tag_existence(bytes_body=True)
+
+ def test_create_tag_with_str_body(self):
+ self._test_create_tag()
+
+ def test_create_tag_with_bytes_body(self):
+ self._test_create_tag(bytes_body=True)
+
+ def test_list_tags_with_str_body(self):
+ self._test_list_tags()
+
+ def test_list_tags_with_bytes_body(self):
+ self._test_list_tags(bytes_body=True)
+
+ def test_delete_tag(self):
+ self.check_service_client_function(
+ self.client.delete_tag,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ resource_type=self.FAKE_RESOURCE_TYPE,
+ resource_id=self.FAKE_RESOURCE_ID,
+ tag=self.FAKE_TAGS['tags'][0],
+ status=204)
diff --git a/tempest/tests/lib/services/network/test_versions_client.py b/tempest/tests/lib/services/network/test_versions_client.py
new file mode 100644
index 0000000..026dc6d
--- /dev/null
+++ b/tempest/tests/lib/services/network/test_versions_client.py
@@ -0,0 +1,74 @@
+# Copyright 2016 VMware, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import copy
+
+from tempest.lib.services.network import versions_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestNetworkVersionsClient(base.BaseServiceTest):
+
+ FAKE_INIT_VERSION = {
+ "version": {
+ "id": "v2.0",
+ "links": [
+ {
+ "href": "http://openstack.example.com/v2.0/",
+ "rel": "self"
+ },
+ {
+ "href": "http://docs.openstack.org/",
+ "rel": "describedby",
+ "type": "text/html"
+ }
+ ],
+ "status": "CURRENT"
+ }
+ }
+
+ FAKE_VERSIONS_INFO = {
+ "versions": [FAKE_INIT_VERSION["version"]]
+ }
+
+ FAKE_VERSION_INFO = copy.deepcopy(FAKE_INIT_VERSION)
+
+ FAKE_VERSION_INFO["version"]["media-types"] = [
+ {
+ "base": "application/json",
+ "type": "application/vnd.openstack.network+json;version=2.0"
+ }
+ ]
+
+ def setUp(self):
+ super(TestNetworkVersionsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.versions_client = (
+ versions_client.NetworkVersionsClient
+ (fake_auth, 'compute', 'regionOne'))
+
+ def _test_versions_client(self, bytes_body=False):
+ self.check_service_client_function(
+ self.versions_client.list_versions,
+ 'tempest.lib.common.rest_client.RestClient.raw_request',
+ self.FAKE_VERSIONS_INFO,
+ bytes_body,
+ 200)
+
+ def test_list_versions_client_with_str_body(self):
+ self._test_versions_client()
+
+ def test_list_versions_client_with_bytes_body(self):
+ self._test_versions_client(bytes_body=True)
diff --git a/tempest/services/object_storage/__init__.py b/tempest/tests/lib/services/object_storage/__init__.py
similarity index 100%
rename from tempest/services/object_storage/__init__.py
rename to tempest/tests/lib/services/object_storage/__init__.py
diff --git a/tempest/tests/lib/services/object_storage/test_bulk_middleware_client.py b/tempest/tests/lib/services/object_storage/test_bulk_middleware_client.py
new file mode 100644
index 0000000..08028c3
--- /dev/null
+++ b/tempest/tests/lib/services/object_storage/test_bulk_middleware_client.py
@@ -0,0 +1,66 @@
+# Copyright 2017 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.services.object_storage import bulk_middleware_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestBulkMiddlewareClient(base.BaseServiceTest):
+
+ def setUp(self):
+ super(TestBulkMiddlewareClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = bulk_middleware_client.BulkMiddlewareClient(
+ fake_auth, 'object-storage', 'regionOne')
+
+ def test_upload_archive(self):
+ url = 'test_path?extract-archive=tar'
+ data = 'test_data'
+ self.check_service_client_function(
+ self.client.upload_archive,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ {},
+ mock_args=[url, data, {}],
+ resp_as_string=True,
+ upload_path='test_path', data=data, archive_file_format='tar')
+
+ def test_delete_bulk_data(self):
+ url = '?bulk-delete'
+ data = 'test_data'
+ self.check_service_client_function(
+ self.client.delete_bulk_data,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ mock_args=[url, {}, data],
+ resp_as_string=True,
+ data=data)
+
+ def _test_delete_bulk_data_with_post(self, status):
+ url = '?bulk-delete'
+ data = 'test_data'
+ self.check_service_client_function(
+ self.client.delete_bulk_data_with_post,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ mock_args=[url, data, {}],
+ resp_as_string=True,
+ status=status,
+ data=data)
+
+ def test_delete_bulk_data_with_post_200(self):
+ self._test_delete_bulk_data_with_post(200)
+
+ def test_delete_bulk_data_with_post_204(self):
+ self._test_delete_bulk_data_with_post(204)
diff --git a/tempest/tests/lib/services/object_storage/test_capabilities_client.py b/tempest/tests/lib/services/object_storage/test_capabilities_client.py
new file mode 100644
index 0000000..b7f972a
--- /dev/null
+++ b/tempest/tests/lib/services/object_storage/test_capabilities_client.py
@@ -0,0 +1,54 @@
+# Copyright 2016 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 tempest.lib.services.object_storage import capabilities_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestCapabilitiesClient(base.BaseServiceTest):
+
+ def setUp(self):
+ super(TestCapabilitiesClient, self).setUp()
+ self.fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.url = self.fake_auth.base_url(None)
+ self.client = capabilities_client.CapabilitiesClient(
+ self.fake_auth, 'swift', 'region1')
+
+ def _test_list_capabilities(self, bytes_body=False):
+ resp = {
+ "swift": {
+ "version": "1.11.0"
+ },
+ "slo": {
+ "max_manifest_segments": 1000,
+ "max_manifest_size": 2097152,
+ "min_segment_size": 1
+ },
+ "staticweb": {},
+ "tempurl": {}
+ }
+ self.check_service_client_function(
+ self.client.list_capabilities,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ resp,
+ bytes_body)
+
+ def test_list_capabilities_with_str_body(self):
+ self._test_list_capabilities()
+
+ def test_list_capabilities_with_bytes_body(self):
+ self._test_list_capabilities(True)
diff --git a/tempest/tests/lib/services/object_storage/test_object_client.py b/tempest/tests/lib/services/object_storage/test_object_client.py
new file mode 100644
index 0000000..a16d1d7
--- /dev/null
+++ b/tempest/tests/lib/services/object_storage/test_object_client.py
@@ -0,0 +1,108 @@
+# Copyright 2016 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.
+
+
+import mock
+
+from tempest.lib import exceptions
+from tempest.lib.services.object_storage import object_client
+from tempest.tests import base
+from tempest.tests.lib import fake_auth_provider
+
+
+class TestObjectClient(base.TestCase):
+
+ def setUp(self):
+ super(TestObjectClient, self).setUp()
+ self.fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.url = self.fake_auth.base_url(None)
+ self.object_client = object_client.ObjectClient(self.fake_auth,
+ 'swift', 'region1')
+
+ @mock.patch.object(object_client, '_create_connection')
+ def test_create_object_continue_no_data(self, mock_poc):
+ self._validate_create_object_continue(None, mock_poc)
+
+ @mock.patch.object(object_client, '_create_connection')
+ def test_create_object_continue_with_data(self, mock_poc):
+ self._validate_create_object_continue('hello', mock_poc)
+
+ @mock.patch.object(object_client, '_create_connection')
+ def test_create_continue_with_no_continue_received(self, mock_poc):
+ self._validate_create_object_continue('hello', mock_poc,
+ initial_status=201)
+
+ def _validate_create_object_continue(self, req_data,
+ mock_poc, initial_status=100):
+
+ expected_hdrs = {
+ 'X-Auth-Token': self.fake_auth.get_token(),
+ 'content-length': 0 if req_data is None else len(req_data),
+ 'Expect': '100-continue'}
+
+ # Setup the Mocks prior to invoking the object creation
+ mock_resp_cls = mock.Mock()
+ mock_resp_cls._read_status.return_value = ("1", initial_status, "OK")
+
+ mock_poc.return_value.response_class.return_value = mock_resp_cls
+
+ # This is the final expected return value
+ mock_poc.return_value.getresponse.return_value.status = 201
+ mock_poc.return_value.getresponse.return_value.reason = 'OK'
+
+ # Call method to PUT object using expect:100-continue
+ cnt = "container1"
+ obj = "object1"
+ path = "/%s/%s" % (cnt, obj)
+
+ # If the expected initial status is not 100, then an exception
+ # should be thrown and the connection closed
+ if initial_status is 100:
+ status, reason = \
+ self.object_client.create_object_continue(cnt, obj, req_data)
+ else:
+ self.assertRaises(exceptions.UnexpectedResponseCode,
+ self.object_client.create_object_continue, cnt,
+ obj, req_data)
+ mock_poc.return_value.close.assert_called_once_with()
+
+ # Verify that putrequest is called 1 time with the appropriate values
+ mock_poc.return_value.putrequest.assert_called_once_with('PUT', path)
+
+ # Verify that headers were written, including "Expect:100-continue"
+ calls = []
+
+ for header, value in expected_hdrs.items():
+ calls.append(mock.call(header, value))
+
+ mock_poc.return_value.putheader.assert_has_calls(calls, False)
+ mock_poc.return_value.endheaders.assert_called_once_with()
+
+ # The following steps are only taken if the initial status is 100
+ if initial_status is 100:
+ # Verify that the method returned what it was supposed to
+ self.assertEqual(status, 201)
+
+ # Verify that _safe_read was called once to remove the CRLF
+ # after the 100 response
+ mock_rc = mock_poc.return_value.response_class.return_value
+ mock_rc._safe_read.assert_called_once_with(2)
+
+ # Verify the actual data was written via send
+ mock_poc.return_value.send.assert_called_once_with(req_data)
+
+ # Verify that the getresponse method was called to receive
+ # the final
+ mock_poc.return_value.getresponse.assert_called_once_with()
diff --git a/tempest/tests/lib/services/registry_fixture.py b/tempest/tests/lib/services/registry_fixture.py
new file mode 100644
index 0000000..1da2112
--- /dev/null
+++ b/tempest/tests/lib/services/registry_fixture.py
@@ -0,0 +1,65 @@
+# Copyright 2017 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.
+
+import fixtures
+
+from tempest.lib.services import clients
+
+
+class RegistryFixture(fixtures.Fixture):
+ """A fixture to setup a test client registry
+
+ The clients registry is a singleton. In Tempest it's filled with
+ content from configuration. When testing Tempest lib classes without
+ configuration it's handy to have the registry setup to be able to access
+ service client factories.
+
+ This fixture sets up the registry using a fake plugin, which includes all
+ services specified at __init__ time. Any other plugin in the registry
+ is removed at setUp time. The fake plugin is removed from the registry
+ on cleanup.
+ """
+
+ PLUGIN_NAME = 'fake_plugin_for_test'
+
+ def __init__(self):
+ """Initialise the registry fixture"""
+ self.services = set(['compute', 'identity.v2', 'identity.v3',
+ 'image.v1', 'image.v2', 'network', 'volume.v1',
+ 'volume.v2', 'volume.v3', 'object-storage'])
+
+ def _setUp(self):
+ # Cleanup the registry
+ registry = clients.ClientsRegistry()
+ registry._service_clients = {}
+ # Prepare the clients for registration
+ all_clients = []
+ service_clients = clients.tempest_modules()
+ for sc in self.services:
+ sc_module = service_clients[sc]
+ sc_unversioned = sc.split('.')[0]
+ sc_name = sc.replace('.', '_').replace('-', '_')
+ # Pass the bare minimum params to satisfy the clients interface
+ service_client_data = dict(
+ name=sc_name, service_version=sc, service=sc_unversioned,
+ module_path=sc_module.__name__,
+ client_names=sc_module.__all__)
+ all_clients.append(service_client_data)
+ registry.register_service_client(self.PLUGIN_NAME, all_clients)
+
+ def _cleanup():
+ del registry._service_clients[self.PLUGIN_NAME]
+
+ self.addCleanup(_cleanup)
diff --git a/tempest/tests/lib/services/test_clients.py b/tempest/tests/lib/services/test_clients.py
new file mode 100644
index 0000000..43fd88f
--- /dev/null
+++ b/tempest/tests/lib/services/test_clients.py
@@ -0,0 +1,422 @@
+# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P.
+#
+# 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 types
+
+import fixtures
+import mock
+import six
+import testtools
+
+from tempest.lib import auth
+from tempest.lib import exceptions
+from tempest.lib.services import clients
+from tempest.tests import base
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib import fake_credentials
+
+
+has_attribute = testtools.matchers.MatchesPredicateWithParams(
+ lambda x, y: hasattr(x, y), '{0} does not have an attribute {1}')
+
+
+class TestClientsFactory(base.TestCase):
+
+ def setUp(self):
+ super(TestClientsFactory, self).setUp()
+ self.classes = []
+
+ def _setup_fake_module(self, class_names=None, extra_dict=None):
+ class_names = class_names or []
+ fake_module = types.ModuleType('fake_service_client')
+ _dict = {}
+ # Add fake classes to the fake module
+ for name in class_names:
+ _dict[name] = type(name, (object,), {})
+ # Store it for assertions
+ self.classes.append(_dict[name])
+ if extra_dict:
+ _dict[extra_dict] = extra_dict
+ fake_module.__dict__.update(_dict)
+ fixture_importlib = self.useFixture(fixtures.MockPatch(
+ 'importlib.import_module', return_value=fake_module))
+ return fixture_importlib.mock
+
+ def test___init___one_class(self):
+ fake_partial = 'fake_partial'
+ partial_mock = self.useFixture(fixtures.MockPatch(
+ 'tempest.lib.services.clients.ClientsFactory._get_partial_class',
+ return_value=fake_partial)).mock
+ class_names = ['FakeServiceClient1']
+ mock_importlib = self._setup_fake_module(class_names=class_names)
+ auth_provider = fake_auth_provider.FakeAuthProvider()
+ params = {'k1': 'v1', 'k2': 'v2'}
+ factory = clients.ClientsFactory('fake_path', class_names,
+ auth_provider, **params)
+ # Assert module has been imported
+ mock_importlib.assert_called_once_with('fake_path')
+ # All attributes have been created
+ for client in class_names:
+ self.assertThat(factory, has_attribute(client))
+ # Partial have been invoked correctly
+ partial_mock.assert_called_once_with(
+ self.classes[0], auth_provider, params)
+ # Get the clients
+ for name in class_names:
+ self.assertEqual(fake_partial, getattr(factory, name))
+
+ def test___init___two_classes(self):
+ fake_partial = 'fake_partial'
+ partial_mock = self.useFixture(fixtures.MockPatch(
+ 'tempest.lib.services.clients.ClientsFactory._get_partial_class',
+ return_value=fake_partial)).mock
+ class_names = ['FakeServiceClient1', 'FakeServiceClient2']
+ mock_importlib = self._setup_fake_module(class_names=class_names)
+ auth_provider = fake_auth_provider.FakeAuthProvider()
+ params = {'k1': 'v1', 'k2': 'v2'}
+ factory = clients.ClientsFactory('fake_path', class_names,
+ auth_provider, **params)
+ # Assert module has been imported
+ mock_importlib.assert_called_once_with('fake_path')
+ # All attributes have been created
+ for client in class_names:
+ self.assertThat(factory, has_attribute(client))
+ # Partial have been invoked the right number of times
+ partial_mock.call_count = len(class_names)
+ # Get the clients
+ for name in class_names:
+ self.assertEqual(fake_partial, getattr(factory, name))
+
+ def test___init___no_module(self):
+ auth_provider = fake_auth_provider.FakeAuthProvider()
+ class_names = ['FakeServiceClient1', 'FakeServiceClient2']
+ self.assertRaises(ImportError, clients.ClientsFactory,
+ 'fake_module', class_names, auth_provider)
+
+ def test___init___not_a_class(self):
+ class_names = ['FakeServiceClient1', 'FakeServiceClient2']
+ extended_class_names = class_names + ['not_really_a_class']
+ self._setup_fake_module(
+ class_names=class_names, extra_dict='not_really_a_class')
+ auth_provider = fake_auth_provider.FakeAuthProvider()
+ expected_msg = '.*not_really_a_class.*str.*'
+ with testtools.ExpectedException(TypeError, expected_msg):
+ clients.ClientsFactory('fake_module', extended_class_names,
+ auth_provider)
+
+ def test___init___class_not_found(self):
+ class_names = ['FakeServiceClient1', 'FakeServiceClient2']
+ extended_class_names = class_names + ['not_really_a_class']
+ self._setup_fake_module(class_names=class_names)
+ auth_provider = fake_auth_provider.FakeAuthProvider()
+ expected_msg = '.*not_really_a_class.*fake_service_client.*'
+ with testtools.ExpectedException(AttributeError, expected_msg):
+ clients.ClientsFactory('fake_module', extended_class_names,
+ auth_provider)
+
+ def test__get_partial_class_no_later_kwargs(self):
+ expected_fake_client = 'not_really_a_client'
+ self._setup_fake_module(class_names=[])
+ auth_provider = fake_auth_provider.FakeAuthProvider()
+ params = {'k1': 'v1', 'k2': 'v2'}
+ factory = clients.ClientsFactory(
+ 'fake_path', [], auth_provider, **params)
+ klass_mock = mock.Mock(return_value=expected_fake_client)
+ partial = factory._get_partial_class(klass_mock, auth_provider, params)
+ # Class has not be initialised yet
+ klass_mock.assert_not_called()
+ # Use partial and assert on parameters
+ client = partial()
+ self.assertEqual(expected_fake_client, client)
+ klass_mock.assert_called_once_with(auth_provider=auth_provider,
+ **params)
+
+ def test__get_partial_class_later_kwargs(self):
+ expected_fake_client = 'not_really_a_client'
+ self._setup_fake_module(class_names=[])
+ auth_provider = fake_auth_provider.FakeAuthProvider()
+ params = {'k1': 'v1', 'k2': 'v2'}
+ later_params = {'k2': 'v4', 'k3': 'v3'}
+ factory = clients.ClientsFactory(
+ 'fake_path', [], auth_provider, **params)
+ klass_mock = mock.Mock(return_value=expected_fake_client)
+ partial = factory._get_partial_class(klass_mock, auth_provider, params)
+ # Class has not be initialised yet
+ klass_mock.assert_not_called()
+ # Use partial and assert on parameters
+ client = partial(**later_params)
+ params.update(later_params)
+ self.assertEqual(expected_fake_client, client)
+ klass_mock.assert_called_once_with(auth_provider=auth_provider,
+ **params)
+
+ def test__get_partial_class_with_alias(self):
+ expected_fake_client = 'not_really_a_client'
+ client_alias = 'fake_client'
+ self._setup_fake_module(class_names=[])
+ auth_provider = fake_auth_provider.FakeAuthProvider()
+ params = {'k1': 'v1', 'k2': 'v2'}
+ later_params = {'k2': 'v4', 'k3': 'v3'}
+ factory = clients.ClientsFactory(
+ 'fake_path', [], auth_provider, **params)
+ klass_mock = mock.Mock(return_value=expected_fake_client)
+ partial = factory._get_partial_class(klass_mock, auth_provider, params)
+ # Class has not be initialised yet
+ klass_mock.assert_not_called()
+ # Use partial and assert on parameters
+ client = partial(alias=client_alias, **later_params)
+ params.update(later_params)
+ self.assertEqual(expected_fake_client, client)
+ klass_mock.assert_called_once_with(auth_provider=auth_provider,
+ **params)
+ self.assertThat(factory, has_attribute(client_alias))
+ self.assertEqual(expected_fake_client, getattr(factory, client_alias))
+
+
+class TestServiceClients(base.TestCase):
+
+ def setUp(self):
+ super(TestServiceClients, self).setUp()
+ self.useFixture(fixtures.MockPatch(
+ 'tempest.lib.services.clients.tempest_modules',
+ return_value=set(['fake_service1'])))
+
+ def test___init___creds_v2_uri(self):
+ # Verify that no API request is made, since no mock
+ # is required to run the test successfully
+ creds = fake_credentials.FakeKeystoneV2Credentials()
+ uri = 'fake_uri'
+ _manager = clients.ServiceClients(creds, identity_uri=uri)
+ self.assertIsInstance(_manager.auth_provider,
+ auth.KeystoneV2AuthProvider)
+
+ def test___init___creds_v3_uri(self):
+ # Verify that no API request is made, since no mock
+ # is required to run the test successfully
+ creds = fake_credentials.FakeKeystoneV3Credentials()
+ uri = 'fake_uri'
+ _manager = clients.ServiceClients(creds, identity_uri=uri)
+ self.assertIsInstance(_manager.auth_provider,
+ auth.KeystoneV3AuthProvider)
+
+ def test___init___base_creds_uri(self):
+ creds = fake_credentials.FakeCredentials()
+ uri = 'fake_uri'
+ with testtools.ExpectedException(exceptions.InvalidCredentials):
+ clients.ServiceClients(creds, identity_uri=uri)
+
+ def test___init___invalid_creds_uri(self):
+ creds = fake_credentials.FakeKeystoneV2Credentials()
+ delattr(creds, 'username')
+ uri = 'fake_uri'
+ with testtools.ExpectedException(exceptions.InvalidCredentials):
+ clients.ServiceClients(creds, identity_uri=uri)
+
+ def test___init___creds_uri_none(self):
+ creds = fake_credentials.FakeKeystoneV2Credentials()
+ msg = ("Invalid Credentials\nDetails: ServiceClients requires a "
+ "non-empty")
+ with testtools.ExpectedException(exceptions.InvalidCredentials,
+ value_re=msg):
+ clients.ServiceClients(creds, None)
+
+ def test___init___creds_uri_params(self):
+ creds = fake_credentials.FakeKeystoneV2Credentials()
+ expeted_params = {'fake_param1': 'fake_value1',
+ 'fake_param2': 'fake_value2'}
+ params = {'fake_service1': expeted_params}
+ uri = 'fake_uri'
+ _manager = clients.ServiceClients(creds, identity_uri=uri,
+ client_parameters=params)
+ self.assertIn('fake_service1', _manager.parameters)
+ for _key in expeted_params:
+ self.assertIn(_key, _manager.parameters['fake_service1'].keys())
+ self.assertEqual(expeted_params[_key],
+ _manager.parameters['fake_service1'].get(_key))
+
+ def test___init___creds_uri_params_unknown_services(self):
+ creds = fake_credentials.FakeKeystoneV2Credentials()
+ fake_params = {'fake_param1': 'fake_value1'}
+ params = {'unknown_service1': fake_params,
+ 'unknown_service2': fake_params}
+ uri = 'fake_uri'
+ msg = "(?=.*{0})(?=.*{1})".format(*list(params.keys()))
+ with testtools.ExpectedException(
+ exceptions.UnknownServiceClient, value_re=msg):
+ clients.ServiceClients(creds, identity_uri=uri,
+ client_parameters=params)
+
+ def test___init___plugin_service_clients_cannot_load(self):
+ creds = fake_credentials.FakeKeystoneV3Credentials()
+ uri = 'fake_uri'
+ fake_service_clients = {
+ 'service1': [{'name': 'client1',
+ 'service_version': 'client1.v1',
+ 'module_path': 'I cannot load this',
+ 'client_names': ['SomeClient1']}],
+ 'service2': [{'name': 'client2',
+ 'service_version': 'client2.v1',
+ 'module_path': 'This neither',
+ 'client_names': ['SomeClient1']}]}
+ msg = "(?=.*{0})(?=.*{1})".format(
+ *[x[1][0]['module_path'] for x in six.iteritems(
+ fake_service_clients)])
+ self.useFixture(fixtures.MockPatchObject(
+ clients.ClientsRegistry(), 'get_service_clients',
+ return_value=fake_service_clients))
+ with testtools.ExpectedException(
+ testtools.MultipleExceptions, value_re=msg):
+ clients.ServiceClients(creds, identity_uri=uri)
+
+ def test___init___plugin_service_clients_name_conflict(self):
+ creds = fake_credentials.FakeKeystoneV3Credentials()
+ uri = 'fake_uri'
+ fake_service_clients = {
+ 'serviceA': [{'name': 'client1',
+ 'service_version': 'client1.v1',
+ 'module_path': 'fake_path_1',
+ 'client_names': ['SomeClient1']}],
+ 'serviceB': [{'name': 'client1',
+ 'service_version': 'client1.v2',
+ 'module_path': 'fake_path_2',
+ 'client_names': ['SomeClient2']}],
+ 'serviceC': [{'name': 'client1',
+ 'service_version': 'client1.v1',
+ 'module_path': 'fake_path_2',
+ 'client_names': ['SomeClient1']}],
+ 'serviceD': [{'name': 'client1',
+ 'service_version': 'client1.v2',
+ 'module_path': 'fake_path_2',
+ 'client_names': ['SomeClient2']}]}
+ msg = "(?=.*{0})(?=.*{1})".format(
+ *[x[1][0]['service_version'] for x in six.iteritems(
+ fake_service_clients)])
+ self.useFixture(fixtures.MockPatchObject(
+ clients.ClientsRegistry(), 'get_service_clients',
+ return_value=fake_service_clients))
+ with testtools.ExpectedException(
+ testtools.MultipleExceptions, value_re=msg):
+ clients.ServiceClients(creds, identity_uri=uri)
+
+ def _get_manager(self, init_region='fake_region'):
+ # Get a manager to invoke _setup_parameters on
+ creds = fake_credentials.FakeKeystoneV2Credentials()
+ return clients.ServiceClients(creds, identity_uri='fake_uri',
+ region=init_region)
+
+ def test__setup_parameters_none_no_region(self):
+ kwargs = {}
+ _manager = self._get_manager(init_region=None)
+ _params = _manager._setup_parameters(kwargs)
+ self.assertNotIn('region', _params)
+
+ def test__setup_parameters_none(self):
+ kwargs = {}
+ _manager = self._get_manager()
+ _params = _manager._setup_parameters(kwargs)
+ self.assertIn('region', _params)
+ self.assertEqual('fake_region', _params['region'])
+
+ def test__setup_parameters_all(self):
+ expected_params = {'region': 'fake_region1',
+ 'catalog_type': 'fake_service2_mod',
+ 'fake_param1': 'fake_value1',
+ 'fake_param2': 'fake_value2'}
+ _manager = self._get_manager()
+ _params = _manager._setup_parameters(expected_params)
+ for _key in _params.keys():
+ self.assertEqual(expected_params[_key],
+ _params[_key])
+
+ def test_register_service_client_module(self):
+ expected_params = {'fake_param1': 'fake_value1',
+ 'fake_param2': 'fake_value2'}
+ _manager = self._get_manager(init_region='fake_region_default')
+ # Mock after the _manager is setup to preserve the call count
+ factory_mock = self.useFixture(fixtures.MockPatch(
+ 'tempest.lib.services.clients.ClientsFactory')).mock
+ _manager.register_service_client_module(
+ name='fake_module',
+ service_version='fake_service',
+ module_path='fake.path.to.module',
+ client_names=[],
+ **expected_params)
+ self.assertThat(_manager, has_attribute('fake_module'))
+ # Assert called once, without check for exact parameters
+ self.assertTrue(factory_mock.called)
+ self.assertEqual(1, factory_mock.call_count)
+ # Assert expected params are in with their values
+ actual_kwargs = factory_mock.call_args[1]
+ self.assertIn('region', actual_kwargs)
+ self.assertEqual('fake_region_default', actual_kwargs['region'])
+ for param in expected_params:
+ self.assertIn(param, actual_kwargs)
+ self.assertEqual(expected_params[param], actual_kwargs[param])
+ # Assert the new service is registered
+ self.assertIn('fake_service', _manager._registered_services)
+
+ def test_register_service_client_module_override_default(self):
+ new_region = 'new_region'
+ expected_params = {'fake_param1': 'fake_value1',
+ 'fake_param2': 'fake_value2',
+ 'region': new_region}
+ _manager = self._get_manager(init_region='fake_region_default')
+ # Mock after the _manager is setup to preserve the call count
+ factory_mock = self.useFixture(fixtures.MockPatch(
+ 'tempest.lib.services.clients.ClientsFactory')).mock
+ _manager.register_service_client_module(
+ name='fake_module',
+ service_version='fake_service',
+ module_path='fake.path.to.module',
+ client_names=[],
+ **expected_params)
+ self.assertThat(_manager, has_attribute('fake_module'))
+ # Assert called once, without check for exact parameters
+ self.assertTrue(factory_mock.called)
+ self.assertEqual(1, factory_mock.call_count)
+ # Assert expected params are in with their values
+ actual_kwargs = factory_mock.call_args[1]
+ self.assertIn('region', actual_kwargs)
+ self.assertEqual(new_region, actual_kwargs['region'])
+ for param in expected_params:
+ self.assertIn(param, actual_kwargs)
+ self.assertEqual(expected_params[param], actual_kwargs[param])
+ # Assert the new service is registered
+ self.assertIn('fake_service', _manager._registered_services)
+
+ def test_register_service_client_module_duplicate_name(self):
+ self.useFixture(fixtures.MockPatch(
+ 'tempest.lib.services.clients.ClientsFactory')).mock
+ _manager = self._get_manager()
+ name_owner = 'this_is_a_string'
+ setattr(_manager, 'fake_module', name_owner)
+ expected_error = '.*' + name_owner
+ with testtools.ExpectedException(
+ exceptions.ServiceClientRegistrationException, expected_error):
+ _manager.register_service_client_module(
+ name='fake_module', module_path='fake.path.to.module',
+ service_version='fake_service', client_names=[])
+
+ def test_register_service_client_module_duplicate_service(self):
+ self.useFixture(fixtures.MockPatch(
+ 'tempest.lib.services.clients.ClientsFactory')).mock
+ _manager = self._get_manager()
+ duplicate_service = 'fake_service1'
+ expected_error = '.*' + duplicate_service
+ _manager._registered_services = [duplicate_service]
+ with testtools.ExpectedException(
+ exceptions.ServiceClientRegistrationException, expected_error):
+ _manager.register_service_client_module(
+ name='fake_module', module_path='fake.path.to.module',
+ service_version=duplicate_service, client_names=[])
diff --git a/tempest/services/volume/__init__.py b/tempest/tests/lib/services/volume/__init__.py
similarity index 100%
rename from tempest/services/volume/__init__.py
rename to tempest/tests/lib/services/volume/__init__.py
diff --git a/tempest/services/volume/v1/__init__.py b/tempest/tests/lib/services/volume/v1/__init__.py
similarity index 100%
rename from tempest/services/volume/v1/__init__.py
rename to tempest/tests/lib/services/volume/v1/__init__.py
diff --git a/tempest/tests/lib/services/volume/v1/test_encryption_types_client.py b/tempest/tests/lib/services/volume/v1/test_encryption_types_client.py
new file mode 100644
index 0000000..585904e
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v1/test_encryption_types_client.py
@@ -0,0 +1,86 @@
+# Copyright 2016 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.services.volume.v1 import encryption_types_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestEncryptionTypesClient(base.BaseServiceTest):
+ FAKE_CREATE_ENCRYPTION_TYPE = {
+ "encryption": {
+ "id": "cbc36478b0bd8e67e89",
+ "name": "FakeEncryptionType",
+ "type": "fakeType",
+ "provider": "LuksEncryptor",
+ "cipher": "aes-xts-plain64",
+ "key_size": "512",
+ "control_location": "front-end"
+ }
+ }
+
+ FAKE_INFO_ENCRYPTION_TYPE = {
+ "encryption": {
+ "name": "FakeEncryptionType",
+ "type": "fakeType",
+ "description": "test_description",
+ "volume_type": "fakeType",
+ "provider": "LuksEncryptor",
+ "cipher": "aes-xts-plain64",
+ "key_size": "512",
+ "control_location": "front-end"
+ }
+ }
+
+ def setUp(self):
+ super(TestEncryptionTypesClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = encryption_types_client.EncryptionTypesClient(fake_auth,
+ 'volume',
+ 'regionOne'
+ )
+
+ def _test_create_encryption(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_encryption_type,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_ENCRYPTION_TYPE,
+ bytes_body, volume_type_id="cbc36478b0bd8e67e89")
+
+ def _test_show_encryption_type(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_encryption_type,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_INFO_ENCRYPTION_TYPE,
+ bytes_body, volume_type_id="cbc36478b0bd8e67e89")
+
+ def test_create_encryption_type_with_str_body(self):
+ self._test_create_encryption()
+
+ def test_create_encryption_type_with_bytes_body(self):
+ self._test_create_encryption(bytes_body=True)
+
+ def test_show_encryption_type_with_str_body(self):
+ self._test_show_encryption_type()
+
+ def test_show_encryption_type_with_bytes_body(self):
+ self._test_show_encryption_type(bytes_body=True)
+
+ def test_delete_encryption_type(self):
+ self.check_service_client_function(
+ self.client.delete_encryption_type,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ volume_type_id="cbc36478b0bd8e67e89",
+ status=202)
diff --git a/tempest/tests/lib/services/volume/v1/test_quotas_client.py b/tempest/tests/lib/services/volume/v1/test_quotas_client.py
new file mode 100644
index 0000000..f9e76af
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v1/test_quotas_client.py
@@ -0,0 +1,96 @@
+# Copyright 2016 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.services.volume.v1 import quotas_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestQuotasClient(base.BaseServiceTest):
+ FAKE_QUOTAS = {
+ "quota_set": {
+ "cores": 20,
+ "fixed_ips": -1,
+ "floating_ips": 10,
+ "id": "fake_tenant",
+ "injected_file_content_bytes": 10240,
+ "injected_file_path_bytes": 255,
+ "injected_files": 5,
+ "instances": 10,
+ "key_pairs": 100,
+ "metadata_items": 128,
+ "ram": 51200,
+ "security_group_rules": 20,
+ "security_groups": 10
+ }
+ }
+
+ FAKE_UPDATE_QUOTAS_REQUEST = {
+ "quota_set": {
+ "security_groups": 45
+ }
+ }
+
+ def setUp(self):
+ super(TestQuotasClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = quotas_client.QuotasClient(fake_auth,
+ 'volume',
+ 'regionOne')
+
+ def _test_show_default_quota_set(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_default_quota_set,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_QUOTAS,
+ bytes_body, tenant_id="fake_tenant")
+
+ def _test_show_quota_set(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_quota_set,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_QUOTAS,
+ bytes_body, tenant_id="fake_tenant")
+
+ def _test_update_quota_set(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_quota_set,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ self.FAKE_UPDATE_QUOTAS_REQUEST,
+ bytes_body, tenant_id="fake_tenant")
+
+ def test_show_default_quota_set_with_str_body(self):
+ self._test_show_default_quota_set()
+
+ def test_show_default_quota_set_with_bytes_body(self):
+ self._test_show_default_quota_set(bytes_body=True)
+
+ def test_show_quota_set_with_str_body(self):
+ self._test_show_quota_set()
+
+ def test_show_quota_set_with_bytes_body(self):
+ self._test_show_quota_set(bytes_body=True)
+
+ def test_update_quota_set_with_str_body(self):
+ self._test_update_quota_set()
+
+ def test_update_quota_set_with_bytes_body(self):
+ self._test_update_quota_set(bytes_body=True)
+
+ def test_delete_quota_set(self):
+ self.check_service_client_function(
+ self.client.delete_quota_set,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ tenant_id="fake_tenant")
diff --git a/tempest/tests/lib/services/volume/v1/test_snapshots_client.py b/tempest/tests/lib/services/volume/v1/test_snapshots_client.py
new file mode 100644
index 0000000..49191e3
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v1/test_snapshots_client.py
@@ -0,0 +1,200 @@
+# Copyright 2016 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.services.volume.v1 import snapshots_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestSnapshotsClient(base.BaseServiceTest):
+ FAKE_CREATE_SNAPSHOT = {
+ "snapshot": {
+ "display_name": "snap-001",
+ "display_description": "Daily backup",
+ "volume_id": "521752a6-acf6-4b2d-bc7a-119f9148cd8c",
+ "force": True
+ }
+ }
+
+ FAKE_UPDATE_SNAPSHOT_REQUEST = {
+ "metadata": {
+ "key": "v1"
+ }
+ }
+
+ FAKE_INFO_SNAPSHOT = {
+ "snapshot": {
+ "id": "3fbbcccf-d058-4502-8844-6feeffdf4cb5",
+ "display_name": "snap-001",
+ "display_description": "Daily backup",
+ "volume_id": "521752a6-acf6-4b2d-bc7a-119f9148cd8c",
+ "status": "available",
+ "size": 30,
+ "created_at": "2012-02-29T03:50:07Z"
+ }
+ }
+
+ FAKE_LIST_SNAPSHOTS = {
+ "snapshots": [
+ {
+ "id": "3fbbcccf-d058-4502-8844-6feeffdf4cb5",
+ "display_name": "snap-001",
+ "display_description": "Daily backup",
+ "volume_id": "521752a6-acf6-4b2d-bc7a-119f9148cd8c",
+ "status": "available",
+ "size": 30,
+ "created_at": "2012-02-29T03:50:07Z",
+ "metadata": {
+ "contents": "junk"
+ }
+ },
+ {
+ "id": "e479997c-650b-40a4-9dfe-77655818b0d2",
+ "display_name": "snap-002",
+ "display_description": "Weekly backup",
+ "volume_id": "76b8950a-8594-4e5b-8dce-0dfa9c696358",
+ "status": "available",
+ "size": 25,
+ "created_at": "2012-03-19T01:52:47Z",
+ "metadata": {}
+ }
+ ]
+ }
+
+ def setUp(self):
+ super(TestSnapshotsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = snapshots_client.SnapshotsClient(fake_auth,
+ 'volume',
+ 'regionOne')
+
+ def _test_create_snapshot(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_snapshot,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_SNAPSHOT,
+ bytes_body)
+
+ def _test_show_snapshot(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_snapshot,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_INFO_SNAPSHOT,
+ bytes_body,
+ snapshot_id="3fbbcccf-d058-4502-8844-6feeffdf4cb5")
+
+ def _test_list_snapshots(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_snapshots,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_SNAPSHOTS,
+ bytes_body,
+ detail=True)
+
+ def _test_create_snapshot_metadata(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_snapshot_metadata,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_INFO_SNAPSHOT,
+ bytes_body,
+ snapshot_id="3fbbcccf-d058-4502-8844-6feeffdf4cb5",
+ metadata={"key": "v1"})
+
+ def _test_update_snapshot(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_snapshot,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ self.FAKE_UPDATE_SNAPSHOT_REQUEST,
+ bytes_body,
+ snapshot_id="3fbbcccf-d058-4502-8844-6feeffdf4cb5")
+
+ def _test_show_snapshot_metadata(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_snapshot_metadata,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_UPDATE_SNAPSHOT_REQUEST,
+ bytes_body,
+ snapshot_id="3fbbcccf-d058-4502-8844-6feeffdf4cb5")
+
+ def _test_update_snapshot_metadata(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_snapshot_metadata,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ self.FAKE_UPDATE_SNAPSHOT_REQUEST,
+ bytes_body, snapshot_id="cbc36478b0bd8e67e89")
+
+ def _test_update_snapshot_metadata_item(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_snapshot_metadata_item,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ self.FAKE_INFO_SNAPSHOT,
+ bytes_body, volume_type_id="cbc36478b0bd8e67e89")
+
+ def test_create_snapshot_with_str_body(self):
+ self._test_create_snapshot()
+
+ def test_create_snapshot_with_bytes_body(self):
+ self._test_create_snapshot(bytes_body=True)
+
+ def test_show_snapshot_with_str_body(self):
+ self._test_show_snapshot()
+
+ def test_show_snapshot_with_bytes_body(self):
+ self._test_show_snapshot(bytes_body=True)
+
+ def test_list_snapshots_with_str_body(self):
+ self._test_list_snapshots()
+
+ def test_list_snapshots_with_bytes_body(self):
+ self._test_list_snapshots(bytes_body=True)
+
+ def test_create_snapshot_metadata_with_str_body(self):
+ self._test_create_snapshot_metadata()
+
+ def test_create_snapshot_metadata_with_bytes_body(self):
+ self._test_create_snapshot_metadata(bytes_body=True)
+
+ def test_update_snapshot_with_str_body(self):
+ self._test_update_snapshot()
+
+ def test_update_snapshot_with_bytes_body(self):
+ self._test_update_snapshot(bytes_body=True)
+
+ def test_show_snapshot_metadata_with_str_body(self):
+ self._test_show_snapshot_metadata()
+
+ def test_show_snapshot_metadata_with_bytes_body(self):
+ self._test_show_snapshot_metadata(bytes_body=True)
+
+ def test_update_snapshot_metadata_with_str_body(self):
+ self._test_update_snapshot_metadata()
+
+ def test_update_snapshot_metadata_with_bytes_body(self):
+ self._test_update_snapshot_metadata(bytes_body=True)
+
+ def test_force_delete_snapshot(self):
+ self.check_service_client_function(
+ self.client.force_delete_snapshot,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ snapshot_id="521752a6-acf6-4b2d-bc7a-119f9148cd8c",
+ status=202)
+
+ def test_delete_snapshot(self):
+ self.check_service_client_function(
+ self.client.delete_snapshot,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ snapshot_id="521752a6-acf6-4b2d-bc7a-119f9148cd8c",
+ status=202)
diff --git a/tempest/services/volume/v2/__init__.py b/tempest/tests/lib/services/volume/v2/__init__.py
similarity index 100%
rename from tempest/services/volume/v2/__init__.py
rename to tempest/tests/lib/services/volume/v2/__init__.py
diff --git a/tempest/tests/lib/services/volume/v2/test_availability_zone_client.py b/tempest/tests/lib/services/volume/v2/test_availability_zone_client.py
new file mode 100644
index 0000000..770565c
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v2/test_availability_zone_client.py
@@ -0,0 +1,51 @@
+# 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 tempest.lib.services.volume.v2 import availability_zone_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestAvailabilityZoneClient(base.BaseServiceTest):
+
+ FAKE_AZ_LIST = {
+ "availabilityZoneInfo": [
+ {
+ "zoneState": {
+ "available": True
+ },
+ "zoneName": "nova"
+ }
+ ]
+ }
+
+ def setUp(self):
+ super(TestAvailabilityZoneClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = availability_zone_client.AvailabilityZoneClient(
+ fake_auth, 'volume', 'regionOne')
+
+ def _test_list_availability_zones(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_availability_zones,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_AZ_LIST,
+ bytes_body)
+
+ def test_list_availability_zones_with_str_body(self):
+ self._test_list_availability_zones()
+
+ def test_list_availability_zones_with_bytes_body(self):
+ self._test_list_availability_zones(bytes_body=True)
diff --git a/tempest/tests/lib/services/volume/v2/test_backups_client.py b/tempest/tests/lib/services/volume/v2/test_backups_client.py
new file mode 100644
index 0000000..14e5fb0
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v2/test_backups_client.py
@@ -0,0 +1,117 @@
+# 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 tempest.lib.services.volume.v2 import backups_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestBackupsClient(base.BaseServiceTest):
+
+ FAKE_BACKUP_LIST = {
+ "backups": [
+ {
+ "id": "2ef47aee-8844-490c-804d-2a8efe561c65",
+ "links": [
+ {
+ "href": "fake-url-1",
+ "rel": "self"
+ },
+ {
+ "href": "fake-url-2",
+ "rel": "bookmark"
+ }
+ ],
+ "name": "backup001"
+ }
+ ]
+ }
+
+ FAKE_BACKUP_LIST_WITH_DETAIL = {
+ "backups": [
+ {
+ "availability_zone": "az1",
+ "container": "volumebackups",
+ "created_at": "2013-04-02T10:35:27.000000",
+ "description": None,
+ "fail_reason": None,
+ "id": "2ef47aee-8844-490c-804d-2a8efe561c65",
+ "links": [
+ {
+ "href": "fake-url-1",
+ "rel": "self"
+ },
+ {
+ "href": "fake-url-2",
+ "rel": "bookmark"
+ }
+ ],
+ "name": "backup001",
+ "object_count": 22,
+ "size": 1,
+ "status": "available",
+ "volume_id": "e5185058-943a-4cb4-96d9-72c184c337d6",
+ "is_incremental": True,
+ "has_dependent_backups": False
+ }
+ ]
+ }
+
+ def setUp(self):
+ super(TestBackupsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = backups_client.BackupsClient(fake_auth,
+ 'volume',
+ 'regionOne')
+
+ def _test_list_backups(self, detail=False, mock_args='backups',
+ bytes_body=False, **params):
+ if detail:
+ resp_body = self.FAKE_BACKUP_LIST_WITH_DETAIL
+ else:
+ resp_body = self.FAKE_BACKUP_LIST
+ self.check_service_client_function(
+ self.client.list_backups,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ resp_body,
+ to_utf=bytes_body,
+ mock_args=[mock_args],
+ detail=detail,
+ **params)
+
+ def test_list_backups_with_str_body(self):
+ self._test_list_backups()
+
+ def test_list_backups_with_bytes_body(self):
+ self._test_list_backups(bytes_body=True)
+
+ def test_list_backups_with_detail_with_str_body(self):
+ mock_args = "backups/detail"
+ self._test_list_backups(detail=True, mock_args=mock_args)
+
+ def test_list_backups_with_detail_with_bytes_body(self):
+ mock_args = "backups/detail"
+ self._test_list_backups(detail=True, mock_args=mock_args,
+ bytes_body=True)
+
+ def test_list_backups_with_params(self):
+ # Run the test separately for each param, to avoid assertion error
+ # resulting from randomized params order.
+ mock_args = 'backups?sort_key=name'
+ self._test_list_backups(mock_args=mock_args, sort_key='name')
+
+ mock_args = 'backups/detail?limit=10'
+ self._test_list_backups(detail=True, mock_args=mock_args,
+ bytes_body=True, limit=10)
diff --git a/tempest/tests/lib/services/volume/v2/test_capabilities_client.py b/tempest/tests/lib/services/volume/v2/test_capabilities_client.py
new file mode 100644
index 0000000..3d3f1e1
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v2/test_capabilities_client.py
@@ -0,0 +1,77 @@
+# Copyright 2017 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 tempest.lib.services.volume.v2 import capabilities_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestCapabilitiesClient(base.BaseServiceTest):
+
+ FAKE_BACKEND_CAPABILITIES = {
+ "namespace": "OS::Storage::Capabilities::fake",
+ "vendor_name": "OpenStack",
+ "volume_backend_name": "lvmdriver-1",
+ "pool_name": "pool",
+ "driver_version": "2.0.0",
+ "storage_protocol": "iSCSI",
+ "display_name": "Capabilities of Cinder LVM driver",
+ "description": (
+ "These are volume type options provided by Cinder LVM driver."),
+ "visibility": "public",
+ "replication_targets": [],
+ "properties": {
+ "compression": {
+ "title": "Compression",
+ "description": "Enables compression.",
+ "type": "boolean"
+ },
+ "qos": {
+ "title": "QoS",
+ "description": "Enables QoS.",
+ "type": "boolean"
+ },
+ "replication": {
+ "title": "Replication",
+ "description": "Enables replication.",
+ "type": "boolean"
+ },
+ "thin_provisioning": {
+ "title": "Thin Provisioning",
+ "description": "Sets thin provisioning.",
+ "type": "boolean"
+ }
+ }
+ }
+
+ def setUp(self):
+ super(TestCapabilitiesClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = capabilities_client.CapabilitiesClient(
+ fake_auth, 'volume', 'regionOne')
+
+ def _test_show_backend_capabilities(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_backend_capabilities,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_BACKEND_CAPABILITIES,
+ bytes_body,
+ host='lvmdriver-1')
+
+ def test_show_backend_capabilities_with_str_body(self):
+ self._test_show_backend_capabilities()
+
+ def test_show_backend_capabilities_with_bytes_body(self):
+ self._test_show_backend_capabilities(bytes_body=True)
diff --git a/tempest/tests/lib/services/volume/v2/test_encryption_types_client.py b/tempest/tests/lib/services/volume/v2/test_encryption_types_client.py
new file mode 100644
index 0000000..8de9fb4
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v2/test_encryption_types_client.py
@@ -0,0 +1,103 @@
+# Copyright 2016 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.services.volume.v2 import encryption_types_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestEncryptionTypesClient(base.BaseServiceTest):
+ FAKE_CREATE_ENCRYPTION_TYPE = {
+ "encryption": {
+ "id": "cbc36478b0bd8e67e89",
+ "name": "FakeEncryptionType",
+ "type": "fakeType",
+ "provider": "LuksEncryptor",
+ "cipher": "aes-xts-plain64",
+ "key_size": "512",
+ "control_location": "front-end"
+ }
+ }
+
+ FAKE_INFO_ENCRYPTION_TYPE = {
+ "encryption": {
+ "name": "FakeEncryptionType",
+ "type": "fakeType",
+ "description": "test_description",
+ "volume_type": "fakeType",
+ "provider": "LuksEncryptor",
+ "cipher": "aes-xts-plain64",
+ "key_size": "512",
+ "control_location": "front-end"
+ }
+ }
+
+ FAKE_ENCRYPTION_SPECS_ITEM = {
+ "cipher": "aes-xts-plain64"
+ }
+
+ def setUp(self):
+ super(TestEncryptionTypesClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = encryption_types_client.EncryptionTypesClient(fake_auth,
+ 'volume',
+ 'regionOne'
+ )
+
+ def _test_create_encryption(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_encryption_type,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_ENCRYPTION_TYPE,
+ bytes_body, volume_type_id="cbc36478b0bd8e67e89")
+
+ def _test_show_encryption_type(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_encryption_type,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_INFO_ENCRYPTION_TYPE,
+ bytes_body, volume_type_id="cbc36478b0bd8e67e89")
+
+ def _test_show_encryption_specs_item(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_encryption_specs_item,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_ENCRYPTION_SPECS_ITEM,
+ bytes_body, volume_type_id="cbc36478b0bd8e67e89", key="cipher")
+
+ def test_create_encryption_type_with_str_body(self):
+ self._test_create_encryption()
+
+ def test_create_encryption_type_with_bytes_body(self):
+ self._test_create_encryption(bytes_body=True)
+
+ def test_show_encryption_type_with_str_body(self):
+ self._test_show_encryption_type()
+
+ def test_show_encryption_type_with_bytes_body(self):
+ self._test_show_encryption_type(bytes_body=True)
+
+ def test_show_encryption_specs_item_with_str_body(self):
+ self._test_show_encryption_specs_item()
+
+ def test_show_encryption_specs_item_with_bytes_body(self):
+ self._test_show_encryption_specs_item(bytes_body=True)
+
+ def test_delete_encryption_type(self):
+ self.check_service_client_function(
+ self.client.delete_encryption_type,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ volume_type_id="cbc36478b0bd8e67e89",
+ status=202)
diff --git a/tempest/tests/lib/services/volume/v2/test_extensions_client.py b/tempest/tests/lib/services/volume/v2/test_extensions_client.py
new file mode 100644
index 0000000..c0ee421
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v2/test_extensions_client.py
@@ -0,0 +1,70 @@
+# 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 tempest.lib.services.volume.v2 import extensions_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestExtensionsClient(base.BaseServiceTest):
+
+ FAKE_EXTENSION_LIST = {
+ "extensions": [
+ {
+ "updated": "2012-03-12T00:00:00+00:00",
+ "name": "QuotaClasses",
+ "links": [],
+ "namespace": "fake-namespace-1",
+ "alias": "os-quota-class-sets",
+ "description": "Quota classes management support."
+ },
+ {
+ "updated": "2013-05-29T00:00:00+00:00",
+ "name": "VolumeTransfer",
+ "links": [],
+ "namespace": "fake-namespace-2",
+ "alias": "os-volume-transfer",
+ "description": "Volume transfer management support."
+ },
+ {
+ "updated": "2014-02-10T00:00:00+00:00",
+ "name": "VolumeManage",
+ "links": [],
+ "namespace": "fake-namespace-3",
+ "alias": "os-volume-manage",
+ "description": "Manage existing backend storage by Cinder."
+ }
+ ]
+ }
+
+ def setUp(self):
+ super(TestExtensionsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = extensions_client.ExtensionsClient(fake_auth,
+ 'volume',
+ 'regionOne')
+
+ def _test_list_extensions(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_extensions,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_EXTENSION_LIST,
+ bytes_body)
+
+ def test_list_extensions_with_str_body(self):
+ self._test_list_extensions()
+
+ def test_list_extensions_with_bytes_body(self):
+ self._test_list_extensions(bytes_body=True)
diff --git a/tempest/tests/lib/services/volume/v2/test_hosts_client.py b/tempest/tests/lib/services/volume/v2/test_hosts_client.py
new file mode 100644
index 0000000..e107910
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v2/test_hosts_client.py
@@ -0,0 +1,97 @@
+# 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 tempest.lib.services.volume.v2 import hosts_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestQuotasClient(base.BaseServiceTest):
+ FAKE_LIST_HOSTS = {
+ "hosts": [
+ {
+ "service-status": "available",
+ "service": "cinder-scheduler",
+ "zone": "nova",
+ "service-state": "enabled",
+ "host_name": "fake-host",
+ "last-update": "2017-04-12T04:26:03.000000"
+ },
+ {
+ "service-status": "available",
+ "service": "cinder-volume",
+ "zone": "nova",
+ "service-state": "enabled",
+ "host_name": "fake-host@rbd",
+ "last-update": "2017-04-12T04:26:07.000000"
+ }
+ ]
+ }
+
+ FAKE_HOST_INFO = {
+ "host": [
+ {
+ "resource": {
+ "volume_count": "2",
+ "total_volume_gb": "2",
+ "total_snapshot_gb": "0",
+ "project": "(total)",
+ "host": "fake-host",
+ "snapshot_count": "0"
+ }
+ },
+ {
+ "resource": {
+ "volume_count": "2",
+ "total_volume_gb": "2",
+ "total_snapshot_gb": "0",
+ "project": "f21a9c86d7114bf99c711f4874d80474",
+ "host": "fake-host",
+ "snapshot_count": "0"
+ }
+ }
+ ]
+ }
+
+ def setUp(self):
+ super(TestQuotasClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = hosts_client.HostsClient(fake_auth,
+ 'volume',
+ 'regionOne')
+
+ def _test_list_hosts(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_hosts,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_HOSTS, bytes_body)
+
+ def _test_show_host(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_host,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_HOST_INFO, bytes_body, host_name='fake-host')
+
+ def test_list_hosts_with_str_body(self):
+ self._test_list_hosts()
+
+ def test_list_hosts_with_bytes_body(self):
+ self._test_list_hosts(bytes_body=True)
+
+ def test_show_host_with_str_body(self):
+ self._test_show_host()
+
+ def test_show_host_with_bytes_body(self):
+ self._test_show_host(bytes_body=True)
diff --git a/tempest/tests/lib/services/volume/v2/test_limits_client.py b/tempest/tests/lib/services/volume/v2/test_limits_client.py
new file mode 100644
index 0000000..202054c
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v2/test_limits_client.py
@@ -0,0 +1,59 @@
+# 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 tempest.lib.services.volume.v2 import limits_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestLimitsClient(base.BaseServiceTest):
+
+ FAKE_LIMIT_INFO = {
+ "limits": {
+ "rate": [],
+ "absolute": {
+ "totalSnapshotsUsed": 0,
+ "maxTotalBackups": 10,
+ "maxTotalVolumeGigabytes": 1000,
+ "maxTotalSnapshots": 10,
+ "maxTotalBackupGigabytes": 1000,
+ "totalBackupGigabytesUsed": 0,
+ "maxTotalVolumes": 10,
+ "totalVolumesUsed": 0,
+ "totalBackupsUsed": 0,
+ "totalGigabytesUsed": 0
+ }
+ }
+ }
+
+ def setUp(self):
+ super(TestLimitsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = limits_client.LimitsClient(fake_auth,
+ 'volume',
+ 'regionOne')
+
+ def _test_show_limits(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_limits,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIMIT_INFO,
+ bytes_body)
+
+ def test_show_limits_with_str_body(self):
+ self._test_show_limits()
+
+ def test_show_limits_with_bytes_body(self):
+ self._test_show_limits(bytes_body=True)
diff --git a/tempest/tests/lib/services/volume/v2/test_quota_classes_client.py b/tempest/tests/lib/services/volume/v2/test_quota_classes_client.py
new file mode 100644
index 0000000..e715fcc
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v2/test_quota_classes_client.py
@@ -0,0 +1,71 @@
+# 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.
+
+import copy
+
+from tempest.lib.services.volume.v2 import quota_classes_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestQuotaClassesClient(base.BaseServiceTest):
+
+ FAKE_QUOTA_CLASS_SET = {
+ "id": "test",
+ "gigabytes": 2000,
+ "volumes": 200,
+ "snapshots": 50,
+ "backups": 20,
+ "backup_gigabytes": 1500,
+ "per_volume_gigabytes": 500,
+ }
+
+ def setUp(self):
+ super(TestQuotaClassesClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = quota_classes_client.QuotaClassesClient(
+ fake_auth, 'volume', 'regionOne')
+
+ def _test_show_quota_class_set(self, bytes_body=False):
+ fake_body = {'quota_class_set': self.FAKE_QUOTA_CLASS_SET}
+ self.check_service_client_function(
+ self.client.show_quota_class_set,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ fake_body,
+ bytes_body,
+ quota_class_id="test")
+
+ def _test_update_quota_class_set(self, bytes_body=False):
+ fake_quota_class_set = copy.deepcopy(self.FAKE_QUOTA_CLASS_SET)
+ fake_quota_class_set.pop("id")
+ fake_body = {'quota_class_set': fake_quota_class_set}
+ self.check_service_client_function(
+ self.client.update_quota_class_set,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ fake_body,
+ bytes_body,
+ quota_class_id="test")
+
+ def test_show_quota_class_set_with_str_body(self):
+ self._test_show_quota_class_set()
+
+ def test_show_quota_class_set_with_bytes_body(self):
+ self._test_show_quota_class_set(bytes_body=True)
+
+ def test_update_quota_class_set_with_str_boy(self):
+ self._test_update_quota_class_set()
+
+ def test_update_quota_class_set_with_bytes_body(self):
+ self._test_update_quota_class_set(bytes_body=True)
diff --git a/tempest/tests/lib/services/volume/v2/test_quotas_client.py b/tempest/tests/lib/services/volume/v2/test_quotas_client.py
new file mode 100644
index 0000000..6384350
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v2/test_quotas_client.py
@@ -0,0 +1,86 @@
+# Copyright 2016 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.services.volume.v2 import quotas_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestQuotasClient(base.BaseServiceTest):
+ FAKE_QUOTAS = {
+ "quota_set": {
+ "gigabytes": 5,
+ "snapshots": 10,
+ "volumes": 20
+ }
+ }
+
+ FAKE_UPDATE_QUOTAS_REQUEST = {
+ "quota_set": {
+ "security_groups": 45
+ }
+ }
+
+ def setUp(self):
+ super(TestQuotasClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = quotas_client.QuotasClient(fake_auth,
+ 'volume',
+ 'regionOne')
+
+ def _test_show_default_quota_set(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_default_quota_set,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_QUOTAS,
+ bytes_body, tenant_id="fake_tenant")
+
+ def _test_show_quota_set(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_quota_set,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_QUOTAS,
+ bytes_body, tenant_id="fake_tenant")
+
+ def _test_update_quota_set(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_quota_set,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ self.FAKE_UPDATE_QUOTAS_REQUEST,
+ bytes_body, tenant_id="fake_tenant")
+
+ def test_show_default_quota_set_with_str_body(self):
+ self._test_show_default_quota_set()
+
+ def test_show_default_quota_set_with_bytes_body(self):
+ self._test_show_default_quota_set(bytes_body=True)
+
+ def test_show_quota_set_with_str_body(self):
+ self._test_show_quota_set()
+
+ def test_show_quota_set_with_bytes_body(self):
+ self._test_show_quota_set(bytes_body=True)
+
+ def test_update_quota_set_with_str_body(self):
+ self._test_update_quota_set()
+
+ def test_update_quota_set_with_bytes_body(self):
+ self._test_update_quota_set(bytes_body=True)
+
+ def test_delete_quota_set(self):
+ self.check_service_client_function(
+ self.client.delete_quota_set,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ tenant_id="fake_tenant")
diff --git a/tempest/tests/lib/services/volume/v2/test_scheduler_stats_client.py b/tempest/tests/lib/services/volume/v2/test_scheduler_stats_client.py
new file mode 100644
index 0000000..8a5f25f
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v2/test_scheduler_stats_client.py
@@ -0,0 +1,83 @@
+# Copyright 2017 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 tempest.lib.services.volume.v2 import scheduler_stats_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestSchedulerStatsClient(base.BaseServiceTest):
+ FAKE_POOLS_LIST = {
+ "pools": [
+ {
+ "name": "pool1",
+ "capabilities": {
+ "updated": "2014-10-28T00:00:00-00:00",
+ "total_capacity": 1024,
+ "free_capacity": 100,
+ "volume_backend_name": "pool1",
+ "reserved_percentage": 0,
+ "driver_version": "1.0.0",
+ "storage_protocol": "iSCSI",
+ "QoS_support": False
+ }
+ },
+ {
+ "name": "pool2",
+ "capabilities": {
+ "updated": "2014-10-28T00:00:00-00:00",
+ "total_capacity": 512,
+ "free_capacity": 200,
+ "volume_backend_name": "pool2",
+ "reserved_percentage": 0,
+ "driver_version": "1.0.2",
+ "storage_protocol": "iSER",
+ "QoS_support": True
+ }
+ }
+ ]
+ }
+
+ def setUp(self):
+ super(TestSchedulerStatsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = scheduler_stats_client.SchedulerStatsClient(
+ fake_auth, 'volume', 'regionOne')
+
+ def _test_list_pools(self, bytes_body=False, detail=False):
+ resp_body = []
+ if detail:
+ resp_body = self.FAKE_POOLS_LIST
+ else:
+ resp_body = {'pools': [{'name': pool['name']}
+ for pool in self.FAKE_POOLS_LIST['pools']]}
+ self.check_service_client_function(
+ self.client.list_pools,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ resp_body,
+ bytes_body,
+ detail=detail)
+
+ def test_list_pools_with_str_body(self):
+ self._test_list_pools()
+
+ def test_list_pools_with_str_body_and_detail(self):
+ self._test_list_pools(detail=True)
+
+ def test_list_pools_with_bytes_body(self):
+ self._test_list_pools(bytes_body=True)
+
+ def test_list_pools_with_bytes_body_and_detail(self):
+ self._test_list_pools(bytes_body=True, detail=True)
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
new file mode 100644
index 0000000..e03a8eb
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v2/test_snapshot_manage_client.py
@@ -0,0 +1,85 @@
+# 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.
+
+import mock
+
+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
+
+
+class TestSnapshotManageClient(base.BaseServiceTest):
+
+ SNAPSHOT_MANAGE_REQUEST = {
+ "snapshot": {
+ "description": "snapshot-manage-description",
+ "metadata": None,
+ "ref": {
+ "source-name": "_snapshot-22b71da0-94f9-4aca-ad45-7522b3fa96bb"
+ },
+ "name": "snapshot-managed",
+ "volume_id": "7c064b34-1e4b-40bd-93ca-4ac5a973661b"
+ }
+ }
+
+ SNAPSHOT_MANAGE_RESPONSE = {
+ "snapshot": {
+ "status": "creating",
+ "description": "snapshot-manage-description",
+ "updated_at": None,
+ "volume_id": "32bafcc8-7109-42cd-8342-70d8de2bedef",
+ "id": "8fd6eb9d-0a82-456d-b1ec-dea4ac7f1ee2",
+ "size": 1,
+ "name": "snapshot-managed",
+ "created_at": "2017-07-11T10:07:58.000000",
+ "metadata": {}
+ }
+ }
+
+ def setUp(self):
+ super(TestSnapshotManageClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = snapshot_manage_client.SnapshotManageClient(fake_auth,
+ 'volume',
+ 'regionOne')
+
+ def _test_manage_snapshot(self, bytes_body=False):
+ payload = json.dumps(self.SNAPSHOT_MANAGE_REQUEST, sort_keys=True)
+ json_dumps = json.dumps
+
+ # 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_clientv3.json,
+ 'dumps') as mock_dumps:
+ mock_dumps.side_effect = lambda d: json_dumps(d, sort_keys=True)
+
+ self.check_service_client_function(
+ self.client.manage_snapshot,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.SNAPSHOT_MANAGE_RESPONSE,
+ to_utf=bytes_body,
+ status=202,
+ mock_args=['os-snapshot-manage', payload],
+ **self.SNAPSHOT_MANAGE_REQUEST['snapshot'])
+
+ def test_manage_snapshot_with_str_body(self):
+ self._test_manage_snapshot()
+
+ def test_manage_snapshot_with_bytes_body(self):
+ self._test_manage_snapshot(bytes_body=True)
diff --git a/tempest/tests/lib/services/volume/v2/test_snapshots_client.py b/tempest/tests/lib/services/volume/v2/test_snapshots_client.py
new file mode 100644
index 0000000..c9f57a0
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v2/test_snapshots_client.py
@@ -0,0 +1,222 @@
+# Copyright 2016 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.services.volume.v2 import snapshots_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestSnapshotsClient(base.BaseServiceTest):
+ FAKE_CREATE_SNAPSHOT = {
+ "snapshot": {
+ "display_name": "snap-001",
+ "display_description": "Daily backup",
+ "volume_id": "521752a6-acf6-4b2d-bc7a-119f9148cd8c",
+ "force": True
+ }
+ }
+
+ FAKE_UPDATE_SNAPSHOT_REQUEST = {
+ "metadata": {
+ "key": "v1"
+ }
+ }
+
+ FAKE_INFO_SNAPSHOT = {
+ "snapshot": {
+ "id": "3fbbcccf-d058-4502-8844-6feeffdf4cb5",
+ "display_name": "snap-001",
+ "display_description": "Daily backup",
+ "volume_id": "521752a6-acf6-4b2d-bc7a-119f9148cd8c",
+ "status": "available",
+ "size": 30,
+ "created_at": "2012-02-29T03:50:07Z"
+ }
+ }
+
+ FAKE_LIST_SNAPSHOTS = {
+ "snapshots": [
+ {
+ "id": "3fbbcccf-d058-4502-8844-6feeffdf4cb5",
+ "display_name": "snap-001",
+ "display_description": "Daily backup",
+ "volume_id": "521752a6-acf6-4b2d-bc7a-119f9148cd8c",
+ "status": "available",
+ "size": 30,
+ "created_at": "2012-02-29T03:50:07Z",
+ "metadata": {
+ "contents": "junk"
+ }
+ },
+ {
+ "id": "e479997c-650b-40a4-9dfe-77655818b0d2",
+ "display_name": "snap-002",
+ "display_description": "Weekly backup",
+ "volume_id": "76b8950a-8594-4e5b-8dce-0dfa9c696358",
+ "status": "available",
+ "size": 25,
+ "created_at": "2012-03-19T01:52:47Z",
+ "metadata": {}
+ }
+ ]
+ }
+
+ FAKE_SNAPSHOT_METADATA_ITEM = {
+ "meta": {
+ "key1": "value1"
+ }
+ }
+
+ def setUp(self):
+ super(TestSnapshotsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = snapshots_client.SnapshotsClient(fake_auth,
+ 'volume',
+ 'regionOne')
+
+ def _test_create_snapshot(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_snapshot,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_SNAPSHOT,
+ bytes_body,
+ status=202)
+
+ def _test_show_snapshot(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_snapshot,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_INFO_SNAPSHOT,
+ bytes_body,
+ snapshot_id="3fbbcccf-d058-4502-8844-6feeffdf4cb5")
+
+ def _test_list_snapshots(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_snapshots,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_SNAPSHOTS,
+ bytes_body,
+ detail=True)
+
+ def _test_create_snapshot_metadata(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_snapshot_metadata,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_INFO_SNAPSHOT,
+ bytes_body,
+ snapshot_id="3fbbcccf-d058-4502-8844-6feeffdf4cb5",
+ metadata={"key": "v1"})
+
+ def _test_update_snapshot(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_snapshot,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ self.FAKE_UPDATE_SNAPSHOT_REQUEST,
+ bytes_body,
+ snapshot_id="3fbbcccf-d058-4502-8844-6feeffdf4cb5")
+
+ def _test_show_snapshot_metadata(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_snapshot_metadata,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_UPDATE_SNAPSHOT_REQUEST,
+ bytes_body,
+ snapshot_id="3fbbcccf-d058-4502-8844-6feeffdf4cb5")
+
+ def _test_update_snapshot_metadata(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_snapshot_metadata,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ self.FAKE_UPDATE_SNAPSHOT_REQUEST,
+ bytes_body, snapshot_id="cbc36478b0bd8e67e89")
+
+ def _test_update_snapshot_metadata_item(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_snapshot_metadata_item,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ self.FAKE_INFO_SNAPSHOT,
+ bytes_body, volume_type_id="cbc36478b0bd8e67e89")
+
+ def _test_show_snapshot_metadata_item(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_snapshot_metadata_item,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_SNAPSHOT_METADATA_ITEM,
+ bytes_body,
+ snapshot_id="3fbbcccf-d058-4502-8844-6feeffdf4cb5",
+ id="key1")
+
+ def test_create_snapshot_with_str_body(self):
+ self._test_create_snapshot()
+
+ def test_create_snapshot_with_bytes_body(self):
+ self._test_create_snapshot(bytes_body=True)
+
+ def test_show_snapshot_with_str_body(self):
+ self._test_show_snapshot()
+
+ def test_show_snapshot_with_bytes_body(self):
+ self._test_show_snapshot(bytes_body=True)
+
+ def test_list_snapshots_with_str_body(self):
+ self._test_list_snapshots()
+
+ def test_list_snapshots_with_bytes_body(self):
+ self._test_list_snapshots(bytes_body=True)
+
+ def test_create_snapshot_metadata_with_str_body(self):
+ self._test_create_snapshot_metadata()
+
+ def test_create_snapshot_metadata_with_bytes_body(self):
+ self._test_create_snapshot_metadata(bytes_body=True)
+
+ def test_update_snapshot_with_str_body(self):
+ self._test_update_snapshot()
+
+ def test_update_snapshot_with_bytes_body(self):
+ self._test_update_snapshot(bytes_body=True)
+
+ def test_show_snapshot_metadata_with_str_body(self):
+ self._test_show_snapshot_metadata()
+
+ def test_show_snapshot_metadata_with_bytes_body(self):
+ self._test_show_snapshot_metadata(bytes_body=True)
+
+ def test_update_snapshot_metadata_with_str_body(self):
+ self._test_update_snapshot_metadata()
+
+ def test_update_snapshot_metadata_with_bytes_body(self):
+ self._test_update_snapshot_metadata(bytes_body=True)
+
+ def test_show_snapshot_metadata_item_with_str_body(self):
+ self._test_show_snapshot_metadata_item()
+
+ def test_show_snapshot_metadata_item_with_bytes_body(self):
+ self._test_show_snapshot_metadata_item(bytes_body=True)
+
+ def test_force_delete_snapshot(self):
+ self.check_service_client_function(
+ self.client.force_delete_snapshot,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ snapshot_id="521752a6-acf6-4b2d-bc7a-119f9148cd8c",
+ status=202)
+
+ def test_delete_snapshot(self):
+ self.check_service_client_function(
+ self.client.delete_snapshot,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ snapshot_id="521752a6-acf6-4b2d-bc7a-119f9148cd8c",
+ status=202)
diff --git a/tempest/tests/lib/services/volume/v2/test_transfers_client.py b/tempest/tests/lib/services/volume/v2/test_transfers_client.py
new file mode 100644
index 0000000..8e7c6f4
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v2/test_transfers_client.py
@@ -0,0 +1,160 @@
+# 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.
+
+import copy
+
+import mock
+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
+
+
+class TestTransfersClient(base.BaseServiceTest):
+
+ FAKE_VOLUME_TRANSFER_ID = "0e89cdd1-6249-421b-96d8-25fac0623d42"
+
+ FAKE_VOLUME_TRANSFER_INFO = {
+ "transfer": {
+ "id": FAKE_VOLUME_TRANSFER_ID,
+ "name": "fake-volume-transfer",
+ "volume_id": "47bf04ef-1ea5-4c5f-a375-430a086d6747",
+ "created_at": "2017-04-18T09:10:03.000000",
+ "links": [
+ {
+ "href": "fake-url-1",
+ "rel": "self"
+ },
+ {
+ "href": "fake-url-2",
+ "rel": "bookmark"
+ }
+ ]
+ }
+ }
+
+ def setUp(self):
+ super(TestTransfersClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = transfers_client.TransfersClient(fake_auth,
+ 'volume',
+ 'regionOne')
+
+ def _test_create_volume_transfer(self, bytes_body=False):
+ resp_body = copy.deepcopy(self.FAKE_VOLUME_TRANSFER_INFO)
+ resp_body['transfer'].update({"auth_key": "fake-auth-key"})
+ kwargs = {"name": "fake-volume-transfer",
+ "volume_id": "47bf04ef-1ea5-4c5f-a375-430a086d6747"}
+ payload = json.dumps({"transfer": kwargs}, sort_keys=True)
+ json_dumps = json.dumps
+
+ # 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_clientv3.json, 'dumps') as mock_dumps:
+ mock_dumps.side_effect = lambda d: json_dumps(d, sort_keys=True)
+
+ self.check_service_client_function(
+ self.client.create_volume_transfer,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ resp_body,
+ to_utf=bytes_body,
+ status=202,
+ mock_args=['os-volume-transfer', payload],
+ **kwargs)
+
+ def _test_accept_volume_transfer(self, bytes_body=False):
+ resp_body = copy.deepcopy(self.FAKE_VOLUME_TRANSFER_INFO)
+ resp_body['transfer'].pop('created_at')
+ kwargs = {"auth_key": "fake-auth-key"}
+ payload = json.dumps({"accept": kwargs}, sort_keys=True)
+ json_dumps = json.dumps
+
+ # 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_clientv3.json, 'dumps') as mock_dumps:
+ mock_dumps.side_effect = lambda d: json_dumps(d, sort_keys=True)
+
+ self.check_service_client_function(
+ self.client.accept_volume_transfer,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ resp_body,
+ to_utf=bytes_body,
+ status=202,
+ mock_args=['os-volume-transfer/%s/accept' %
+ self.FAKE_VOLUME_TRANSFER_ID, payload],
+ transfer_id=self.FAKE_VOLUME_TRANSFER_ID,
+ **kwargs)
+
+ def _test_show_volume_transfer(self, bytes_body=False):
+ resp_body = self.FAKE_VOLUME_TRANSFER_INFO
+ self.check_service_client_function(
+ self.client.show_volume_transfer,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ resp_body,
+ to_utf=bytes_body,
+ transfer_id="0e89cdd1-6249-421b-96d8-25fac0623d42")
+
+ def _test_list_volume_transfers(self, detail=False, bytes_body=False):
+ resp_body = copy.deepcopy(self.FAKE_VOLUME_TRANSFER_INFO)
+ if not detail:
+ resp_body['transfer'].pop('created_at')
+ resp_body = {"transfers": [resp_body['transfer']]}
+ self.check_service_client_function(
+ self.client.list_volume_transfers,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ resp_body,
+ to_utf=bytes_body,
+ detail=detail)
+
+ def test_create_volume_transfer_with_str_body(self):
+ self._test_create_volume_transfer()
+
+ def test_create_volume_transfer_with_bytes_body(self):
+ self._test_create_volume_transfer(bytes_body=True)
+
+ def test_accept_volume_transfer_with_str_body(self):
+ self._test_accept_volume_transfer()
+
+ def test_accept_volume_transfer_with_bytes_body(self):
+ self._test_accept_volume_transfer(bytes_body=True)
+
+ def test_show_volume_transfer_with_str_body(self):
+ self._test_show_volume_transfer()
+
+ def test_show_volume_transfer_with_bytes_body(self):
+ self._test_show_volume_transfer(bytes_body=True)
+
+ def test_list_volume_transfers_with_str_body(self):
+ self._test_list_volume_transfers()
+
+ def test_list_volume_transfers_with_bytes_body(self):
+ self._test_list_volume_transfers(bytes_body=True)
+
+ def test_list_volume_transfers_with_detail_with_str_body(self):
+ self._test_list_volume_transfers(detail=True)
+
+ def test_list_volume_transfers_with_detail_with_bytes_body(self):
+ self._test_list_volume_transfers(detail=True, bytes_body=True)
+
+ def test_delete_volume_transfer(self):
+ self.check_service_client_function(
+ self.client.delete_volume_transfer,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ status=202,
+ transfer_id="0e89cdd1-6249-421b-96d8-25fac0623d42")
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
new file mode 100644
index 0000000..0fb66bb
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v2/test_volume_manage_client.py
@@ -0,0 +1,113 @@
+# 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.
+
+import mock
+
+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
+
+
+class TestVolumeManageClient(base.BaseServiceTest):
+
+ VOLUME_MANAGE_REQUEST = {
+ "volume": {
+ "host": "controller1@rbd#rbd",
+ "name": "volume-managed",
+ "availability_zone": "nova",
+ "bootable": False,
+ "metadata": None,
+ "ref": {
+ "source-name": "volume-2ce6ca46-e6c1-4fe5-8268-3a1c536fcbf3"
+ },
+ "volume_type": None,
+ "description": "volume-manage-description"
+ }
+ }
+
+ VOLUME_MANAGE_RESPONSE = {
+ "volume": {
+ "migration_status": None,
+ "attachments": [],
+ "links": [
+ {
+ "href": "fake-url-1",
+ "rel": "self"
+ },
+ {
+ "href": "fake-url-2",
+ "rel": "bookmark"
+ }
+ ],
+ "availability_zone": "nova",
+ "os-vol-host-attr:host": "controller1@rbd#rbd",
+ "encrypted": False,
+ "updated_at": None,
+ "replication_status": None,
+ "snapshot_id": None,
+ "id": "c07cd4a4-b52b-4511-a176-fbaa2011a227",
+ "size": 0,
+ "user_id": "142d8663efce464c89811c63e45bd82e",
+ "os-vol-tenant-attr:tenant_id": "f21a9c86d7114bf99c711f4874d80474",
+ "os-vol-mig-status-attr:migstat": None,
+ "metadata": {},
+ "status": "creating",
+ "description": "volume-manage-description",
+ "multiattach": False,
+ "source_volid": None,
+ "consistencygroup_id": None,
+ "os-vol-mig-status-attr:name_id": None,
+ "name": "volume-managed",
+ "bootable": "false",
+ "created_at": "2017-07-11T09:14:01.000000",
+ "volume_type": None
+ }
+ }
+
+ def setUp(self):
+ super(TestVolumeManageClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = volume_manage_client.VolumeManageClient(fake_auth,
+ 'volume',
+ 'regionOne')
+
+ def _test_manage_volume(self, bytes_body=False):
+ payload = json.dumps(self.VOLUME_MANAGE_REQUEST, sort_keys=True)
+ json_dumps = json.dumps
+
+ # 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_clientv3.json,
+ 'dumps') as mock_dumps:
+ mock_dumps.side_effect = lambda d: json_dumps(d, sort_keys=True)
+
+ self.check_service_client_function(
+ self.client.manage_volume,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.VOLUME_MANAGE_RESPONSE,
+ to_utf=bytes_body,
+ status=202,
+ mock_args=['os-volume-manage', payload],
+ **self.VOLUME_MANAGE_REQUEST['volume'])
+
+ def test_manage_volume_with_str_body(self):
+ self._test_manage_volume()
+
+ def test_manage_volume_with_bytes_body(self):
+ self._test_manage_volume(bytes_body=True)
diff --git a/tempest/tests/lib/services/volume/v2/test_volumes_client.py b/tempest/tests/lib/services/volume/v2/test_volumes_client.py
new file mode 100644
index 0000000..d7b042e
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v2/test_volumes_client.py
@@ -0,0 +1,127 @@
+# 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.services.volume.v2 import volumes_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestVolumesClient(base.BaseServiceTest):
+
+ FAKE_VOLUME_METADATA_ITEM = {
+ "meta": {
+ "key1": "value1"
+ }
+ }
+
+ FAKE_VOLUME_IMAGE_METADATA = {
+ "metadata": {
+ "container_format": "bare",
+ "min_ram": "0",
+ "disk_format": "raw",
+ "image_name": "xly-ubuntu16-server",
+ "image_id": "3e087b0c-10c5-4255-b147-6e8e9dbad6fc",
+ "checksum": "008f5d22fe3cb825d714da79607a90f9",
+ "min_disk": "0",
+ "size": "8589934592"
+ }
+ }
+
+ def setUp(self):
+ super(TestVolumesClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = volumes_client.VolumesClient(fake_auth,
+ 'volume',
+ 'regionOne')
+
+ def _test_retype_volume(self, bytes_body=False):
+ kwargs = {
+ "new_type": "dedup-tier-replication",
+ "migration_policy": "never"
+ }
+
+ self.check_service_client_function(
+ self.client.retype_volume,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ to_utf=bytes_body,
+ status=202,
+ volume_id="a3be971b-8de5-4bdf-bdb8-3d8eb0fb69f8",
+ **kwargs
+ )
+
+ def _test_force_detach_volume(self, bytes_body=False):
+ kwargs = {
+ 'attachment_id': '6980e295-920f-412e-b189-05c50d605acd',
+ 'connector': {
+ 'initiator': 'iqn.2017-04.org.fake:01'
+ }
+ }
+
+ self.check_service_client_function(
+ self.client.force_detach_volume,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ to_utf=bytes_body,
+ status=202,
+ volume_id="a3be971b-8de5-4bdf-bdb8-3d8eb0fb69f8",
+ **kwargs
+ )
+
+ def _test_show_volume_metadata_item(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_volume_metadata_item,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_VOLUME_METADATA_ITEM,
+ to_utf=bytes_body,
+ volume_id="a3be971b-8de5-4bdf-bdb8-3d8eb0fb69f8",
+ id="key1")
+
+ def _test_show_volume_image_metadata(self, bytes_body=False):
+ fake_volume_id = "a3be971b-8de5-4bdf-bdb8-3d8eb0fb69f8"
+ self.check_service_client_function(
+ self.client.show_volume_image_metadata,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_VOLUME_IMAGE_METADATA,
+ to_utf=bytes_body,
+ mock_args=['volumes/%s/action' % fake_volume_id,
+ json.dumps({"os-show_image_metadata": {}})],
+ volume_id=fake_volume_id)
+
+ def test_force_detach_volume_with_str_body(self):
+ self._test_force_detach_volume()
+
+ def test_force_detach_volume_with_bytes_body(self):
+ self._test_force_detach_volume(bytes_body=True)
+
+ def test_show_volume_metadata_item_with_str_body(self):
+ self._test_show_volume_metadata_item()
+
+ def test_show_volume_metadata_item_with_bytes_body(self):
+ self._test_show_volume_metadata_item(bytes_body=True)
+
+ def test_show_volume_image_metadata_with_str_body(self):
+ self._test_show_volume_image_metadata()
+
+ def test_show_volume_image_metadata_with_bytes_body(self):
+ self._test_show_volume_image_metadata(bytes_body=True)
+
+ def test_retype_volume_with_str_body(self):
+ self._test_retype_volume()
+
+ def test_retype_volume_with_bytes_body(self):
+ self._test_retype_volume(bytes_body=True)
diff --git a/tempest/tests/negative/__init__.py b/tempest/tests/lib/services/volume/v3/__init__.py
similarity index 100%
copy from tempest/tests/negative/__init__.py
copy to tempest/tests/lib/services/volume/v3/__init__.py
diff --git a/tempest/tests/lib/services/volume/v3/test_backups_client.py b/tempest/tests/lib/services/volume/v3/test_backups_client.py
new file mode 100644
index 0000000..f1ce987
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v3/test_backups_client.py
@@ -0,0 +1,50 @@
+# 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 tempest.lib.services.volume.v3 import backups_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestBackupsClient(base.BaseServiceTest):
+
+ FAKE_BACKUP_UPDATE = {
+ "backup": {
+ "id": "4c65c15f-a5c5-464b-b92a-90e4c04636a7",
+ "name": "fake-backup-name",
+ "links": "fake-links"
+ }
+ }
+
+ def setUp(self):
+ super(TestBackupsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = backups_client.BackupsClient(fake_auth,
+ 'volume',
+ 'regionOne')
+
+ def _test_update_backup(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_backup,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ self.FAKE_BACKUP_UPDATE,
+ bytes_body,
+ backup_id='4c65c15f-a5c5-464b-b92a-90e4c04636a7')
+
+ def test_update_backup_with_str_body(self):
+ self._test_update_backup()
+
+ def test_update_backup_with_bytes_body(self):
+ self._test_update_backup(bytes_body=True)
diff --git a/tempest/tests/lib/services/volume/v3/test_group_snapshots_client.py b/tempest/tests/lib/services/volume/v3/test_group_snapshots_client.py
new file mode 100644
index 0000000..c2784b2
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v3/test_group_snapshots_client.py
@@ -0,0 +1,172 @@
+# Copyright (C) 2017 Dell Inc. or its subsidiaries.
+#
+# 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.volume.v3 import group_snapshots_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestGroupSnapshotsClient(base.BaseServiceTest):
+ FAKE_CREATE_GROUP_SNAPSHOT = {
+ "group_snapshot": {
+ "group_id": "49c8c114-0d68-4e89-b8bc-3f5a674d54be",
+ "name": "group-snapshot-001",
+ "description": "Test group snapshot 1"
+ }
+ }
+
+ FAKE_INFO_GROUP_SNAPSHOT = {
+ "group_snapshot": {
+ "id": "0e701ab8-1bec-4b9f-b026-a7ba4af13578",
+ "group_id": "49c8c114-0d68-4e89-b8bc-3f5a674d54be",
+ "name": "group-snapshot-001",
+ "description": "Test group snapshot 1",
+ "group_type_id": "0e58433f-d108-4bf3-a22c-34e6b71ef86b",
+ "status": "available",
+ "created_at": "20127-06-20T03:50:07Z"
+ }
+ }
+
+ FAKE_LIST_GROUP_SNAPSHOTS = {
+ "group_snapshots": [
+ {
+ "id": "0e701ab8-1bec-4b9f-b026-a7ba4af13578",
+ "group_id": "49c8c114-0d68-4e89-b8bc-3f5a674d54be",
+ "name": "group-snapshot-001",
+ "description": "Test group snapshot 1",
+ "group_type_id": "0e58433f-d108-4bf3-a22c-34e6b71ef86b",
+ "status": "available",
+ "created_at": "2017-06-20T03:50:07Z",
+ },
+ {
+ "id": "e479997c-650b-40a4-9dfe-77655818b0d2",
+ "group_id": "49c8c114-0d68-4e89-b8bc-3f5a674d54be",
+ "name": "group-snapshot-002",
+ "description": "Test group snapshot 2",
+ "group_type_id": "0e58433f-d108-4bf3-a22c-34e6b71ef86b",
+ "status": "available",
+ "created_at": "2017-06-19T01:52:47Z",
+ },
+ {
+ "id": "c5c4769e-213c-40a6-a568-8e797bb691d4",
+ "group_id": "49c8c114-0d68-4e89-b8bc-3f5a674d54be",
+ "name": "group-snapshot-003",
+ "description": "Test group snapshot 3",
+ "group_type_id": "0e58433f-d108-4bf3-a22c-34e6b71ef86b",
+ "status": "available",
+ "created_at": "2017-06-18T06:34:32Z",
+ }
+ ]
+ }
+
+ def setUp(self):
+ super(TestGroupSnapshotsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = group_snapshots_client.GroupSnapshotsClient(
+ fake_auth, 'volume', 'regionOne')
+
+ def _test_create_group_snapshot(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_group_snapshot,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_GROUP_SNAPSHOT,
+ bytes_body,
+ group_id="49c8c114-0d68-4e89-b8bc-3f5a674d54be",
+ status=202)
+
+ def _test_show_group_snapshot(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_group_snapshot,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_INFO_GROUP_SNAPSHOT,
+ bytes_body,
+ group_snapshot_id="3fbbcccf-d058-4502-8844-6feeffdf4cb5")
+
+ def _test_list_group_snapshots(self, detail=False, bytes_body=False,
+ mock_args='group_snapshots', **params):
+ resp_body = []
+ if detail:
+ resp_body = self.FAKE_LIST_GROUP_SNAPSHOTS
+ else:
+ resp_body = {
+ 'group_snapshots': [{
+ 'id': group_snapshot['id'],
+ 'name': group_snapshot['name'],
+ 'group_type_id': group_snapshot['group_type_id']}
+ for group_snapshot in
+ self.FAKE_LIST_GROUP_SNAPSHOTS['group_snapshots']
+ ]
+ }
+ self.check_service_client_function(
+ self.client.list_group_snapshots,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ resp_body,
+ to_utf=bytes_body,
+ mock_args=[mock_args],
+ detail=detail,
+ **params)
+
+ def test_create_group_snapshot_with_str_body(self):
+ self._test_create_group_snapshot()
+
+ def test_create_group_snapshot_with_bytes_body(self):
+ self._test_create_group_snapshot(bytes_body=True)
+
+ def test_show_group_snapshot_with_str_body(self):
+ self._test_show_group_snapshot()
+
+ def test_show_group_snapshot_with_bytes_body(self):
+ self._test_show_group_snapshot(bytes_body=True)
+
+ def test_list_group_snapshots_with_str_body(self):
+ self._test_list_group_snapshots()
+
+ def test_list_group_snapshots_with_bytes_body(self):
+ self._test_list_group_snapshots(bytes_body=True)
+
+ def test_list_group_snapshots_with_detail_with_str_body(self):
+ mock_args = "group_snapshots/detail"
+ self._test_list_group_snapshots(detail=True, mock_args=mock_args)
+
+ def test_list_group_snapshots_with_detail_with_bytes_body(self):
+ mock_args = "group_snapshots/detail"
+ self._test_list_group_snapshots(detail=True, bytes_body=True,
+ mock_args=mock_args)
+
+ def test_list_group_snapshots_with_params(self):
+ # Run the test separately for each param, to avoid assertion error
+ # resulting from randomized params order.
+ mock_args = 'group_snapshots?sort_key=name'
+ self._test_list_group_snapshots(mock_args=mock_args, sort_key='name')
+
+ mock_args = 'group_snapshots/detail?limit=10'
+ self._test_list_group_snapshots(detail=True, bytes_body=True,
+ mock_args=mock_args, limit=10)
+
+ def test_delete_group_snapshot(self):
+ self.check_service_client_function(
+ self.client.delete_group_snapshot,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ group_snapshot_id='0e701ab8-1bec-4b9f-b026-a7ba4af13578',
+ status=202)
+
+ def test_reset_group_snapshot_status(self):
+ self.check_service_client_function(
+ self.client.reset_group_snapshot_status,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ status=202,
+ group_snapshot_id='0e701ab8-1bec-4b9f-b026-a7ba4af13578',
+ status_to_set='error')
diff --git a/tempest/tests/lib/services/volume/v3/test_group_types_client.py b/tempest/tests/lib/services/volume/v3/test_group_types_client.py
new file mode 100644
index 0000000..c60cc36
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v3/test_group_types_client.py
@@ -0,0 +1,238 @@
+# Copyright (C) 2017 Dell Inc. or its subsidiaries.
+#
+# 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.services.volume.v3 import group_types_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestGroupTypesClient(base.BaseServiceTest):
+ FAKE_CREATE_GROUP_TYPE = {
+ "group_type": {
+ "name": "group-type-001",
+ "description": "Test group type 1",
+ "group_specs": {},
+ "is_public": True,
+ }
+ }
+
+ FAKE_INFO_GROUP_TYPE = {
+ "group_type": {
+ "id": "0e701ab8-1bec-4b9f-b026-a7ba4af13578",
+ "name": "group-type-001",
+ "description": "Test group type 1",
+ "is_public": True,
+ "created_at": "20127-06-20T03:50:07Z",
+ "group_specs": {},
+ }
+ }
+
+ FAKE_LIST_GROUP_TYPES = {
+ "group_types": [
+ {
+ "id": "0e701ab8-1bec-4b9f-b026-a7ba4af13578",
+ "name": "group-type-001",
+ "description": "Test group type 1",
+ "is_public": True,
+ "created_at": "2017-06-20T03:50:07Z",
+ "group_specs": {},
+ },
+ {
+ "id": "e479997c-650b-40a4-9dfe-77655818b0d2",
+ "name": "group-type-002",
+ "description": "Test group type 2",
+ "is_public": True,
+ "created_at": "2017-06-19T01:52:47Z",
+ "group_specs": {},
+ },
+ {
+ "id": "c5c4769e-213c-40a6-a568-8e797bb691d4",
+ "name": "group-type-003",
+ "description": "Test group type 3",
+ "is_public": True,
+ "created_at": "2017-06-18T06:34:32Z",
+ "group_specs": {},
+ }
+ ]
+ }
+
+ FAKE_CREATE_GROUP_TYPE_SPECS = {
+ "group_specs": {
+ "key1": "value1",
+ "key2": "value2"
+ }
+ }
+
+ FAKE_LIST_GROUP_TYPE_SPECS = {
+ "group_specs": {
+ "key1": "value1",
+ "key2": "value2"
+ }
+ }
+
+ FAKE_SHOW_GROUP_TYPE_SPECS_ITEM = {
+ "key1": "value1"
+ }
+
+ FAKE_UPDATE_GROUP_TYPE_SPECS_ITEM = {
+ "key2": "value2-updated"
+ }
+
+ def setUp(self):
+ super(TestGroupTypesClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = group_types_client.GroupTypesClient(fake_auth,
+ 'volume',
+ 'regionOne')
+
+ def _test_create_group_type(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_group_type,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_GROUP_TYPE,
+ bytes_body,
+ status=202)
+
+ def _test_show_group_type(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_group_type,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_INFO_GROUP_TYPE,
+ bytes_body,
+ group_type_id="3fbbcccf-d058-4502-8844-6feeffdf4cb5")
+
+ def _test_list_group_types(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_group_types,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_GROUP_TYPES,
+ bytes_body)
+
+ def _test_update_group_types(self, bytes_body=False):
+ resp_body = copy.deepcopy(self.FAKE_INFO_GROUP_TYPE)
+ resp_body['group_type'].pop('created_at')
+
+ self.check_service_client_function(
+ self.client.update_group_type,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ resp_body,
+ bytes_body,
+ group_type_id="3fbbcccf-d058-4502-8844-6feeffdf4cb5",
+ name='updated-group-type-name')
+
+ def _test_create_or_update_group_type_specs(self, bytes_body=False):
+ group_specs = self.FAKE_CREATE_GROUP_TYPE_SPECS['group_specs']
+ self.check_service_client_function(
+ self.client.create_or_update_group_type_specs,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_GROUP_TYPE_SPECS,
+ bytes_body,
+ group_type_id="3fbbcccf-d058-4502-8844-6feeffdf4cb5",
+ group_specs=group_specs,
+ status=202)
+
+ def _test_list_group_type_specs(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_group_type_specs,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_GROUP_TYPE_SPECS,
+ bytes_body,
+ group_type_id="3fbbcccf-d058-4502-8844-6feeffdf4cb5")
+
+ def _test_show_group_type_specs_item(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_group_type_specs_item,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_SHOW_GROUP_TYPE_SPECS_ITEM,
+ bytes_body,
+ group_type_id="3fbbcccf-d058-4502-8844-6feeffdf4cb5",
+ spec_id="key1")
+
+ def _test_update_group_type_specs_item(self, bytes_body=False):
+ spec = self.FAKE_UPDATE_GROUP_TYPE_SPECS_ITEM
+ self.check_service_client_function(
+ self.client.update_group_type_specs_item,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ self.FAKE_UPDATE_GROUP_TYPE_SPECS_ITEM,
+ bytes_body,
+ group_type_id="3fbbcccf-d058-4502-8844-6feeffdf4cb5",
+ spec_id="key2",
+ spec=spec)
+
+ def test_create_group_type_with_str_body(self):
+ self._test_create_group_type()
+
+ def test_create_group_type_with_bytes_body(self):
+ self._test_create_group_type(bytes_body=True)
+
+ def test_delete_group_type(self):
+ self.check_service_client_function(
+ self.client.delete_group_type,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ group_type_id='0e58433f-d108-4bf3-a22c-34e6b71ef86b',
+ status=202)
+
+ def test_show_group_type_with_str_body(self):
+ self._test_show_group_type()
+
+ def test_show_group_type_with_bytes_body(self):
+ self._test_show_group_type(bytes_body=True)
+
+ def test_list_group_types_with_str_body(self):
+ self._test_list_group_types()
+
+ def test_list_group_types_with_bytes_body(self):
+ self._test_list_group_types(bytes_body=True)
+
+ def test_update_group_types_with_str_body(self):
+ self._test_update_group_types()
+
+ def test_update_group_types_with_bytes_body(self):
+ self._test_update_group_types(bytes_body=True)
+
+ def test_create_or_update_group_type_specs_with_str_body(self):
+ self._test_create_or_update_group_type_specs()
+
+ def test_create_or_update_group_type_specs_with_bytes_body(self):
+ self._test_create_or_update_group_type_specs(bytes_body=True)
+
+ def test_list_group_type_specs_with_str_body(self):
+ self._test_list_group_type_specs()
+
+ def test_list_group_type_specs_with_bytes_body(self):
+ self._test_list_group_type_specs(bytes_body=True)
+
+ def test_show_group_type_specs_item_with_str_body(self):
+ self._test_show_group_type_specs_item()
+
+ def test_show_group_type_specs_item_with_bytes_body(self):
+ self._test_show_group_type_specs_item(bytes_body=True)
+
+ def test_update_group_type_specs_item_with_str_body(self):
+ self._test_update_group_type_specs_item()
+
+ def test_update_group_type_specs_item_with_bytes_body(self):
+ self._test_update_group_type_specs_item(bytes_body=True)
+
+ def test_delete_group_type_specs_item(self):
+ self.check_service_client_function(
+ self.client.delete_group_type_specs_item,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ group_type_id='0e58433f-d108-4bf3-a22c-34e6b71ef86b',
+ spec_id='key1',
+ status=202)
diff --git a/tempest/tests/lib/services/volume/v3/test_groups_client.py b/tempest/tests/lib/services/volume/v3/test_groups_client.py
new file mode 100644
index 0000000..918e958
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v3/test_groups_client.py
@@ -0,0 +1,195 @@
+# Copyright (C) 2017 Dell Inc. or its subsidiaries.
+#
+# 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.volume.v3 import groups_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestGroupsClient(base.BaseServiceTest):
+ FAKE_CREATE_GROUP = {
+ "group": {
+ "name": "group-001",
+ "description": "Test group 1",
+ "group_type": "0e58433f-d108-4bf3-a22c-34e6b71ef86b",
+ "volume_types": ["2103099d-7cc3-4e52-a2f1-23a5284416f3"],
+ "availability_zone": "az1",
+ }
+ }
+
+ FAKE_CREATE_GROUP_FROM_GROUP_SNAPSHOT = {
+ "create-from-src": {
+ "name": "group-002",
+ "description": "Test group 2",
+ "group_snapshot_id": "79c9afdb-7e46-4d71-9249-1f022886963c",
+ }
+ }
+
+ FAKE_CREATE_GROUP_FROM_GROUP = {
+ "create-from-src": {
+ "name": "group-003",
+ "description": "Test group 3",
+ "source_group_id": "e92f9dc7-0b20-492d-8ab2-3ad8fdac270e",
+ }
+ }
+
+ FAKE_UPDATE_GROUP = {
+ "group": {
+ "name": "new-group",
+ "description": "New test group",
+ "add_volumes": "27d45037-ade3-4a87-b729-dba3293c06f3,"
+ "6e7cd916-d961-41cc-b3bd-0601ca0c701f",
+ "remove_volumes": "4d580519-6467-448e-95e9-5b25c94d83c7,"
+ "ea22464c-f095-4a87-a31f-c5d34e0c6fc9"
+ }
+ }
+
+ FAKE_INFO_GROUP = {
+ "group": {
+ "id": "0e701ab8-1bec-4b9f-b026-a7ba4af13578",
+ "name": "group-001",
+ "description": "Test group 1",
+ "group_type": "0e58433f-d108-4bf3-a22c-34e6b71ef86b",
+ "volume_types": ["2103099d-7cc3-4e52-a2f1-23a5284416f3"],
+ "status": "available",
+ "availability_zone": "az1",
+ "created_at": "20127-06-20T03:50:07Z"
+ }
+ }
+
+ FAKE_LIST_GROUPS = {
+ "groups": [
+ {
+ "id": "0e701ab8-1bec-4b9f-b026-a7ba4af13578",
+ "name": "group-001",
+ "description": "Test group 1",
+ "group_type": "0e58433f-d108-4bf3-a22c-34e6b71ef86b",
+ "volume_types": ["2103099d-7cc3-4e52-a2f1-23a5284416f3"],
+ "status": "available",
+ "availability_zone": "az1",
+ "created_at": "2017-06-20T03:50:07Z",
+ },
+ {
+ "id": "e479997c-650b-40a4-9dfe-77655818b0d2",
+ "name": "group-002",
+ "description": "Test group 2",
+ "group_snapshot_id": "79c9afdb-7e46-4d71-9249-1f022886963c",
+ "group_type": "0e58433f-d108-4bf3-a22c-34e6b71ef86b",
+ "volume_types": ["2103099d-7cc3-4e52-a2f1-23a5284416f3"],
+ "status": "available",
+ "availability_zone": "az1",
+ "created_at": "2017-06-19T01:52:47Z",
+ },
+ {
+ "id": "c5c4769e-213c-40a6-a568-8e797bb691d4",
+ "name": "group-003",
+ "description": "Test group 3",
+ "source_group_id": "e92f9dc7-0b20-492d-8ab2-3ad8fdac270e",
+ "group_type": "0e58433f-d108-4bf3-a22c-34e6b71ef86b",
+ "volume_types": ["2103099d-7cc3-4e52-a2f1-23a5284416f3"],
+ "status": "available",
+ "availability_zone": "az1",
+ "created_at": "2017-06-18T06:34:32Z",
+ }
+ ]
+ }
+
+ def setUp(self):
+ super(TestGroupsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = groups_client.GroupsClient(fake_auth,
+ 'volume',
+ 'regionOne')
+
+ def _test_create_group(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_group,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_GROUP,
+ bytes_body,
+ status=202)
+
+ def _test_show_group(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_group,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_INFO_GROUP,
+ bytes_body,
+ group_id="3fbbcccf-d058-4502-8844-6feeffdf4cb5")
+
+ def _test_list_groups(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_groups,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_GROUPS,
+ bytes_body,
+ detail=True)
+
+ def test_create_group_with_str_body(self):
+ self._test_create_group()
+
+ def test_create_group_with_bytes_body(self):
+ self._test_create_group(bytes_body=True)
+
+ def test_show_group_with_str_body(self):
+ self._test_show_group()
+
+ def test_show_group_with_bytes_body(self):
+ self._test_show_group(bytes_body=True)
+
+ def test_list_groups_with_str_body(self):
+ self._test_list_groups()
+
+ def test_list_groups_with_bytes_body(self):
+ self._test_list_groups(bytes_body=True)
+
+ def test_delete_group(self):
+ self.check_service_client_function(
+ self.client.delete_group,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ group_id='0e701ab8-1bec-4b9f-b026-a7ba4af13578',
+ status=202)
+
+ def test_create_group_from_group_snapshot(self):
+ self.check_service_client_function(
+ self.client.create_group_from_source,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_GROUP_FROM_GROUP_SNAPSHOT,
+ status=202)
+
+ def test_create_group_from_group(self):
+ self.check_service_client_function(
+ self.client.create_group_from_source,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_GROUP_FROM_GROUP,
+ status=202)
+
+ def test_update_group(self):
+ self.check_service_client_function(
+ self.client.update_group,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ {},
+ group_id='0e701ab8-1bec-4b9f-b026-a7ba4af13578',
+ status=202,
+ **self.FAKE_UPDATE_GROUP['group'])
+
+ def test_reset_group_status(self):
+ self.check_service_client_function(
+ self.client.reset_group_status,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ status=202,
+ group_id='0e701ab8-1bec-4b9f-b026-a7ba4af13578',
+ status_to_set='error')
diff --git a/tempest/tests/lib/services/volume/v3/test_services_client.py b/tempest/tests/lib/services/volume/v3/test_services_client.py
new file mode 100644
index 0000000..f65228f
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v3/test_services_client.py
@@ -0,0 +1,214 @@
+# Copyright 2018 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.
+
+import copy
+
+import mock
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.services.volume.v3 import services_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestServicesClient(base.BaseServiceTest):
+
+ FAKE_SERVICE_LIST = {
+ "services": [
+ {
+ "status": "enabled",
+ "binary": "cinder-backup",
+ "zone": "nova",
+ "state": "up",
+ "updated_at": "2017-07-20T07:20:17.000000",
+ "host": "fake-host",
+ "disabled_reason": None
+ },
+ {
+ "status": "enabled",
+ "binary": "cinder-scheduler",
+ "zone": "nova",
+ "state": "up",
+ "updated_at": "2017-07-20T07:20:24.000000",
+ "host": "fake-host",
+ "disabled_reason": None
+ },
+ {
+ "status": "enabled",
+ "binary": "cinder-volume",
+ "zone": "nova",
+ "frozen": False,
+ "state": "up",
+ "updated_at": "2017-07-20T07:20:20.000000",
+ "host": "fake-host@lvm",
+ "replication_status": "disabled",
+ "active_backend_id": None,
+ "disabled_reason": None
+ }
+ ]
+ }
+
+ FAKE_SERVICE_REQUEST = {
+ "host": "fake-host",
+ "binary": "cinder-volume"
+ }
+
+ FAKE_SERVICE_RESPONSE = {
+ "disabled": False,
+ "status": "enabled",
+ "host": "fake-host@lvm",
+ "service": "",
+ "binary": "cinder-volume",
+ "disabled_reason": None
+ }
+
+ def setUp(self):
+ super(TestServicesClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = services_client.ServicesClient(fake_auth,
+ 'volume',
+ 'regionOne')
+
+ def _test_list_services(self, bytes_body=False,
+ mock_args='os-services', **params):
+ self.check_service_client_function(
+ self.client.list_services,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_SERVICE_LIST,
+ to_utf=bytes_body,
+ mock_args=[mock_args],
+ **params)
+
+ def _test_enable_service(self, bytes_body=False):
+ resp_body = self.FAKE_SERVICE_RESPONSE
+ kwargs = self.FAKE_SERVICE_REQUEST
+ payload = json.dumps(kwargs, sort_keys=True)
+ json_dumps = json.dumps
+
+ # 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(services_client.json, 'dumps') as mock_dumps:
+ mock_dumps.side_effect = lambda d: json_dumps(d, sort_keys=True)
+
+ self.check_service_client_function(
+ self.client.enable_service,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ resp_body,
+ to_utf=bytes_body,
+ mock_args=['os-services/enable', payload],
+ **kwargs)
+
+ def _test_disable_service(self, bytes_body=False):
+ resp_body = copy.deepcopy(self.FAKE_SERVICE_RESPONSE)
+ resp_body.pop('disabled_reason')
+ resp_body['disabled'] = True
+ resp_body['status'] = 'disabled'
+ kwargs = self.FAKE_SERVICE_REQUEST
+ payload = json.dumps(kwargs, sort_keys=True)
+ json_dumps = json.dumps
+
+ # 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(services_client.json, 'dumps') as mock_dumps:
+ mock_dumps.side_effect = lambda d: json_dumps(d, sort_keys=True)
+
+ self.check_service_client_function(
+ self.client.disable_service,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ resp_body,
+ to_utf=bytes_body,
+ mock_args=['os-services/disable', payload],
+ **kwargs)
+
+ def _test_disable_log_reason(self, bytes_body=False):
+ resp_body = copy.deepcopy(self.FAKE_SERVICE_RESPONSE)
+ resp_body['disabled_reason'] = "disabled for test"
+ resp_body['disabled'] = True
+ resp_body['status'] = 'disabled'
+ kwargs = copy.deepcopy(self.FAKE_SERVICE_REQUEST)
+ kwargs.update({"disabled_reason": "disabled for test"})
+ payload = json.dumps(kwargs, sort_keys=True)
+ json_dumps = json.dumps
+
+ # 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(services_client.json, 'dumps') as mock_dumps:
+ mock_dumps.side_effect = lambda d: json_dumps(d, sort_keys=True)
+
+ self.check_service_client_function(
+ self.client.disable_log_reason,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ resp_body,
+ to_utf=bytes_body,
+ mock_args=['os-services/disable-log-reason', payload],
+ **kwargs)
+
+ def _test_freeze_host(self, bytes_body=False):
+ kwargs = {'host': 'host1@lvm'}
+ self.check_service_client_function(
+ self.client.freeze_host,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ {},
+ bytes_body,
+ **kwargs)
+
+ def _test_thaw_host(self, bytes_body=False):
+ kwargs = {'host': 'host1@lvm'}
+ self.check_service_client_function(
+ self.client.thaw_host,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ {},
+ bytes_body,
+ **kwargs)
+
+ def test_list_services_with_str_body(self):
+ self._test_list_services()
+
+ def test_list_services_with_bytes_body(self):
+ self._test_list_services(bytes_body=True)
+
+ def test_list_services_with_params(self):
+ mock_args = 'os-services?host=fake-host'
+ self._test_list_services(mock_args=mock_args, host='fake-host')
+
+ def test_enable_service_with_str_body(self):
+ self._test_enable_service()
+
+ def test_enable_service_with_bytes_body(self):
+ self._test_enable_service(bytes_body=True)
+
+ def test_disable_service_with_str_body(self):
+ self._test_disable_service()
+
+ def test_disable_service_with_bytes_body(self):
+ self._test_disable_service(bytes_body=True)
+
+ def test_disable_log_reason_with_str_body(self):
+ self._test_disable_log_reason()
+
+ def test_disable_log_reason_with_bytes_body(self):
+ self._test_disable_log_reason(bytes_body=True)
+
+ def test_freeze_host_with_str_body(self):
+ self._test_freeze_host()
+
+ def test_freeze_host_with_bytes_body(self):
+ self._test_freeze_host(bytes_body=True)
+
+ def test_thaw_host_with_str_body(self):
+ self._test_thaw_host()
+
+ def test_thaw_host_with_bytes_body(self):
+ self._test_thaw_host(bytes_body=True)
diff --git a/tempest/tests/lib/services/volume/v3/test_user_messages_client.py b/tempest/tests/lib/services/volume/v3/test_user_messages_client.py
new file mode 100644
index 0000000..4aeed5f
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v3/test_user_messages_client.py
@@ -0,0 +1,92 @@
+# Copyright 2016 Red Hat. 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.services.volume.v3 import messages_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestUserMessagesClient(base.BaseServiceTest):
+ USER_MESSAGE_INFO = {
+ "created_at": "2016-11-21T06:16:34.000000",
+ "guaranteed_until": "2016-12-21T06:16:34.000000",
+ "user_message": "No storage could be allocated for this volume "
+ "request. You may be able to try another size or"
+ " volume type.",
+ "resource_uuid": "c570b406-bf0b-4067-9398-f0bb09a7d9d7",
+ "request_id": "req-8f68681e-9b6b-4009-b94c-ac0811595451",
+ "message_level": "ERROR",
+ "id": "9a7dafbd-a156-4540-8996-50e71b5dcadf",
+ "resource_type": "VOLUME",
+ "links": [
+ {"href": "http://192.168.100.230:8776/v3/"
+ "a678cb65f701462ea2257245cd640829/messages/"
+ "9a7dafbd-a156-4540-8996-50e71b5dcadf",
+ "rel": "self"},
+ {"href": "http://192.168.100.230:8776/"
+ "a678cb65f701462ea2257245cd640829/messages/"
+ "9a7dafbd-a156-4540-8996-50e71b5dcadf",
+ "rel": "bookmark"}]
+ }
+ FAKE_SHOW_USER_MESSAGE = {
+ "message": dict(event_id="000002", **USER_MESSAGE_INFO)}
+
+ FAKE_LIST_USER_MESSAGES = {
+ "messages": [
+ dict(event_id="000003", **USER_MESSAGE_INFO),
+ dict(event_id="000004", **USER_MESSAGE_INFO)
+ ]
+ }
+
+ def setUp(self):
+ super(TestUserMessagesClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = messages_client.MessagesClient(fake_auth,
+ 'volume',
+ 'regionOne')
+
+ def _test_show_user_message(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_message,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_SHOW_USER_MESSAGE,
+ bytes_body,
+ message_id="9a7dafbd-a156-4540-8996-50e71b5dcadf")
+
+ def _test_list_user_message(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_messages,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_USER_MESSAGES,
+ bytes_body)
+
+ def test_list_user_message_with_str_body(self):
+ self._test_list_user_message()
+
+ def test_list_user_message_with_bytes_body(self):
+ self._test_list_user_message(bytes_body=True)
+
+ def test_show_user_message_with_str_body(self):
+ self._test_show_user_message()
+
+ def test_show_user_message_with_bytes_body(self):
+ self._test_show_user_message(bytes_body=True)
+
+ def test_delete_user_message(self):
+ self.check_service_client_function(
+ self.client.delete_message,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ message_id="9a7dafbd-a156-4540-8996-50e71b5dcadf",
+ status=204)
diff --git a/tempest/tests/lib/services/volume/v3/test_versions_client.py b/tempest/tests/lib/services/volume/v3/test_versions_client.py
new file mode 100644
index 0000000..9627b9a
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v3/test_versions_client.py
@@ -0,0 +1,91 @@
+# Copyright 2017 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.services.volume.v3 import versions_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestVersionsClient(base.BaseServiceTest):
+
+ FAKE_VERSIONS_INFO = {
+ "versions": [
+ {
+ "status": "DEPRECATED", "updated": "2016-05-02T20:25:19Z",
+ "links": [
+ {"href": "http://docs.openstack.org/", "type": "text/html",
+ "rel": "describedby"},
+ {"href": "https://10.30.197.39:8776/v1/", "rel": "self"}
+ ],
+ "min_version": "",
+ "version": "",
+ "media-types": [
+ {"base": "application/json",
+ "type": "application/vnd.openstack.volume+json;version=1"}
+ ],
+ "id": "v1.0"
+ },
+ {
+ "status": "DEPRECATED", "updated": "2017-02-25T12:00:00Z",
+ "links": [
+ {"href": "http://docs.openstack.org/", "type": "text/html",
+ "rel": "describedby"},
+ {"href": "https://10.30.197.39:8776/v2/", "rel": "self"}
+ ],
+ "min_version": "",
+ "version": "",
+ "media-types": [
+ {"base": "application/json",
+ "type": "application/vnd.openstack.volume+json;version=1"}
+ ],
+ "id": "v2.0"
+ },
+ {
+ "status": "CURRENT", "updated": "2016-02-08T12:20:21Z",
+ "links": [
+ {"href": "http://docs.openstack.org/", "type": "text/html",
+ "rel": "describedby"},
+ {"href": "https://10.30.197.39:8776/v3/", "rel": "self"}
+ ],
+ "min_version": "3.0",
+ "version": "3.28",
+ "media-types": [
+ {"base": "application/json",
+ "type": "application/vnd.openstack.volume+json;version=1"}
+ ],
+ "id": "v3.0"
+ }
+ ]
+ }
+
+ def setUp(self):
+ super(TestVersionsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = versions_client.VersionsClient(fake_auth,
+ 'volume',
+ 'regionOne')
+
+ def _test_list_versions(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_versions,
+ 'tempest.lib.common.rest_client.RestClient.raw_request',
+ self.FAKE_VERSIONS_INFO,
+ bytes_body,
+ 300)
+
+ def test_list_versions_with_str_body(self):
+ self._test_list_versions()
+
+ def test_list_versions_with_bytes_body(self):
+ self._test_list_versions(bytes_body=True)
diff --git a/tempest/tests/lib/services/volume/v3/test_volumes_client.py b/tempest/tests/lib/services/volume/v3/test_volumes_client.py
new file mode 100644
index 0000000..a515fd3
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v3/test_volumes_client.py
@@ -0,0 +1,48 @@
+# 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 tempest.lib.services.volume.v3 import volumes_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestVolumesClient(base.BaseServiceTest):
+
+ FAKE_VOLUME_SUMMARY = {
+ "volume-summary": {
+ "total_size": 20,
+ "total_count": 5
+ }
+ }
+
+ def setUp(self):
+ super(TestVolumesClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = volumes_client.VolumesClient(fake_auth,
+ 'volume',
+ 'regionOne')
+
+ def _test_show_volume_summary(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_volume_summary,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_VOLUME_SUMMARY,
+ bytes_body)
+
+ def test_show_volume_summary_with_str_body(self):
+ self._test_show_volume_summary()
+
+ def test_show_volume_summary_with_bytes_body(self):
+ self._test_show_volume_summary(bytes_body=True)
diff --git a/tempest/tests/lib/test_auth.py b/tempest/tests/lib/test_auth.py
new file mode 100644
index 0000000..c3a792f
--- /dev/null
+++ b/tempest/tests/lib/test_auth.py
@@ -0,0 +1,915 @@
+# 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.
+
+import copy
+import datetime
+
+import fixtures
+import testtools
+
+from tempest.lib import auth
+from tempest.lib import exceptions
+from tempest.lib.services.identity.v2 import token_client as v2_client
+from tempest.lib.services.identity.v3 import token_client as v3_client
+from tempest.tests import base
+from tempest.tests.lib import fake_credentials
+from tempest.tests.lib import fake_identity
+
+
+def fake_get_credentials(fill_in=True, identity_version='v2', **kwargs):
+ return fake_credentials.FakeCredentials()
+
+
+class BaseAuthTestsSetUp(base.TestCase):
+ _auth_provider_class = None
+ credentials = fake_credentials.FakeCredentials()
+
+ def _auth(self, credentials, auth_url, **params):
+ """returns auth method according to keystone"""
+ return self._auth_provider_class(credentials, auth_url, **params)
+
+ def setUp(self):
+ super(BaseAuthTestsSetUp, self).setUp()
+ self.patchobject(auth, 'get_credentials', fake_get_credentials)
+ self.auth_provider = self._auth(self.credentials,
+ fake_identity.FAKE_AUTH_URL)
+
+
+class TestBaseAuthProvider(BaseAuthTestsSetUp):
+ """Tests for base AuthProvider
+
+ This tests auth.AuthProvider class which is base for the other so we
+ obviously don't test not implemented method or the ones which strongly
+ depends on them.
+ """
+
+ class FakeAuthProviderImpl(auth.AuthProvider):
+ def _decorate_request(self):
+ pass
+
+ def _fill_credentials(self):
+ pass
+
+ def _get_auth(self):
+ pass
+
+ def base_url(self):
+ pass
+
+ def is_expired(self):
+ pass
+
+ _auth_provider_class = FakeAuthProviderImpl
+
+ def _auth(self, credentials, auth_url, **params):
+ """returns auth method according to keystone"""
+ return self._auth_provider_class(credentials, **params)
+
+ def test_check_credentials_bad_type(self):
+ self.assertFalse(self.auth_provider.check_credentials([]))
+
+ def test_auth_data_property_when_cache_exists(self):
+ self.auth_provider.cache = 'foo'
+ self.useFixture(fixtures.MockPatchObject(self.auth_provider,
+ 'is_expired',
+ return_value=False))
+ self.assertEqual('foo', getattr(self.auth_provider, 'auth_data'))
+
+ def test_delete_auth_data_property_through_deleter(self):
+ self.auth_provider.cache = 'foo'
+ del self.auth_provider.auth_data
+ self.assertIsNone(self.auth_provider.cache)
+
+ def test_delete_auth_data_property_through_clear_auth(self):
+ self.auth_provider.cache = 'foo'
+ self.auth_provider.clear_auth()
+ self.assertIsNone(self.auth_provider.cache)
+
+ def test_set_and_reset_alt_auth_data(self):
+ self.auth_provider.set_alt_auth_data('foo', 'bar')
+ self.assertEqual(self.auth_provider.alt_part, 'foo')
+ self.assertEqual(self.auth_provider.alt_auth_data, 'bar')
+
+ self.auth_provider.reset_alt_auth_data()
+ self.assertIsNone(self.auth_provider.alt_part)
+ self.assertIsNone(self.auth_provider.alt_auth_data)
+
+ def test_auth_class(self):
+ self.assertRaises(TypeError,
+ auth.AuthProvider,
+ fake_credentials.FakeCredentials)
+
+
+class TestKeystoneV2AuthProvider(BaseAuthTestsSetUp):
+ _endpoints = fake_identity.IDENTITY_V2_RESPONSE['access']['serviceCatalog']
+ _auth_provider_class = auth.KeystoneV2AuthProvider
+ credentials = fake_credentials.FakeKeystoneV2Credentials()
+
+ def setUp(self):
+ super(TestKeystoneV2AuthProvider, self).setUp()
+ self.patchobject(v2_client.TokenClient, 'raw_request',
+ fake_identity._fake_v2_response)
+ self.target_url = 'test_api'
+
+ def _get_fake_identity(self):
+ return fake_identity.IDENTITY_V2_RESPONSE['access']
+
+ def _get_fake_alt_identity(self):
+ return fake_identity.ALT_IDENTITY_V2_RESPONSE['access']
+
+ def _get_result_url_from_endpoint(self, ep, endpoint_type='publicURL',
+ replacement=None):
+ if replacement:
+ return ep[endpoint_type].replace('v2', replacement)
+ return ep[endpoint_type]
+
+ def _get_token_from_fake_identity(self):
+ return fake_identity.TOKEN
+
+ def _get_from_fake_identity(self, attr):
+ access = fake_identity.IDENTITY_V2_RESPONSE['access']
+ if attr == 'user_id':
+ return access['user']['id']
+ elif attr == 'tenant_id':
+ return access['token']['tenant']['id']
+
+ def _test_request_helper(self, filters, expected):
+ url, headers, body = self.auth_provider.auth_request('GET',
+ self.target_url,
+ filters=filters)
+
+ self.assertEqual(expected['url'], url)
+ self.assertEqual(expected['token'], headers['X-Auth-Token'])
+ self.assertEqual(expected['body'], body)
+
+ def _auth_data_with_expiry(self, date_as_string):
+ token, access = self.auth_provider.auth_data
+ access['token']['expires'] = date_as_string
+ return token, access
+
+ def test_request(self):
+ filters = {
+ 'service': 'compute',
+ 'endpoint_type': 'publicURL',
+ 'region': 'FakeRegion'
+ }
+
+ url = self._get_result_url_from_endpoint(
+ self._endpoints[0]['endpoints'][1]) + '/' + self.target_url
+
+ expected = {
+ 'body': None,
+ 'url': url,
+ 'token': self._get_token_from_fake_identity(),
+ }
+ self._test_request_helper(filters, expected)
+
+ def test_request_with_alt_auth_cleans_alt(self):
+ """Test alternate auth data for headers
+
+ Assert that when the alt data is provided for headers, after an
+ auth_request the data alt_data is cleaned-up.
+ """
+ self.auth_provider.set_alt_auth_data(
+ 'headers',
+ (fake_identity.ALT_TOKEN, self._get_fake_alt_identity()))
+ filters = {
+ 'service': 'compute',
+ 'endpoint_type': 'publicURL',
+ 'region': 'fakeRegion'
+ }
+ self.auth_provider.auth_request('GET', self.target_url,
+ filters=filters)
+
+ # Assert alt auth data is clear after it
+ self.assertIsNone(self.auth_provider.alt_part)
+ self.assertIsNone(self.auth_provider.alt_auth_data)
+
+ def _test_request_with_identical_alt_auth(self, part):
+ """Test alternate but identical auth data for headers
+
+ Assert that when the alt data is provided, but it's actually
+ identical, an exception is raised.
+ """
+ self.auth_provider.set_alt_auth_data(
+ part,
+ (fake_identity.TOKEN, self._get_fake_identity()))
+ filters = {
+ 'service': 'compute',
+ 'endpoint_type': 'publicURL',
+ 'region': 'fakeRegion'
+ }
+
+ self.assertRaises(exceptions.BadAltAuth,
+ self.auth_provider.auth_request,
+ 'GET', self.target_url, filters=filters)
+
+ def test_request_with_identical_alt_auth_headers(self):
+ self._test_request_with_identical_alt_auth('headers')
+
+ def test_request_with_identical_alt_auth_url(self):
+ self._test_request_with_identical_alt_auth('url')
+
+ def test_request_with_identical_alt_auth_body(self):
+ self._test_request_with_identical_alt_auth('body')
+
+ def test_request_with_alt_part_without_alt_data(self):
+ """Test empty alternate auth data
+
+ Assert that when alt_part is defined, the corresponding original
+ request element is kept the same.
+ """
+ filters = {
+ 'service': 'compute',
+ 'endpoint_type': 'publicURL',
+ 'region': 'fakeRegion'
+ }
+ self.auth_provider.set_alt_auth_data('headers', None)
+
+ url, headers, body = self.auth_provider.auth_request('GET',
+ self.target_url,
+ filters=filters)
+ # The original headers where empty
+ self.assertNotEqual(url, self.target_url)
+ self.assertIsNone(headers)
+ self.assertIsNone(body)
+
+ def _test_request_with_alt_part_without_alt_data_no_change(self, body):
+ """Test empty alternate auth data with no effect
+
+ Assert that when alt_part is defined, no auth_data is provided,
+ and the corresponding original request element was not going to
+ be changed anyways, and exception is raised
+ """
+ filters = {
+ 'service': 'compute',
+ 'endpoint_type': 'publicURL',
+ 'region': 'fakeRegion'
+ }
+ self.auth_provider.set_alt_auth_data('body', None)
+
+ self.assertRaises(exceptions.BadAltAuth,
+ self.auth_provider.auth_request,
+ 'GET', self.target_url, filters=filters)
+
+ def test_request_with_alt_part_without_alt_data_no_change_headers(self):
+ self._test_request_with_alt_part_without_alt_data_no_change('headers')
+
+ def test_request_with_alt_part_without_alt_data_no_change_url(self):
+ self._test_request_with_alt_part_without_alt_data_no_change('url')
+
+ def test_request_with_alt_part_without_alt_data_no_change_body(self):
+ self._test_request_with_alt_part_without_alt_data_no_change('body')
+
+ def test_request_with_bad_service(self):
+ filters = {
+ 'service': 'BAD_SERVICE',
+ 'endpoint_type': 'publicURL',
+ 'region': 'fakeRegion'
+ }
+ self.assertRaises(exceptions.EndpointNotFound,
+ self.auth_provider.auth_request, 'GET',
+ self.target_url, filters=filters)
+
+ def test_request_without_service(self):
+ filters = {
+ 'service': None,
+ 'endpoint_type': 'publicURL',
+ 'region': 'fakeRegion'
+ }
+ self.assertRaises(exceptions.EndpointNotFound,
+ self.auth_provider.auth_request, 'GET',
+ self.target_url, filters=filters)
+
+ def test_check_credentials_missing_attribute(self):
+ for attr in ['username', 'password']:
+ cred = copy.copy(self.credentials)
+ del cred[attr]
+ self.assertFalse(self.auth_provider.check_credentials(cred))
+
+ def test_fill_credentials(self):
+ self.auth_provider.fill_credentials()
+ creds = self.auth_provider.credentials
+ for attr in ['user_id', 'tenant_id']:
+ self.assertEqual(self._get_from_fake_identity(attr),
+ getattr(creds, attr))
+
+ def _test_base_url_helper(self, expected_url, filters,
+ auth_data=None):
+
+ url = self.auth_provider.base_url(filters, auth_data)
+ self.assertEqual(url, expected_url)
+
+ def test_base_url(self):
+ self.filters = {
+ 'service': 'compute',
+ 'endpoint_type': 'publicURL',
+ 'region': 'FakeRegion'
+ }
+ expected = self._get_result_url_from_endpoint(
+ self._endpoints[0]['endpoints'][1])
+ self._test_base_url_helper(expected, self.filters)
+
+ def test_base_url_to_get_admin_endpoint(self):
+ self.filters = {
+ 'service': 'compute',
+ 'endpoint_type': 'adminURL',
+ 'region': 'FakeRegion'
+ }
+ expected = self._get_result_url_from_endpoint(
+ self._endpoints[0]['endpoints'][1], endpoint_type='adminURL')
+ self._test_base_url_helper(expected, self.filters)
+
+ def test_base_url_unknown_region(self):
+ """If the region is unknown, the first endpoint is returned."""
+ self.filters = {
+ 'service': 'compute',
+ 'endpoint_type': 'publicURL',
+ 'region': 'AintNoBodyKnowThisRegion'
+ }
+ expected = self._get_result_url_from_endpoint(
+ self._endpoints[0]['endpoints'][0])
+ self._test_base_url_helper(expected, self.filters)
+
+ def test_base_url_with_non_existent_service(self):
+ self.filters = {
+ 'service': 'BAD_SERVICE',
+ 'endpoint_type': 'publicURL',
+ 'region': 'FakeRegion'
+ }
+ self.assertRaises(exceptions.EndpointNotFound,
+ self._test_base_url_helper, None, self.filters)
+
+ def test_base_url_without_service(self):
+ self.filters = {
+ 'endpoint_type': 'publicURL',
+ 'region': 'FakeRegion'
+ }
+ self.assertRaises(exceptions.EndpointNotFound,
+ self._test_base_url_helper, None, self.filters)
+
+ def test_base_url_with_known_name(self):
+ """If name and service is known, return the endpoint."""
+ self.filters = {
+ 'service': 'compute',
+ 'endpoint_type': 'publicURL',
+ 'region': 'FakeRegion',
+ 'name': 'nova'
+ }
+ expected = self._get_result_url_from_endpoint(
+ self._endpoints[0]['endpoints'][1])
+ self._test_base_url_helper(expected, self.filters)
+
+ def test_base_url_with_known_name_and_unknown_servce(self):
+ """Test with Known Name and Unknown service
+
+ If the name is known but the service is unknown, raise an exception.
+ """
+ self.filters = {
+ 'service': 'AintNoBodyKnowThatService',
+ 'endpoint_type': 'publicURL',
+ 'region': 'FakeRegion',
+ 'name': 'AintNoBodyKnowThatName'
+ }
+ self.assertRaises(exceptions.EndpointNotFound,
+ self._test_base_url_helper, None, self.filters)
+
+ def test_base_url_with_unknown_name_and_known_service(self):
+ """Test with Unknown Name and Known Service
+
+ If the name is unknown, raise an exception. Note that filtering by
+ name is only successful service exists.
+ """
+
+ self.filters = {
+ 'service': 'compute',
+ 'endpoint_type': 'publicURL',
+ 'region': 'FakeRegion',
+ 'name': 'AintNoBodyKnowThatName'
+ }
+ self.assertRaises(exceptions.EndpointNotFound,
+ self._test_base_url_helper, None, self.filters)
+
+ def test_base_url_without_name(self):
+ self.filters = {
+ 'service': 'compute',
+ 'endpoint_type': 'publicURL',
+ 'region': 'FakeRegion',
+ }
+ expected = self._get_result_url_from_endpoint(
+ self._endpoints[0]['endpoints'][1])
+ self._test_base_url_helper(expected, self.filters)
+
+ def test_base_url_with_api_version_filter(self):
+ self.filters = {
+ 'service': 'compute',
+ 'endpoint_type': 'publicURL',
+ 'region': 'FakeRegion',
+ 'api_version': 'v12'
+ }
+ expected = self._get_result_url_from_endpoint(
+ self._endpoints[0]['endpoints'][1], replacement='v12')
+ self._test_base_url_helper(expected, self.filters)
+
+ def test_base_url_with_skip_path_filter(self):
+ self.filters = {
+ 'service': 'compute',
+ 'endpoint_type': 'publicURL',
+ 'region': 'FakeRegion',
+ 'skip_path': True
+ }
+ expected = 'http://fake_url/'
+ self._test_base_url_helper(expected, self.filters)
+
+ def test_base_url_with_unversioned_endpoint(self):
+ auth_data = {
+ 'serviceCatalog': [
+ {
+ 'type': 'identity',
+ 'endpoints': [
+ {
+ 'region': 'FakeRegion',
+ 'publicURL': 'http://fake_url'
+ }
+ ]
+ }
+ ]
+ }
+
+ filters = {
+ 'service': 'identity',
+ 'endpoint_type': 'publicURL',
+ 'region': 'FakeRegion',
+ 'api_version': 'v2.0'
+ }
+
+ expected = 'http://fake_url/v2.0'
+ self._test_base_url_helper(expected, filters, ('token', auth_data))
+
+ def test_base_url_with_extra_path_endpoint(self):
+ auth_data = {
+ 'serviceCatalog': [
+ {
+ 'type': 'compute',
+ 'endpoints': [
+ {
+ 'region': 'FakeRegion',
+ 'publicURL': 'http://fake_url/some_path/v2.0'
+ }
+ ]
+ }
+ ]
+ }
+
+ filters = {
+ 'service': 'compute',
+ 'endpoint_type': 'publicURL',
+ 'region': 'FakeRegion',
+ 'api_version': 'v2.0'
+ }
+
+ expected = 'http://fake_url/some_path/v2.0'
+ self._test_base_url_helper(expected, filters, ('token', auth_data))
+
+ def test_base_url_with_unversioned_extra_path_endpoint(self):
+ auth_data = {
+ 'serviceCatalog': [
+ {
+ 'type': 'compute',
+ 'endpoints': [
+ {
+ 'region': 'FakeRegion',
+ 'publicURL': 'http://fake_url/some_path'
+ }
+ ]
+ }
+ ]
+ }
+
+ filters = {
+ 'service': 'compute',
+ 'endpoint_type': 'publicURL',
+ 'region': 'FakeRegion',
+ 'api_version': 'v2.0'
+ }
+
+ expected = 'http://fake_url/some_path/v2.0'
+ self._test_base_url_helper(expected, filters, ('token', auth_data))
+
+ def test_token_not_expired(self):
+ expiry_data = datetime.datetime.utcnow() + datetime.timedelta(days=1)
+ self._verify_expiry(expiry_data=expiry_data, should_be_expired=False)
+
+ def test_token_expired(self):
+ expiry_data = datetime.datetime.utcnow() - datetime.timedelta(hours=1)
+ self._verify_expiry(expiry_data=expiry_data, should_be_expired=True)
+
+ def test_token_not_expired_to_be_renewed(self):
+ expiry_data = (datetime.datetime.utcnow() +
+ self.auth_provider.token_expiry_threshold / 2)
+ self._verify_expiry(expiry_data=expiry_data, should_be_expired=True)
+
+ def _verify_expiry(self, expiry_data, should_be_expired):
+ for expiry_format in self.auth_provider.EXPIRY_DATE_FORMATS:
+ auth_data = self._auth_data_with_expiry(
+ expiry_data.strftime(expiry_format))
+ self.assertEqual(self.auth_provider.is_expired(auth_data),
+ should_be_expired)
+
+ def test_set_scope_all_valid(self):
+ for scope in self.auth_provider.SCOPES:
+ self.auth_provider.scope = scope
+ self.assertEqual(scope, self.auth_provider.scope)
+
+ def test_set_scope_invalid(self):
+ with testtools.ExpectedException(exceptions.InvalidScope,
+ '.* invalid_scope .*'):
+ self.auth_provider.scope = 'invalid_scope'
+
+
+class TestKeystoneV3AuthProvider(TestKeystoneV2AuthProvider):
+ _endpoints = fake_identity.IDENTITY_V3_RESPONSE['token']['catalog']
+ _auth_provider_class = auth.KeystoneV3AuthProvider
+ credentials = fake_credentials.FakeKeystoneV3Credentials()
+
+ def setUp(self):
+ super(TestKeystoneV3AuthProvider, self).setUp()
+ self.patchobject(v3_client.V3TokenClient, 'raw_request',
+ fake_identity._fake_v3_response)
+
+ def _get_fake_identity(self):
+ return fake_identity.IDENTITY_V3_RESPONSE['token']
+
+ def _get_fake_alt_identity(self):
+ return fake_identity.ALT_IDENTITY_V3['token']
+
+ def _get_result_url_from_endpoint(self, ep, replacement=None):
+ if replacement:
+ return ep['url'].replace('v3', replacement)
+ return ep['url']
+
+ def _auth_data_with_expiry(self, date_as_string):
+ token, access = self.auth_provider.auth_data
+ access['expires_at'] = date_as_string
+ return token, access
+
+ def _get_from_fake_identity(self, attr):
+ token = fake_identity.IDENTITY_V3_RESPONSE['token']
+ if attr == 'user_id':
+ return token['user']['id']
+ elif attr == 'project_id':
+ return token['project']['id']
+ elif attr == 'user_domain_id':
+ return token['user']['domain']['id']
+ elif attr == 'project_domain_id':
+ return token['project']['domain']['id']
+
+ def test_check_credentials_missing_attribute(self):
+ # reset credentials to fresh ones
+ self.credentials.reset()
+ for attr in ['username', 'password', 'user_domain_name',
+ 'project_domain_name']:
+ cred = copy.copy(self.credentials)
+ del cred[attr]
+ self.assertFalse(self.auth_provider.check_credentials(cred),
+ "Credentials should be invalid without %s" % attr)
+
+ def test_check_domain_credentials_missing_attribute(self):
+ # reset credentials to fresh ones
+ self.credentials.reset()
+ domain_creds = fake_credentials.FakeKeystoneV3DomainCredentials()
+ for attr in ['username', 'password', 'user_domain_name']:
+ cred = copy.copy(domain_creds)
+ del cred[attr]
+ self.assertFalse(self.auth_provider.check_credentials(cred),
+ "Credentials should be invalid without %s" % attr)
+
+ def test_fill_credentials(self):
+ self.auth_provider.fill_credentials()
+ creds = self.auth_provider.credentials
+ for attr in ['user_id', 'project_id', 'user_domain_id',
+ 'project_domain_id']:
+ self.assertEqual(self._get_from_fake_identity(attr),
+ getattr(creds, attr))
+
+ # Overwrites v2 test
+ def test_base_url_to_get_admin_endpoint(self):
+ self.filters = {
+ 'service': 'compute',
+ 'endpoint_type': 'admin',
+ 'region': 'MiddleEarthRegion'
+ }
+ expected = self._get_result_url_from_endpoint(
+ self._endpoints[0]['endpoints'][2])
+ self._test_base_url_helper(expected, self.filters)
+
+ # Overwrites v2 test
+ def test_base_url_with_unversioned_endpoint(self):
+ auth_data = {
+ 'catalog': [
+ {
+ 'type': 'identity',
+ 'endpoints': [
+ {
+ 'region': 'FakeRegion',
+ 'url': 'http://fake_url',
+ 'interface': 'public'
+ }
+ ]
+ }
+ ]
+ }
+
+ filters = {
+ 'service': 'identity',
+ 'endpoint_type': 'publicURL',
+ 'region': 'FakeRegion',
+ 'api_version': 'v3'
+ }
+
+ expected = 'http://fake_url/v3'
+ self._test_base_url_helper(expected, filters, ('token', auth_data))
+
+ def test_base_url_with_extra_path_endpoint(self):
+ auth_data = {
+ 'catalog': [
+ {
+ 'type': 'compute',
+ 'endpoints': [
+ {
+ 'region': 'FakeRegion',
+ 'url': 'http://fake_url/some_path/v2.0',
+ 'interface': 'public'
+ }
+ ]
+ }
+ ]
+ }
+
+ filters = {
+ 'service': 'compute',
+ 'endpoint_type': 'publicURL',
+ 'region': 'FakeRegion',
+ 'api_version': 'v2.0'
+ }
+
+ expected = 'http://fake_url/some_path/v2.0'
+ self._test_base_url_helper(expected, filters, ('token', auth_data))
+
+ def test_base_url_with_unversioned_extra_path_endpoint(self):
+ auth_data = {
+ 'catalog': [
+ {
+ 'type': 'compute',
+ 'endpoints': [
+ {
+ 'region': 'FakeRegion',
+ 'url': 'http://fake_url/some_path',
+ 'interface': 'public'
+ }
+ ]
+ }
+ ]
+ }
+
+ filters = {
+ 'service': 'compute',
+ 'endpoint_type': 'publicURL',
+ 'region': 'FakeRegion',
+ 'api_version': 'v2.0'
+ }
+
+ expected = 'http://fake_url/some_path/v2.0'
+ self._test_base_url_helper(expected, filters, ('token', auth_data))
+
+ # Base URL test with scope only for V3
+ def test_base_url_scope_project(self):
+ self.auth_provider.scope = 'project'
+ self.filters = {
+ 'service': 'compute',
+ 'endpoint_type': 'publicURL',
+ 'region': 'FakeRegion'
+ }
+ expected = self._get_result_url_from_endpoint(
+ self._endpoints[0]['endpoints'][1])
+ self._test_base_url_helper(expected, self.filters)
+
+ # Base URL test with scope only for V3
+ def test_base_url_unscoped_identity(self):
+ self.auth_provider.scope = 'unscoped'
+ self.patchobject(v3_client.V3TokenClient, 'raw_request',
+ fake_identity._fake_v3_response_no_scope)
+ self.filters = {
+ 'service': 'identity',
+ 'endpoint_type': 'publicURL',
+ 'region': 'FakeRegion'
+ }
+ expected = fake_identity.FAKE_AUTH_URL
+ self._test_base_url_helper(expected, self.filters)
+
+ # Base URL test with scope only for V3
+ def test_base_url_unscoped_other(self):
+ self.auth_provider.scope = 'unscoped'
+ self.patchobject(v3_client.V3TokenClient, 'raw_request',
+ fake_identity._fake_v3_response_no_scope)
+ self.filters = {
+ 'service': 'compute',
+ 'endpoint_type': 'publicURL',
+ 'region': 'FakeRegion'
+ }
+ self.assertRaises(exceptions.EndpointNotFound,
+ self.auth_provider.base_url,
+ auth_data=self.auth_provider.auth_data,
+ filters=self.filters)
+
+ def test_auth_parameters_with_scope_unset(self):
+ # No scope defaults to 'project'
+ all_creds = fake_credentials.FakeKeystoneV3AllCredentials()
+ self.auth_provider.credentials = all_creds
+ auth_params = self.auth_provider._auth_params()
+ self.assertNotIn('scope', auth_params.keys())
+ for attr in all_creds.get_init_attributes():
+ if attr.startswith('domain_'):
+ self.assertNotIn(attr, auth_params.keys())
+ else:
+ self.assertIn(attr, auth_params.keys())
+ self.assertEqual(getattr(all_creds, attr), auth_params[attr])
+
+ def test_auth_parameters_with_project_scope(self):
+ all_creds = fake_credentials.FakeKeystoneV3AllCredentials()
+ self.auth_provider.credentials = all_creds
+ self.auth_provider.scope = 'project'
+ auth_params = self.auth_provider._auth_params()
+ self.assertNotIn('scope', auth_params.keys())
+ for attr in all_creds.get_init_attributes():
+ if attr.startswith('domain_'):
+ self.assertNotIn(attr, auth_params.keys())
+ else:
+ self.assertIn(attr, auth_params.keys())
+ self.assertEqual(getattr(all_creds, attr), auth_params[attr])
+
+ def test_auth_parameters_with_domain_scope(self):
+ all_creds = fake_credentials.FakeKeystoneV3AllCredentials()
+ self.auth_provider.credentials = all_creds
+ self.auth_provider.scope = 'domain'
+ auth_params = self.auth_provider._auth_params()
+ self.assertNotIn('scope', auth_params.keys())
+ for attr in all_creds.get_init_attributes():
+ if attr.startswith('project_'):
+ self.assertNotIn(attr, auth_params.keys())
+ else:
+ self.assertIn(attr, auth_params.keys())
+ self.assertEqual(getattr(all_creds, attr), auth_params[attr])
+
+ def test_auth_parameters_unscoped(self):
+ all_creds = fake_credentials.FakeKeystoneV3AllCredentials()
+ self.auth_provider.credentials = all_creds
+ self.auth_provider.scope = 'unscoped'
+ auth_params = self.auth_provider._auth_params()
+ self.assertNotIn('scope', auth_params.keys())
+ for attr in all_creds.get_init_attributes():
+ if attr.startswith('project_') or attr.startswith('domain_'):
+ self.assertNotIn(attr, auth_params.keys())
+ else:
+ self.assertIn(attr, auth_params.keys())
+ self.assertEqual(getattr(all_creds, attr), auth_params[attr])
+
+
+class TestKeystoneV3Credentials(base.TestCase):
+ def testSetAttrUserDomain(self):
+ creds = auth.KeystoneV3Credentials()
+ creds.user_domain_name = 'user_domain'
+ creds.domain_name = 'domain'
+ self.assertEqual('user_domain', creds.user_domain_name)
+ creds = auth.KeystoneV3Credentials()
+ creds.domain_name = 'domain'
+ creds.user_domain_name = 'user_domain'
+ self.assertEqual('user_domain', creds.user_domain_name)
+
+ def testSetAttrProjectDomain(self):
+ creds = auth.KeystoneV3Credentials()
+ creds.project_domain_name = 'project_domain'
+ creds.domain_name = 'domain'
+ self.assertEqual('project_domain', creds.user_domain_name)
+ creds = auth.KeystoneV3Credentials()
+ creds.domain_name = 'domain'
+ creds.project_domain_name = 'project_domain'
+ self.assertEqual('project_domain', creds.project_domain_name)
+
+ def testProjectTenantNoCollision(self):
+ creds = auth.KeystoneV3Credentials(tenant_id='tenant')
+ self.assertEqual('tenant', creds.project_id)
+ creds = auth.KeystoneV3Credentials(project_id='project')
+ self.assertEqual('project', creds.tenant_id)
+ creds = auth.KeystoneV3Credentials(tenant_name='tenant')
+ self.assertEqual('tenant', creds.project_name)
+ creds = auth.KeystoneV3Credentials(project_name='project')
+ self.assertEqual('project', creds.tenant_name)
+
+ def testProjectTenantCollision(self):
+ attrs = {'tenant_id': 'tenant', 'project_id': 'project'}
+ self.assertRaises(
+ exceptions.InvalidCredentials, auth.KeystoneV3Credentials, **attrs)
+ attrs = {'tenant_name': 'tenant', 'project_name': 'project'}
+ self.assertRaises(
+ exceptions.InvalidCredentials, auth.KeystoneV3Credentials, **attrs)
+
+
+class TestReplaceVersion(base.TestCase):
+ def test_version_no_trailing_path(self):
+ self.assertEqual(
+ 'http://localhost:35357/v2.0',
+ auth.replace_version('http://localhost:35357/v3', 'v2.0'))
+
+ def test_version_no_trailing_path_solidus(self):
+ self.assertEqual(
+ 'http://localhost:35357/v2.0/',
+ auth.replace_version('http://localhost:35357/v3/', 'v2.0'))
+
+ def test_version_trailing_path(self):
+ self.assertEqual(
+ 'http://localhost:35357/v2.0/uuid',
+ auth.replace_version('http://localhost:35357/v3/uuid', 'v2.0'))
+
+ def test_version_trailing_path_solidus(self):
+ self.assertEqual(
+ 'http://localhost:35357/v2.0/uuid/',
+ auth.replace_version('http://localhost:35357/v3/uuid/', 'v2.0'))
+
+ def test_no_version_base(self):
+ self.assertEqual(
+ 'http://localhost:35357/v2.0',
+ auth.replace_version('http://localhost:35357', 'v2.0'))
+
+ def test_no_version_base_solidus(self):
+ self.assertEqual(
+ 'http://localhost:35357/v2.0',
+ auth.replace_version('http://localhost:35357/', 'v2.0'))
+
+ def test_no_version_path(self):
+ self.assertEqual(
+ 'http://localhost/identity/v2.0',
+ auth.replace_version('http://localhost/identity', 'v2.0'))
+
+ def test_no_version_path_solidus(self):
+ self.assertEqual(
+ 'http://localhost/identity/v2.0',
+ auth.replace_version('http://localhost/identity/', 'v2.0'))
+
+ def test_path_version(self):
+ self.assertEqual(
+ 'http://localhost/identity/v2.0',
+ auth.replace_version('http://localhost/identity/v3', 'v2.0'))
+
+ def test_path_version_solidus(self):
+ self.assertEqual(
+ 'http://localhost/identity/v2.0/',
+ auth.replace_version('http://localhost/identity/v3/', 'v2.0'))
+
+ def test_path_version_trailing_path(self):
+ self.assertEqual(
+ 'http://localhost/identity/v2.0/uuid',
+ auth.replace_version('http://localhost/identity/v3/uuid', 'v2.0'))
+
+ def test_path_version_trailing_path_solidus(self):
+ self.assertEqual(
+ 'http://localhost/identity/v2.0/uuid/',
+ auth.replace_version('http://localhost/identity/v3/uuid/', 'v2.0'))
+
+
+class TestKeystoneV3AuthProvider_DomainScope(BaseAuthTestsSetUp):
+ _endpoints = fake_identity.IDENTITY_V3_RESPONSE['token']['catalog']
+ _auth_provider_class = auth.KeystoneV3AuthProvider
+ credentials = fake_credentials.FakeKeystoneV3Credentials()
+
+ def setUp(self):
+ super(TestKeystoneV3AuthProvider_DomainScope, self).setUp()
+ self.patchobject(v3_client.V3TokenClient, 'raw_request',
+ fake_identity._fake_v3_response_domain_scope)
+
+ def test_get_auth_with_domain_scope(self):
+ self.auth_provider.scope = 'domain'
+ _, auth_data = self.auth_provider.get_auth()
+ self.assertIn('domain', auth_data)
+ self.assertNotIn('project', auth_data)
+
+
+class TestGetCredentials(base.TestCase):
+
+ def test_invalid_identity_version(self):
+ with testtools.ExpectedException(exceptions.InvalidIdentityVersion,
+ '.* v1 .*'):
+ auth.get_credentials('http://localhost/identity/v3',
+ identity_version='v1')
diff --git a/tempest/tests/lib/test_base.py b/tempest/tests/lib/test_base.py
new file mode 100644
index 0000000..27cda1a
--- /dev/null
+++ b/tempest/tests/lib/test_base.py
@@ -0,0 +1,64 @@
+# Copyright 2014 Mirantis Inc.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import testtools
+
+from tempest.lib import base
+from tempest.lib import exceptions
+
+
+class TestAttr(base.BaseTestCase):
+
+ def test_has_no_attrs(self):
+ self.assertEqual(
+ 'tempest.tests.lib.test_base.TestAttr.test_has_no_attrs',
+ self.id()
+ )
+
+ @testtools.testcase.attr('foo')
+ def test_has_one_attr(self):
+ self.assertEqual(
+ 'tempest.tests.lib.test_base.TestAttr.test_has_one_attr[foo]',
+ self.id()
+ )
+
+ @testtools.testcase.attr('foo')
+ @testtools.testcase.attr('bar')
+ def test_has_two_attrs(self):
+ self.assertEqual(
+ 'tempest.tests.lib.test_base.TestAttr.test_has_two_attrs[bar,foo]',
+ self.id(),
+ )
+
+
+class TestSetUpClass(base.BaseTestCase):
+
+ @classmethod
+ def setUpClass(cls): # noqa
+ """Simulate absence of super() call."""
+
+ def setUp(self):
+ try:
+ # We expect here RuntimeError exception because 'setUpClass'
+ # has not called 'super'.
+ super(TestSetUpClass, self).setUp()
+ except RuntimeError:
+ pass
+ else:
+ raise exceptions.TempestException(
+ "If you see this, then expected exception was not raised.")
+
+ def test_setup_class_raises_runtime_error(self):
+ """No-op test just to call setUp."""
diff --git a/tempest/tests/lib/test_credentials.py b/tempest/tests/lib/test_credentials.py
new file mode 100644
index 0000000..c910d6d
--- /dev/null
+++ b/tempest/tests/lib/test_credentials.py
@@ -0,0 +1,182 @@
+# Copyright 2014 Hewlett-Packard Development Company, L.P.
+# 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 copy
+
+from tempest.lib import auth
+from tempest.lib import exceptions
+from tempest.lib.services.identity.v2 import token_client as v2_client
+from tempest.lib.services.identity.v3 import token_client as v3_client
+from tempest.tests import base
+from tempest.tests.lib import fake_identity
+
+
+class CredentialsTests(base.TestCase):
+ attributes = {}
+ credentials_class = auth.Credentials
+
+ def _get_credentials(self, attributes=None):
+ if attributes is None:
+ attributes = self.attributes
+ return self.credentials_class(**attributes)
+
+ def _check(self, credentials, credentials_class, filled):
+ # Check the right version of credentials has been returned
+ self.assertIsInstance(credentials, credentials_class)
+ # Check the id attributes are filled in
+ # NOTE(andreaf) project_* attributes are accepted as input but
+ # never set on the credentials object
+ attributes = [x for x in credentials.ATTRIBUTES if (
+ '_id' in x and x != 'domain_id' and x != 'project_id')]
+ for attr in attributes:
+ if filled:
+ self.assertIsNotNone(getattr(credentials, attr))
+ else:
+ self.assertIsNone(getattr(credentials, attr))
+
+ def test_create(self):
+ creds = self._get_credentials()
+ self.assertEqual(self.attributes, creds._initial)
+
+ def test_create_invalid_attr(self):
+ self.assertRaises(exceptions.InvalidCredentials,
+ self._get_credentials,
+ attributes=dict(invalid='fake'))
+
+ def test_is_valid(self):
+ creds = self._get_credentials()
+ self.assertRaises(NotImplementedError, creds.is_valid)
+
+
+class KeystoneV2CredentialsTests(CredentialsTests):
+ attributes = {
+ 'username': 'fake_username',
+ 'password': 'fake_password',
+ 'tenant_name': 'fake_tenant_name'
+ }
+
+ identity_response = fake_identity._fake_v2_response
+ credentials_class = auth.KeystoneV2Credentials
+ tokenclient_class = v2_client.TokenClient
+ identity_version = 'v2'
+
+ def setUp(self):
+ super(KeystoneV2CredentialsTests, self).setUp()
+ self.patchobject(self.tokenclient_class, 'raw_request',
+ self.identity_response)
+
+ def _verify_credentials(self, credentials_class, creds_dict, filled=True):
+ creds = auth.get_credentials(fake_identity.FAKE_AUTH_URL,
+ fill_in=filled,
+ identity_version=self.identity_version,
+ **creds_dict)
+ self._check(creds, credentials_class, filled)
+
+ def test_get_credentials(self):
+ self._verify_credentials(credentials_class=self.credentials_class,
+ creds_dict=self.attributes)
+
+ def test_get_credentials_not_filled(self):
+ self._verify_credentials(credentials_class=self.credentials_class,
+ creds_dict=self.attributes,
+ filled=False)
+
+ def test_is_valid(self):
+ creds = self._get_credentials()
+ self.assertTrue(creds.is_valid())
+
+ def _test_is_not_valid(self, ignore_key):
+ creds = self._get_credentials()
+ for attr in self.attributes:
+ if attr == ignore_key:
+ continue
+ temp_attr = getattr(creds, attr)
+ delattr(creds, attr)
+ self.assertFalse(creds.is_valid(),
+ "Credentials should be invalid without %s" % attr)
+ setattr(creds, attr, temp_attr)
+
+ def test_is_not_valid(self):
+ # NOTE(mtreinish): A KeystoneV2 credential object is valid without
+ # a tenant_name. So skip that check. See tempest.auth for the valid
+ # credential requirements
+ self._test_is_not_valid('tenant_name')
+
+ def test_reset_all_attributes(self):
+ creds = self._get_credentials()
+ initial_creds = copy.deepcopy(creds)
+ set_attr = creds.__dict__.keys()
+ missing_attr = set(creds.ATTRIBUTES).difference(set_attr)
+ # Set all unset attributes, then reset
+ for attr in missing_attr:
+ setattr(creds, attr, 'fake' + attr)
+ creds.reset()
+ # Check reset credentials are same as initial ones
+ self.assertEqual(creds, initial_creds)
+
+ def test_reset_single_attribute(self):
+ creds = self._get_credentials()
+ initial_creds = copy.deepcopy(creds)
+ set_attr = creds.__dict__.keys()
+ missing_attr = set(creds.ATTRIBUTES).difference(set_attr)
+ # Set one unset attributes, then reset
+ for attr in missing_attr:
+ setattr(creds, attr, 'fake' + attr)
+ creds.reset()
+ # Check reset credentials are same as initial ones
+ self.assertEqual(creds, initial_creds)
+
+
+class KeystoneV3CredentialsTests(KeystoneV2CredentialsTests):
+ attributes = {
+ 'username': 'fake_username',
+ 'password': 'fake_password',
+ 'project_name': 'fake_project_name',
+ 'user_domain_name': 'fake_domain_name'
+ }
+
+ credentials_class = auth.KeystoneV3Credentials
+ identity_response = fake_identity._fake_v3_response
+ tokenclient_class = v3_client.V3TokenClient
+ identity_version = 'v3'
+
+ def test_is_not_valid(self):
+ # NOTE(mtreinish) For a Keystone V3 credential object a project name
+ # is not required to be valid, so we skip that check. See tempest.auth
+ # for the valid credential requirements
+ self._test_is_not_valid('project_name')
+
+ def test_synced_attributes(self):
+ attributes = self.attributes
+ # Create V3 credentials with tenant instead of project, and user_domain
+ for attr in ['project_id', 'user_domain_id']:
+ attributes[attr] = 'fake_' + attr
+ creds = self._get_credentials(attributes)
+ self.assertEqual(creds.project_name, creds.tenant_name)
+ self.assertEqual(creds.project_id, creds.tenant_id)
+ self.assertEqual(creds.user_domain_name, creds.project_domain_name)
+ self.assertEqual(creds.user_domain_id, creds.project_domain_id)
+ # Replace user_domain with project_domain
+ del attributes['user_domain_name']
+ del attributes['user_domain_id']
+ del attributes['project_name']
+ del attributes['project_id']
+ for attr in ['project_domain_name', 'project_domain_id',
+ 'tenant_name', 'tenant_id']:
+ attributes[attr] = 'fake_' + attr
+ self.assertEqual(creds.tenant_name, creds.project_name)
+ self.assertEqual(creds.tenant_id, creds.project_id)
+ self.assertEqual(creds.project_domain_name, creds.user_domain_name)
+ self.assertEqual(creds.project_domain_id, creds.user_domain_id)
diff --git a/tempest/tests/lib/test_decorators.py b/tempest/tests/lib/test_decorators.py
new file mode 100644
index 0000000..ed0eea3
--- /dev/null
+++ b/tempest/tests/lib/test_decorators.py
@@ -0,0 +1,155 @@
+# Copyright 2013 IBM Corp
+# Copyright 2015 Hewlett-Packard Development Company, L.P.
+#
+# 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 mock
+import testtools
+
+from tempest.lib import base as test
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.tests import base
+
+
+class TestAttrDecorator(base.TestCase):
+ def _test_attr_helper(self, expected_attrs, **decorator_args):
+ @decorators.attr(**decorator_args)
+ def foo():
+ pass
+
+ # By our decorators.attr decorator the attribute __testtools_attrs
+ # will be set only for 'type' argument, so we test it first.
+ if 'type' in decorator_args:
+ # this is what testtools sets
+ self.assertEqual(getattr(foo, '__testtools_attrs'),
+ set(expected_attrs))
+
+ def test_attr_without_type(self):
+ self._test_attr_helper(expected_attrs='baz', bar='baz')
+
+ def test_attr_decorator_with_list_type(self):
+ # if type is 'smoke' we'll get the original list of types
+ self._test_attr_helper(expected_attrs=['smoke', 'foo'],
+ type=['smoke', 'foo'])
+
+ def test_attr_decorator_with_unknown_type(self):
+ self._test_attr_helper(expected_attrs=['foo'], type='foo')
+
+ def test_attr_decorator_with_duplicated_type(self):
+ self._test_attr_helper(expected_attrs=['foo'], type=['foo', 'foo'])
+
+
+class TestSkipBecauseDecorator(base.TestCase):
+ def _test_skip_because_helper(self, expected_to_skip=True,
+ **decorator_args):
+ class TestFoo(test.BaseTestCase):
+ _interface = 'json'
+
+ @decorators.skip_because(**decorator_args)
+ def test_bar(self):
+ return 0
+
+ t = TestFoo('test_bar')
+ if expected_to_skip:
+ self.assertRaises(testtools.TestCase.skipException, t.test_bar)
+ else:
+ # assert that test_bar returned 0
+ self.assertEqual(TestFoo('test_bar').test_bar(), 0)
+
+ def test_skip_because_bug(self):
+ self._test_skip_because_helper(bug='12345')
+
+ def test_skip_because_bug_and_condition_true(self):
+ self._test_skip_because_helper(bug='12348', condition=True)
+
+ def test_skip_because_bug_and_condition_false(self):
+ self._test_skip_because_helper(expected_to_skip=False,
+ bug='12349', condition=False)
+
+ def test_skip_because_bug_without_bug_never_skips(self):
+ """Never skip without a bug parameter."""
+ self._test_skip_because_helper(expected_to_skip=False,
+ condition=True)
+ self._test_skip_because_helper(expected_to_skip=False)
+
+ def test_skip_because_invalid_bug_number(self):
+ """Raise ValueError if with an invalid bug number"""
+ self.assertRaises(ValueError, self._test_skip_because_helper,
+ bug='critical_bug')
+
+
+class TestIdempotentIdDecorator(base.TestCase):
+ def _test_helper(self, _id, **decorator_args):
+ @decorators.idempotent_id(_id)
+ def foo():
+ """Docstring"""
+ pass
+
+ return foo
+
+ def _test_helper_without_doc(self, _id, **decorator_args):
+ @decorators.idempotent_id(_id)
+ def foo():
+ pass
+
+ return foo
+
+ def test_positive(self):
+ _id = data_utils.rand_uuid()
+ foo = self._test_helper(_id)
+ self.assertIn('id-%s' % _id, getattr(foo, '__testtools_attrs'))
+ self.assertTrue(foo.__doc__.startswith('Test idempotent id: %s' % _id))
+
+ def test_positive_without_doc(self):
+ _id = data_utils.rand_uuid()
+ foo = self._test_helper_without_doc(_id)
+ self.assertTrue(foo.__doc__.startswith('Test idempotent id: %s' % _id))
+
+ def test_idempotent_id_not_str(self):
+ _id = 42
+ self.assertRaises(TypeError, self._test_helper, _id)
+
+ def test_idempotent_id_not_valid_uuid(self):
+ _id = '42'
+ self.assertRaises(ValueError, self._test_helper, _id)
+
+
+class TestRelatedBugDecorator(base.TestCase):
+ def test_relatedbug_when_no_exception(self):
+ f = mock.Mock()
+ sentinel = object()
+
+ @decorators.related_bug(bug="1234", status_code=500)
+ def test_foo(self):
+ f(self)
+
+ test_foo(sentinel)
+ f.assert_called_once_with(sentinel)
+
+ def test_relatedbug_when_exception(self):
+ class MyException(Exception):
+ def __init__(self, status_code):
+ self.status_code = status_code
+
+ def f(self):
+ raise MyException(status_code=500)
+
+ @decorators.related_bug(bug="1234", status_code=500)
+ def test_foo(self):
+ f(self)
+
+ with mock.patch.object(decorators.LOG, 'error') as m_error:
+ self.assertRaises(MyException, test_foo, object())
+
+ m_error.assert_called_once_with(mock.ANY, '1234', '1234')
diff --git a/tempest/tests/lib/test_ssh.py b/tempest/tests/lib/test_ssh.py
new file mode 100644
index 0000000..37fe646
--- /dev/null
+++ b/tempest/tests/lib/test_ssh.py
@@ -0,0 +1,305 @@
+# Copyright 2014 OpenStack Foundation
+#
+# 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 socket
+
+import mock
+import six
+from six import StringIO
+import testtools
+
+from tempest.lib.common import ssh
+from tempest.lib import exceptions
+from tempest.tests import base
+import tempest.tests.utils as utils
+
+
+class TestSshClient(base.TestCase):
+
+ SELECT_POLLIN = 1
+
+ @mock.patch('paramiko.RSAKey.from_private_key')
+ @mock.patch('six.StringIO')
+ def test_pkey_calls_paramiko_RSAKey(self, cs_mock, rsa_mock):
+ cs_mock.return_value = mock.sentinel.csio
+ pkey = 'mykey'
+ ssh.Client('localhost', 'root', pkey=pkey)
+ rsa_mock.assert_called_once_with(mock.sentinel.csio)
+ cs_mock.assert_called_once_with('mykey')
+ rsa_mock.reset_mock()
+ cs_mock.reset_mock()
+ pkey = mock.sentinel.pkey
+ # Shouldn't call out to load a file from RSAKey, since
+ # a sentinel isn't a basestring...
+ ssh.Client('localhost', 'root', pkey=pkey)
+ self.assertEqual(0, rsa_mock.call_count)
+ self.assertEqual(0, cs_mock.call_count)
+
+ def _set_ssh_connection_mocks(self):
+ client_mock = mock.MagicMock()
+ client_mock.connect.return_value = True
+ return (self.patch('paramiko.SSHClient'),
+ self.patch('paramiko.AutoAddPolicy'),
+ client_mock)
+
+ def test_get_ssh_connection(self):
+ c_mock, aa_mock, client_mock = self._set_ssh_connection_mocks()
+ s_mock = self.patch('time.sleep')
+
+ c_mock.return_value = client_mock
+ aa_mock.return_value = mock.sentinel.aa
+
+ # Test normal case for successful connection on first try
+ client = ssh.Client('localhost', 'root', timeout=2)
+ client._get_ssh_connection(sleep=1)
+
+ aa_mock.assert_called_once_with()
+ client_mock.set_missing_host_key_policy.assert_called_once_with(
+ mock.sentinel.aa)
+ expected_connect = [mock.call(
+ 'localhost',
+ port=22,
+ username='root',
+ pkey=None,
+ key_filename=None,
+ look_for_keys=False,
+ timeout=10.0,
+ password=None,
+ sock=None
+ )]
+ self.assertEqual(expected_connect, client_mock.connect.mock_calls)
+ self.assertEqual(0, s_mock.call_count)
+
+ def test_get_ssh_connection_over_ssh(self):
+ c_mock, aa_mock, client_mock = self._set_ssh_connection_mocks()
+ proxy_client_mock = mock.MagicMock()
+ proxy_client_mock.connect.return_value = True
+ s_mock = self.patch('time.sleep')
+
+ c_mock.side_effect = [client_mock, proxy_client_mock]
+ aa_mock.return_value = mock.sentinel.aa
+
+ proxy_client = ssh.Client('proxy-host', 'proxy-user', timeout=2)
+ client = ssh.Client('localhost', 'root', timeout=2,
+ proxy_client=proxy_client)
+ client._get_ssh_connection(sleep=1)
+
+ aa_mock.assert_has_calls([mock.call(), mock.call()])
+ proxy_client_mock.set_missing_host_key_policy.assert_called_once_with(
+ mock.sentinel.aa)
+ proxy_expected_connect = [mock.call(
+ 'proxy-host',
+ port=22,
+ username='proxy-user',
+ pkey=None,
+ key_filename=None,
+ look_for_keys=False,
+ timeout=10.0,
+ password=None,
+ sock=None
+ )]
+ self.assertEqual(proxy_expected_connect,
+ proxy_client_mock.connect.mock_calls)
+ client_mock.set_missing_host_key_policy.assert_called_once_with(
+ mock.sentinel.aa)
+ expected_connect = [mock.call(
+ 'localhost',
+ port=22,
+ username='root',
+ pkey=None,
+ key_filename=None,
+ look_for_keys=False,
+ timeout=10.0,
+ password=None,
+ sock=proxy_client_mock.get_transport().open_session()
+ )]
+ self.assertEqual(expected_connect, client_mock.connect.mock_calls)
+ self.assertEqual(0, s_mock.call_count)
+
+ @mock.patch('time.sleep')
+ def test_get_ssh_connection_two_attemps(self, sleep_mock):
+ c_mock, aa_mock, client_mock = self._set_ssh_connection_mocks()
+
+ c_mock.return_value = client_mock
+ client_mock.connect.side_effect = [
+ socket.error,
+ mock.MagicMock()
+ ]
+
+ client = ssh.Client('localhost', 'root', timeout=1)
+ client._get_ssh_connection(sleep=1)
+ # We slept 2 seconds: because sleep is "1" and backoff is "1" too
+ sleep_mock.assert_called_once_with(2)
+ self.assertEqual(2, client_mock.connect.call_count)
+
+ def test_get_ssh_connection_timeout(self):
+ c_mock, aa_mock, client_mock = self._set_ssh_connection_mocks()
+
+ timeout = 2
+ time_mock = self.patch('time.time')
+ time_mock.side_effect = utils.generate_timeout_series(timeout + 1)
+
+ c_mock.return_value = client_mock
+ client_mock.connect.side_effect = [
+ socket.error,
+ socket.error,
+ socket.error,
+ ]
+
+ client = ssh.Client('localhost', 'root', timeout=timeout)
+ # We need to mock LOG here because LOG.info() calls time.time()
+ # in order to preprend a timestamp.
+ with mock.patch.object(ssh, 'LOG'):
+ self.assertRaises(exceptions.SSHTimeout,
+ client._get_ssh_connection)
+
+ # time.time() should be called twice, first to start the timer
+ # and then to compute the timedelta
+ self.assertEqual(2, time_mock.call_count)
+
+ @mock.patch('select.POLLIN', SELECT_POLLIN, create=True)
+ def test_timeout_in_exec_command(self):
+ chan_mock, poll_mock, _ = self._set_mocks_for_select([0, 0, 0], True)
+
+ # Test for a timeout condition immediately raised
+ client = ssh.Client('localhost', 'root', timeout=2)
+ with testtools.ExpectedException(exceptions.TimeoutException):
+ client.exec_command("test")
+
+ chan_mock.fileno.assert_called_once_with()
+ chan_mock.exec_command.assert_called_once_with("test")
+ chan_mock.shutdown_write.assert_called_once_with()
+
+ poll_mock.register.assert_called_once_with(
+ chan_mock, self.SELECT_POLLIN)
+ poll_mock.poll.assert_called_once_with(10)
+
+ @mock.patch('select.POLLIN', SELECT_POLLIN, create=True)
+ def test_exec_command(self):
+ chan_mock, poll_mock, select_mock = (
+ self._set_mocks_for_select([[1, 0, 0]], True))
+
+ chan_mock.recv_exit_status.return_value = 0
+ chan_mock.recv.return_value = b''
+ chan_mock.recv_stderr.return_value = b''
+
+ client = ssh.Client('localhost', 'root', timeout=2)
+ client.exec_command("test")
+
+ chan_mock.fileno.assert_called_once_with()
+ chan_mock.exec_command.assert_called_once_with("test")
+ chan_mock.shutdown_write.assert_called_once_with()
+
+ select_mock.assert_called_once_with()
+ poll_mock.register.assert_called_once_with(
+ chan_mock, self.SELECT_POLLIN)
+ poll_mock.poll.assert_called_once_with(10)
+ chan_mock.recv_ready.assert_called_once_with()
+ chan_mock.recv.assert_called_once_with(1024)
+ chan_mock.recv_stderr_ready.assert_called_once_with()
+ chan_mock.recv_stderr.assert_called_once_with(1024)
+ chan_mock.recv_exit_status.assert_called_once_with()
+
+ def _set_mocks_for_select(self, poll_data, ito_value=False):
+ gsc_mock = self.patch('tempest.lib.common.ssh.Client.'
+ '_get_ssh_connection')
+ ito_mock = self.patch('tempest.lib.common.ssh.Client._is_timed_out')
+ csp_mock = self.patch(
+ 'tempest.lib.common.ssh.Client._can_system_poll')
+ csp_mock.return_value = True
+
+ select_mock = self.patch('select.poll', create=True)
+ client_mock = mock.MagicMock()
+ tran_mock = mock.MagicMock()
+ chan_mock = mock.MagicMock()
+ poll_mock = mock.MagicMock()
+
+ select_mock.return_value = poll_mock
+ gsc_mock.return_value = client_mock
+ ito_mock.return_value = ito_value
+ client_mock.get_transport.return_value = tran_mock
+ tran_mock.open_session().__enter__.return_value = chan_mock
+ if isinstance(poll_data[0], list):
+ poll_mock.poll.side_effect = poll_data
+ else:
+ poll_mock.poll.return_value = poll_data
+
+ return chan_mock, poll_mock, select_mock
+
+ _utf8_string = six.unichr(1071)
+ _utf8_bytes = _utf8_string.encode("utf-8")
+
+ @mock.patch('select.POLLIN', SELECT_POLLIN, create=True)
+ def test_exec_good_command_output(self):
+ chan_mock, poll_mock, _ = self._set_mocks_for_select([1, 0, 0])
+ closed_prop = mock.PropertyMock(return_value=True)
+ type(chan_mock).closed = closed_prop
+
+ chan_mock.recv_exit_status.return_value = 0
+ chan_mock.recv.side_effect = [self._utf8_bytes[0:1],
+ self._utf8_bytes[1:], b'R', b'']
+ chan_mock.recv_stderr.return_value = b''
+
+ client = ssh.Client('localhost', 'root', timeout=2)
+ out_data = client.exec_command("test")
+ self.assertEqual(self._utf8_string + 'R', out_data)
+
+ @mock.patch('select.POLLIN', SELECT_POLLIN, create=True)
+ def test_exec_bad_command_output(self):
+ chan_mock, poll_mock, _ = self._set_mocks_for_select([1, 0, 0])
+ closed_prop = mock.PropertyMock(return_value=True)
+ type(chan_mock).closed = closed_prop
+
+ chan_mock.recv_exit_status.return_value = 1
+ chan_mock.recv.return_value = b''
+ chan_mock.recv_stderr.side_effect = [b'R', self._utf8_bytes[0:1],
+ self._utf8_bytes[1:], b'']
+
+ client = ssh.Client('localhost', 'root', timeout=2)
+ exc = self.assertRaises(exceptions.SSHExecCommandFailed,
+ client.exec_command, "test")
+ self.assertIn('R' + self._utf8_string, six.text_type(exc))
+
+ def test_exec_command_no_select(self):
+ gsc_mock = self.patch('tempest.lib.common.ssh.Client.'
+ '_get_ssh_connection')
+ csp_mock = self.patch(
+ 'tempest.lib.common.ssh.Client._can_system_poll')
+ csp_mock.return_value = False
+
+ select_mock = self.patch('select.poll', create=True)
+ client_mock = mock.MagicMock()
+ tran_mock = mock.MagicMock()
+ chan_mock = mock.MagicMock()
+
+ # Test for proper reading of STDOUT and STDERROR
+
+ gsc_mock.return_value = client_mock
+ client_mock.get_transport.return_value = tran_mock
+ tran_mock.open_session().__enter__.return_value = chan_mock
+ chan_mock.recv_exit_status.return_value = 0
+
+ std_out_mock = mock.MagicMock(StringIO)
+ std_err_mock = mock.MagicMock(StringIO)
+ chan_mock.makefile.return_value = std_out_mock
+ chan_mock.makefile_stderr.return_value = std_err_mock
+
+ client = ssh.Client('localhost', 'root', timeout=2)
+ client.exec_command("test")
+
+ chan_mock.makefile.assert_called_once_with('rb', 1024)
+ chan_mock.makefile_stderr.assert_called_once_with('rb', 1024)
+ std_out_mock.read.assert_called_once_with()
+ std_err_mock.read.assert_called_once_with()
+ self.assertFalse(select_mock.called)
diff --git a/tempest/tests/lib/test_tempest_lib.py b/tempest/tests/lib/test_tempest_lib.py
new file mode 100644
index 0000000..4d9f099
--- /dev/null
+++ b/tempest/tests/lib/test_tempest_lib.py
@@ -0,0 +1,26 @@
+# 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.
+
+"""
+test_tempest.lib
+----------------------------------
+
+Tests for `tempest.lib` module.
+"""
+
+from tempest.tests import base
+
+
+class TestTempest_lib(base.TestCase):
+
+ def test_something(self):
+ pass
diff --git a/tempest/tests/negative/test_negative_auto_test.py b/tempest/tests/negative/test_negative_auto_test.py
deleted file mode 100644
index 7a127cd..0000000
--- a/tempest/tests/negative/test_negative_auto_test.py
+++ /dev/null
@@ -1,66 +0,0 @@
-# Copyright 2014 Deutsche Telekom AG
-# 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 import config
-import tempest.test as test
-from tempest.tests import base
-from tempest.tests import fake_config
-
-
-class TestNegativeAutoTest(base.TestCase):
- # Fake entries
- _service = 'compute'
-
- fake_input_desc = {"name": "list-flavors-with-detail",
- "http-method": "GET",
- "url": "flavors/detail",
- "json-schema": {"type": "object",
- "properties":
- {"minRam": {"type": "integer"},
- "minDisk": {"type": "integer"}}
- },
- "resources": ["flavor", "volume", "image"]
- }
-
- def setUp(self):
- super(TestNegativeAutoTest, self).setUp()
- self.useFixture(fake_config.ConfigFixture())
- self.stubs.Set(config, 'TempestConfigPrivate', fake_config.FakePrivate)
-
- def _check_prop_entries(self, result, entry):
- entries = [a for a in result if entry in a[0]]
- self.assertIsNotNone(entries)
- self.assertGreater(len(entries), 1)
- for entry in entries:
- self.assertIsNotNone(entry[1]['_negtest_name'])
-
- def _check_resource_entries(self, result, entry):
- entries = [a for a in result if entry in a[0]]
- self.assertIsNotNone(entries)
- self.assertIs(len(entries), 3)
- for entry in entries:
- self.assertIsNotNone(entry[1]['resource'])
-
- def test_generate_scenario(self):
- scenarios = test.NegativeAutoTest.\
- generate_scenario(self.fake_input_desc)
- self.assertIsInstance(scenarios, list)
- for scenario in scenarios:
- self.assertIsInstance(scenario, tuple)
- self.assertIsInstance(scenario[0], str)
- self.assertIsInstance(scenario[1], dict)
- self._check_prop_entries(scenarios, "minRam")
- self._check_prop_entries(scenarios, "minDisk")
- self._check_resource_entries(scenarios, "inv_res")
diff --git a/tempest/tests/negative/test_negative_generators.py b/tempest/tests/negative/test_negative_generators.py
deleted file mode 100644
index 78fd80d..0000000
--- a/tempest/tests/negative/test_negative_generators.py
+++ /dev/null
@@ -1,150 +0,0 @@
-# Copyright 2014 Deutsche Telekom AG
-# 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 copy
-
-import jsonschema
-import mock
-import six
-
-from tempest.common.generator import base_generator
-from tempest.common.generator import negative_generator
-from tempest.common.generator import valid_generator
-from tempest.tests import base
-
-
-class TestNegativeBasicGenerator(base.TestCase):
- valid_desc = {
- "name": "list-flavors-with-detail",
- "http-method": "GET",
- "url": "flavors/detail",
- "json-schema": {
- "type": "object",
- "properties": {
- "minRam": {"type": "integer"},
- "minDisk": {"type": "integer"}
- }
- },
- "resources": ["flavor", "volume", "image"]
- }
-
- minimal_desc = {
- "name": "list-flavors-with-detail",
- "http-method": "GET",
- "url": "flavors/detail",
- }
-
- add_prop_desc = {
- "name": "list-flavors-with-detail",
- "http-method": "GET",
- "url": "flavors/detail",
- "unknown_field": [12]
- }
-
- invalid_json_schema_desc = {
- "name": "list-flavors-with-detail",
- "http-method": "GET",
- "url": "flavors/detail",
- "json-schema": {"type": "NotExistingType"}
- }
-
- def setUp(self):
- super(TestNegativeBasicGenerator, self).setUp()
- self.generator = base_generator.BasicGeneratorSet()
-
- def _assert_valid_jsonschema_call(self, jsonschema_mock, desc):
- self.assertEqual(jsonschema_mock.call_count, 1)
- jsonschema_mock.assert_called_with(desc, self.generator.schema)
-
- @mock.patch('jsonschema.validate', wraps=jsonschema.validate)
- def test_validate_schema_with_valid_input(self, jsonschema_mock):
- self.generator.validate_schema(self.valid_desc)
- self._assert_valid_jsonschema_call(jsonschema_mock, self.valid_desc)
-
- @mock.patch('jsonschema.validate', wraps=jsonschema.validate)
- def test_validate_schema_with_minimal_input(self, jsonschema_mock):
- self.generator.validate_schema(self.minimal_desc)
- self._assert_valid_jsonschema_call(jsonschema_mock, self.minimal_desc)
-
- def test_validate_schema_with_invalid_input(self):
- self.assertRaises(jsonschema.ValidationError,
- self.generator.validate_schema, self.add_prop_desc)
- self.assertRaises(jsonschema.SchemaError,
- self.generator.validate_schema,
- self.invalid_json_schema_desc)
-
-
-class BaseNegativeGenerator(object):
- types = ['string', 'integer', 'object']
-
- fake_input_obj = {"type": "object",
- "properties": {"minRam": {"type": "integer"},
- "diskName": {"type": "string"},
- "maxRam": {"type": "integer", }
- }
- }
-
- unknown_type_schema = {
- "type": "not_defined"
- }
-
- class fake_test_class(object):
- def __init__(self, scenario):
- for k, v in six.iteritems(scenario):
- setattr(self, k, v)
-
- def _validate_result(self, valid_schema, invalid_schema):
- for k, v in six.iteritems(valid_schema):
- self.assertTrue(k in invalid_schema)
-
- def test_generator_mandatory_functions(self):
- for data_type in self.types:
- self.assertIn(data_type, self.generator.types_dict)
-
- def test_generate_with_unknown_type(self):
- self.assertRaises(TypeError, self.generator.generate_payload,
- self.unknown_type_schema)
-
-
-class TestNegativeValidGenerator(base.TestCase, BaseNegativeGenerator):
- def setUp(self):
- super(TestNegativeValidGenerator, self).setUp()
- self.generator = valid_generator.ValidTestGenerator()
-
- def test_generate_valid(self):
- result = self.generator.generate_valid(self.fake_input_obj)
- self.assertIn("minRam", result)
- self.assertIsInstance(result["minRam"], int)
- self.assertIn("diskName", result)
- self.assertIsInstance(result["diskName"], str)
-
-
-class TestNegativeNegativeGenerator(base.TestCase, BaseNegativeGenerator):
- def setUp(self):
- super(TestNegativeNegativeGenerator, self).setUp()
- self.generator = negative_generator.NegativeTestGenerator()
-
- def test_generate_obj(self):
- schema = self.fake_input_obj
- scenarios = self.generator.generate_scenarios(schema)
- for scenario in scenarios:
- test = self.fake_test_class(scenario)
- valid_schema = \
- valid_generator.ValidTestGenerator().generate_valid(schema)
- schema_under_test = copy.copy(valid_schema)
- expected_result = \
- self.generator.generate_payload(test, schema_under_test)
- self.assertEqual(expected_result, None)
- self._validate_result(valid_schema, schema_under_test)
diff --git a/tempest/tests/services/compute/base.py b/tempest/tests/services/compute/base.py
deleted file mode 100644
index a35a87c..0000000
--- a/tempest/tests/services/compute/base.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# Copyright 2015 Deutsche Telekom AG. 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 httplib2
-
-from oslo_serialization import jsonutils as json
-from oslotest import mockpatch
-
-from tempest.tests import base
-
-
-class BaseComputeServiceTest(base.TestCase):
- def create_response(self, body, to_utf=False, status=200):
- json_body = {}
- if body:
- json_body = json.dumps(body)
- if to_utf:
- json_body = json_body.encode('utf-8')
- response = (httplib2.Response({'status': status}), json_body)
- return response
-
- def check_service_client_function(self, function, function2mock,
- body, to_utf=False, status=200,
- **kwargs):
- mocked_response = self.create_response(body, to_utf, status)
- self.useFixture(mockpatch.Patch(
- function2mock, return_value=mocked_response))
- if kwargs:
- resp = function(**kwargs)
- else:
- resp = function()
- self.assertEqual(body, resp)
diff --git a/tempest/tests/services/compute/test_base_compute_client.py b/tempest/tests/services/compute/test_base_compute_client.py
deleted file mode 100644
index 13461e4..0000000
--- a/tempest/tests/services/compute/test_base_compute_client.py
+++ /dev/null
@@ -1,72 +0,0 @@
-# Copyright 2015 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.
-
-import httplib2
-import mock
-from tempest_lib.common import rest_client
-
-from tempest.services.compute.json import base as base_compute_client
-from tempest.tests import fake_auth_provider
-from tempest.tests.services.compute import base
-
-
-class TestClientWithoutMicroversionHeader(base.BaseComputeServiceTest):
-
- def setUp(self):
- super(TestClientWithoutMicroversionHeader, self).setUp()
- fake_auth = fake_auth_provider.FakeAuthProvider()
- self.client = base_compute_client.BaseComputeClient(
- fake_auth, 'compute', 'regionOne')
-
- def test_no_microverion_header(self):
- header = self.client.get_headers()
- self.assertNotIn('X-OpenStack-Nova-API-Version', header)
-
- def test_no_microverion_header_in_raw_request(self):
- def raw_request(*args, **kwargs):
- self.assertNotIn('X-OpenStack-Nova-API-Version', kwargs['headers'])
- return (httplib2.Response({'status': 200}), {})
-
- with mock.patch.object(rest_client.RestClient,
- 'raw_request') as mock_get:
- mock_get.side_effect = raw_request
- self.client.get('fake_url')
-
-
-class TestClientWithMicroversionHeader(base.BaseComputeServiceTest):
-
- def setUp(self):
- super(TestClientWithMicroversionHeader, self).setUp()
- fake_auth = fake_auth_provider.FakeAuthProvider()
- self.client = base_compute_client.BaseComputeClient(
- fake_auth, 'compute', 'regionOne')
- self.client.api_microversion = '2.2'
-
- def test_microverion_header(self):
- header = self.client.get_headers()
- self.assertIn('X-OpenStack-Nova-API-Version', header)
- self.assertEqual(self.client.api_microversion,
- header['X-OpenStack-Nova-API-Version'])
-
- def test_microverion_header_in_raw_request(self):
- def raw_request(*args, **kwargs):
- self.assertIn('X-OpenStack-Nova-API-Version', kwargs['headers'])
- self.assertEqual(self.client.api_microversion,
- kwargs['headers']['X-OpenStack-Nova-API-Version'])
- return (httplib2.Response({'status': 200}), {})
-
- with mock.patch.object(rest_client.RestClient,
- 'raw_request') as mock_get:
- mock_get.side_effect = raw_request
- self.client.get('fake_url')
diff --git a/tempest/tests/services/compute/test_keypairs_client.py b/tempest/tests/services/compute/test_keypairs_client.py
deleted file mode 100644
index 03aee53..0000000
--- a/tempest/tests/services/compute/test_keypairs_client.py
+++ /dev/null
@@ -1,95 +0,0 @@
-# Copyright 2015 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.
-
-import copy
-
-from tempest_lib.tests import fake_auth_provider
-
-from tempest.services.compute.json import keypairs_client
-from tempest.tests.services.compute import base
-
-
-class TestKeyPairsClient(base.BaseComputeServiceTest):
-
- FAKE_KEYPAIR = {"keypair": {
- "public_key": "ssh-rsa foo Generated-by-Nova",
- "name": u'\u2740(*\xb4\u25e1`*)\u2740',
- "user_id": "525d55f98980415ba98e634972fa4a10",
- "fingerprint": "76:24:66:49:d7:ca:6e:5c:77:ea:8e:bb:9c:15:5f:98"
- }}
-
- def setUp(self):
- super(TestKeyPairsClient, self).setUp()
- fake_auth = fake_auth_provider.FakeAuthProvider()
- self.client = keypairs_client.KeyPairsClient(
- fake_auth, 'compute', 'regionOne')
-
- def _test_list_keypairs(self, bytes_body=False):
- self.check_service_client_function(
- self.client.list_keypairs,
- 'tempest_lib.common.rest_client.RestClient.get',
- {"keypairs": []},
- bytes_body)
-
- def test_list_keypairs_with_str_body(self):
- self._test_list_keypairs()
-
- def test_list_keypairs_with_bytes_body(self):
- self._test_list_keypairs(bytes_body=True)
-
- def _test_show_keypair(self, bytes_body=False):
- fake_keypair = copy.deepcopy(self.FAKE_KEYPAIR)
- fake_keypair["keypair"].update({
- "deleted": False,
- "created_at": "2015-07-22T04:53:52.000000",
- "updated_at": None,
- "deleted_at": None,
- "id": 1
- })
-
- self.check_service_client_function(
- self.client.show_keypair,
- 'tempest_lib.common.rest_client.RestClient.get',
- fake_keypair,
- bytes_body,
- keypair_name="test")
-
- def test_show_keypair_with_str_body(self):
- self._test_show_keypair()
-
- def test_show_keypair_with_bytes_body(self):
- self._test_show_keypair(bytes_body=True)
-
- def _test_create_keypair(self, bytes_body=False):
- fake_keypair = copy.deepcopy(self.FAKE_KEYPAIR)
- fake_keypair["keypair"].update({"private_key": "foo"})
-
- self.check_service_client_function(
- self.client.create_keypair,
- 'tempest_lib.common.rest_client.RestClient.post',
- fake_keypair,
- bytes_body,
- name="test")
-
- def test_create_keypair_with_str_body(self):
- self._test_create_keypair()
-
- def test_create_keypair_with_bytes_body(self):
- self._test_create_keypair(bytes_body=True)
-
- def test_delete_keypair(self):
- self.check_service_client_function(
- self.client.delete_keypair,
- 'tempest_lib.common.rest_client.RestClient.delete',
- {}, status=202, keypair_name='test')
diff --git a/tempest/tests/stress/test_stress.py b/tempest/tests/stress/test_stress.py
deleted file mode 100644
index 0ec2a5d..0000000
--- a/tempest/tests/stress/test_stress.py
+++ /dev/null
@@ -1,55 +0,0 @@
-# Copyright 2013 Deutsche Telekom AG
-# 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 shlex
-import subprocess
-
-from tempest_lib import exceptions
-
-from oslo_log import log as logging
-from tempest.tests import base
-
-LOG = logging.getLogger(__name__)
-
-
-class StressFrameworkTest(base.TestCase):
- """Basic test for the stress test framework."""
-
- def _cmd(self, cmd, param):
- """Executes specified command."""
- cmd = ' '.join([cmd, param])
- LOG.info("running: '%s'" % cmd)
- cmd_str = cmd
- cmd = shlex.split(cmd)
- result = ''
- result_err = ''
- try:
- stdout = subprocess.PIPE
- stderr = subprocess.PIPE
- proc = subprocess.Popen(
- cmd, stdout=stdout, stderr=stderr)
- result, result_err = proc.communicate()
- if proc.returncode != 0:
- LOG.debug('error of %s:\n%s' % (cmd_str, result_err))
- raise exceptions.CommandFailed(proc.returncode,
- cmd,
- result)
- finally:
- LOG.debug('output of %s:\n%s' % (cmd_str, result))
- return proc.returncode
-
- def test_help_function(self):
- result = self._cmd("python", "-m tempest.cmd.run_stress -h")
- self.assertEqual(0, result)
diff --git a/tempest/tests/stress/test_stressaction.py b/tempest/tests/stress/test_stressaction.py
deleted file mode 100644
index 1a1bb67..0000000
--- a/tempest/tests/stress/test_stressaction.py
+++ /dev/null
@@ -1,63 +0,0 @@
-# Copyright 2013 Deutsche Telekom AG
-# 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 tempest.stress.stressaction as stressaction
-import tempest.test
-
-
-class FakeStressAction(stressaction.StressAction):
- def __init__(self, manager, max_runs=None, stop_on_error=False):
- super(self.__class__, self).__init__(manager, max_runs, stop_on_error)
- self._run_called = False
-
- def run(self):
- self._run_called = True
-
- @property
- def run_called(self):
- return self._run_called
-
-
-class FakeStressActionFailing(stressaction.StressAction):
- def run(self):
- raise Exception('FakeStressActionFailing raise exception')
-
-
-class TestStressAction(tempest.test.BaseTestCase):
- def _bulid_stats_dict(self, runs=0, fails=0):
- return {'runs': runs, 'fails': fails}
-
- def testStressTestRun(self):
- stressAction = FakeStressAction(manager=None, max_runs=1)
- stats = self._bulid_stats_dict()
- stressAction.execute(stats)
- self.assertTrue(stressAction.run_called)
- self.assertEqual(stats['runs'], 1)
- self.assertEqual(stats['fails'], 0)
-
- def testStressMaxTestRuns(self):
- stressAction = FakeStressAction(manager=None, max_runs=500)
- stats = self._bulid_stats_dict(runs=499)
- stressAction.execute(stats)
- self.assertTrue(stressAction.run_called)
- self.assertEqual(stats['runs'], 500)
- self.assertEqual(stats['fails'], 0)
-
- def testStressTestRunWithException(self):
- stressAction = FakeStressActionFailing(manager=None, max_runs=1)
- stats = self._bulid_stats_dict()
- stressAction.execute(stats)
- self.assertEqual(stats['runs'], 1)
- self.assertEqual(stats['fails'], 1)
diff --git a/tempest/tests/test_base_test.py b/tempest/tests/test_base_test.py
new file mode 100644
index 0000000..2b5a947
--- /dev/null
+++ b/tempest/tests/test_base_test.py
@@ -0,0 +1,114 @@
+# Copyright 2016 IBM Corp.
+#
+# 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 mock
+from oslo_config import cfg
+
+from tempest import clients
+from tempest.common import credentials_factory as credentials
+from tempest import config
+from tempest.lib.common import fixed_network
+from tempest import test
+from tempest.tests import base
+from tempest.tests import fake_config
+
+
+class TestBaseTestCase(base.TestCase):
+ def setUp(self):
+ super(TestBaseTestCase, self).setUp()
+ self.useFixture(fake_config.ConfigFixture())
+ self.patchobject(config, 'TempestConfigPrivate',
+ fake_config.FakePrivate)
+ self.fixed_network_name = 'fixed-net'
+ cfg.CONF.set_default('fixed_network_name', self.fixed_network_name,
+ 'compute')
+ cfg.CONF.set_default('neutron', True, 'service_available')
+
+ @mock.patch.object(test.BaseTestCase, 'get_client_manager')
+ @mock.patch.object(test.BaseTestCase, '_get_credentials_provider')
+ @mock.patch.object(fixed_network, 'get_tenant_network')
+ def test_get_tenant_network(self, mock_gtn, mock_gprov, mock_gcm):
+ net_client = mock.Mock()
+ mock_prov = mock.Mock()
+ mock_gcm.return_value.networks_client = net_client
+ mock_gprov.return_value = mock_prov
+
+ test.BaseTestCase.get_tenant_network()
+
+ mock_gcm.assert_called_once_with(credential_type='primary')
+ mock_gprov.assert_called_once_with()
+ mock_gtn.assert_called_once_with(mock_prov, net_client,
+ self.fixed_network_name)
+
+ @mock.patch.object(test.BaseTestCase, 'get_client_manager')
+ @mock.patch.object(test.BaseTestCase, '_get_credentials_provider')
+ @mock.patch.object(fixed_network, 'get_tenant_network')
+ @mock.patch.object(test.BaseTestCase, 'get_identity_version')
+ @mock.patch.object(credentials, 'is_admin_available')
+ @mock.patch.object(clients, 'Manager')
+ def test_get_tenant_network_with_nova_net(self, mock_man, mock_iaa,
+ mock_giv, mock_gtn, mock_gcp,
+ mock_gcm):
+ cfg.CONF.set_default('neutron', False, 'service_available')
+ mock_prov = mock.Mock()
+ mock_admin_man = mock.Mock()
+ mock_iaa.return_value = True
+ mock_gcp.return_value = mock_prov
+ mock_man.return_value = mock_admin_man
+
+ test.BaseTestCase.get_tenant_network()
+
+ mock_man.assert_called_once_with(
+ mock_prov.get_admin_creds.return_value.credentials)
+ mock_iaa.assert_called_once_with(
+ identity_version=mock_giv.return_value)
+ mock_gcp.assert_called_once_with()
+ mock_gtn.assert_called_once_with(
+ mock_prov, mock_admin_man.compute_networks_client,
+ self.fixed_network_name)
+
+ @mock.patch.object(test.BaseTestCase, 'get_client_manager')
+ @mock.patch.object(test.BaseTestCase, '_get_credentials_provider')
+ @mock.patch.object(fixed_network, 'get_tenant_network')
+ def test_get_tenant_network_with_alt_creds(self, mock_gtn, mock_gprov,
+ mock_gcm):
+ net_client = mock.Mock()
+ mock_prov = mock.Mock()
+ mock_gcm.return_value.networks_client = net_client
+ mock_gprov.return_value = mock_prov
+
+ test.BaseTestCase.get_tenant_network(credentials_type='alt')
+
+ mock_gcm.assert_called_once_with(credential_type='alt')
+ mock_gprov.assert_called_once_with()
+ mock_gtn.assert_called_once_with(mock_prov, net_client,
+ self.fixed_network_name)
+
+ @mock.patch.object(test.BaseTestCase, 'get_client_manager')
+ @mock.patch.object(test.BaseTestCase, '_get_credentials_provider')
+ @mock.patch.object(fixed_network, 'get_tenant_network')
+ def test_get_tenant_network_with_role_creds(self, mock_gtn, mock_gprov,
+ mock_gcm):
+ net_client = mock.Mock()
+ mock_prov = mock.Mock()
+ mock_gcm.return_value.networks_client = net_client
+ mock_gprov.return_value = mock_prov
+ creds = ['foo_type', 'role1']
+
+ test.BaseTestCase.get_tenant_network(credentials_type=creds)
+
+ mock_gcm.assert_called_once_with(roles=['role1'])
+ mock_gprov.assert_called_once_with()
+ mock_gtn.assert_called_once_with(mock_prov, net_client,
+ self.fixed_network_name)
diff --git a/tempest/tests/test_config.py b/tempest/tests/test_config.py
new file mode 100644
index 0000000..2808a9c
--- /dev/null
+++ b/tempest/tests/test_config.py
@@ -0,0 +1,89 @@
+# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P.
+#
+# 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 import config
+from tempest.lib import exceptions
+from tempest.tests import base
+from tempest.tests import fake_config
+
+
+class TestServiceClientConfig(base.TestCase):
+
+ expected_common_params = set(['disable_ssl_certificate_validation',
+ 'ca_certs', 'trace_requests'])
+ expected_extra_params = set(['service', 'endpoint_type', 'region',
+ 'build_timeout', 'build_interval'])
+
+ def setUp(self):
+ super(TestServiceClientConfig, self).setUp()
+ self.useFixture(fake_config.ServiceClientsConfigFixture())
+ self.patchobject(config, 'CONF',
+ fake_config.ServiceClientsFakePrivate())
+ self.CONF = config.CONF
+
+ def test_service_client_config_no_service(self):
+ params = config.service_client_config()
+ for param_name in self.expected_common_params:
+ self.assertIn(param_name, params)
+ for param_name in self.expected_extra_params:
+ self.assertNotIn(param_name, params)
+ self.assertEqual(
+ self.CONF.identity.disable_ssl_certificate_validation,
+ params['disable_ssl_certificate_validation'])
+ self.assertEqual(self.CONF.identity.ca_certificates_file,
+ params['ca_certs'])
+ self.assertEqual(self.CONF.debug.trace_requests,
+ params['trace_requests'])
+
+ def test_service_client_config_service_all(self):
+ params = config.service_client_config(
+ service_client_name='fake-service1')
+ for param_name in self.expected_common_params:
+ self.assertIn(param_name, params)
+ for param_name in self.expected_extra_params:
+ self.assertIn(param_name, params)
+ self.assertEqual(self.CONF.fake_service1.catalog_type,
+ params['service'])
+ self.assertEqual(self.CONF.fake_service1.endpoint_type,
+ params['endpoint_type'])
+ self.assertEqual(self.CONF.fake_service1.region, params['region'])
+ self.assertEqual(self.CONF.fake_service1.build_timeout,
+ params['build_timeout'])
+ self.assertEqual(self.CONF.fake_service1.build_interval,
+ params['build_interval'])
+
+ def test_service_client_config_service_minimal(self):
+ params = config.service_client_config(
+ service_client_name='fake-service2')
+ for param_name in self.expected_common_params:
+ self.assertIn(param_name, params)
+ for param_name in self.expected_extra_params:
+ self.assertIn(param_name, params)
+ self.assertEqual(self.CONF.fake_service2.catalog_type,
+ params['service'])
+ self.assertEqual(self.CONF.fake_service2.endpoint_type,
+ params['endpoint_type'])
+ self.assertEqual(self.CONF.identity.region, params['region'])
+ self.assertEqual(self.CONF.compute.build_timeout,
+ params['build_timeout'])
+ self.assertEqual(self.CONF.compute.build_interval,
+ params['build_interval'])
+
+ def test_service_client_config_service_unknown(self):
+ unknown_service = 'unknown_service'
+ with testtools.ExpectedException(exceptions.UnknownServiceClient,
+ '.*' + unknown_service + '.*'):
+ config.service_client_config(service_client_name=unknown_service)
diff --git a/tempest/tests/test_decorators.py b/tempest/tests/test_decorators.py
index ce3eb7e..6018441 100644
--- a/tempest/tests/test_decorators.py
+++ b/tempest/tests/test_decorators.py
@@ -12,15 +12,14 @@
# License for the specific language governing permissions and limitations
# under the License.
-import uuid
-
-import mock
+import fixtures
from oslo_config import cfg
-from oslotest import mockpatch
import testtools
+from tempest.common import utils
from tempest import config
from tempest import exceptions
+from tempest.lib.common.utils import data_utils
from tempest import test
from tempest.tests import base
from tempest.tests import fake_config
@@ -30,37 +29,14 @@
def setUp(self):
super(BaseDecoratorsTest, self).setUp()
self.config_fixture = self.useFixture(fake_config.ConfigFixture())
- self.stubs.Set(config, 'TempestConfigPrivate', fake_config.FakePrivate)
+ self.patchobject(config, 'TempestConfigPrivate',
+ fake_config.FakePrivate)
-class TestAttrDecorator(BaseDecoratorsTest):
- def _test_attr_helper(self, expected_attrs, **decorator_args):
- @test.attr(**decorator_args)
- def foo():
- pass
-
- # By our test.attr decorator the attribute __testtools_attrs will be
- # set only for 'type' argument, so we test it first.
- if 'type' in decorator_args:
- # this is what testtools sets
- self.assertEqual(getattr(foo, '__testtools_attrs'),
- set(expected_attrs))
-
- def test_attr_without_type(self):
- self._test_attr_helper(expected_attrs='baz', bar='baz')
-
- def test_attr_decorator_with_list_type(self):
- # if type is 'smoke' we'll get the original list of types
- self._test_attr_helper(expected_attrs=['smoke', 'foo'],
- type=['smoke', 'foo'])
-
- def test_attr_decorator_with_unknown_type(self):
- self._test_attr_helper(expected_attrs=['foo'], type='foo')
-
- def test_attr_decorator_with_duplicated_type(self):
- self._test_attr_helper(expected_attrs=['foo'], type=['foo', 'foo'])
-
-
+# NOTE: The test module is for tempest.test.idempotent_id.
+# After all projects switch to use decorators.idempotent_id,
+# we can remove tempest.test.idempotent_id as well as this
+# test module
class TestIdempotentIdDecorator(BaseDecoratorsTest):
def _test_helper(self, _id, **decorator_args):
@@ -79,13 +55,13 @@
return foo
def test_positive(self):
- _id = str(uuid.uuid4())
+ _id = data_utils.rand_uuid()
foo = self._test_helper(_id)
self.assertIn('id-%s' % _id, getattr(foo, '__testtools_attrs'))
self.assertTrue(foo.__doc__.startswith('Test idempotent id: %s' % _id))
def test_positive_without_doc(self):
- _id = str(uuid.uuid4())
+ _id = data_utils.rand_uuid()
foo = self._test_helper_without_doc(_id)
self.assertTrue(foo.__doc__.startswith('Test idempotent id: %s' % _id))
@@ -101,7 +77,7 @@
class TestServicesDecorator(BaseDecoratorsTest):
def _test_services_helper(self, *decorator_args):
class TestFoo(test.BaseTestCase):
- @test.services(*decorator_args)
+ @utils.services(*decorator_args)
def test_bar(self):
return 0
@@ -125,14 +101,14 @@
'bad_service')
def test_services_decorator_with_service_valid_and_unavailable(self):
- self.useFixture(mockpatch.PatchObject(test.CONF.service_available,
- 'cinder', False))
+ self.useFixture(fixtures.MockPatchObject(test.CONF.service_available,
+ 'cinder', False))
self.assertRaises(testtools.TestCase.skipException,
self._test_services_helper, 'compute',
'volume')
def test_services_list(self):
- service_list = test.get_service_list()
+ service_list = utils.get_service_list()
for service in service_list:
try:
self._test_services_helper(service)
@@ -140,7 +116,7 @@
self.fail('%s is not listed in the valid service tag list'
% service)
except KeyError:
- # NOTE(mtreinish): This condition is to test for a entry in
+ # NOTE(mtreinish): This condition is to test for an entry in
# the outer decorator list but not in the service_list dict.
# However, because we're looping over the service_list dict
# it's unlikely we'll trigger this. So manual review is still
@@ -154,36 +130,6 @@
continue
-class TestStressDecorator(BaseDecoratorsTest):
- def _test_stresstest_helper(self, expected_frequency='process',
- expected_inheritance=False,
- **decorator_args):
- @test.stresstest(**decorator_args)
- def foo():
- pass
- self.assertEqual(getattr(foo, 'st_class_setup_per'),
- expected_frequency)
- self.assertEqual(getattr(foo, 'st_allow_inheritance'),
- expected_inheritance)
- self.assertEqual(set(['stress']), getattr(foo, '__testtools_attrs'))
-
- def test_stresstest_decorator_default(self):
- self._test_stresstest_helper()
-
- def test_stresstest_decorator_class_setup_frequency(self):
- self._test_stresstest_helper('process', class_setup_per='process')
-
- def test_stresstest_decorator_class_setup_frequency_non_default(self):
- self._test_stresstest_helper(expected_frequency='application',
- class_setup_per='application')
-
- def test_stresstest_decorator_set_frequency_and_inheritance(self):
- self._test_stresstest_helper(expected_frequency='application',
- expected_inheritance=True,
- class_setup_per='application',
- allow_inheritance=True)
-
-
class TestRequiresExtDecorator(BaseDecoratorsTest):
def setUp(self):
super(TestRequiresExtDecorator, self).setUp()
@@ -193,7 +139,7 @@
def _test_requires_ext_helper(self, expected_to_skip=True,
**decorator_args):
class TestFoo(test.BaseTestCase):
- @test.requires_ext(**decorator_args)
+ @utils.requires_ext(**decorator_args)
def test_bar(self):
return 0
@@ -201,7 +147,13 @@
if expected_to_skip:
self.assertRaises(testtools.TestCase.skipException, t.test_bar)
else:
- self.assertEqual(t.test_bar(), 0)
+ try:
+ self.assertEqual(t.test_bar(), 0)
+ except testtools.TestCase.skipException:
+ # We caught a skipException but we didn't expect to skip
+ # this test so raise a hard test failure instead.
+ raise testtools.TestCase.failureException(
+ "Not supposed to skip")
def test_requires_ext_decorator(self):
self._test_requires_ext_helper(expected_to_skip=False,
@@ -213,7 +165,7 @@
service='compute')
def test_requires_ext_decorator_with_all_ext_enabled(self):
- cfg.CONF.set_default('api_extensions', 'all',
+ cfg.CONF.set_default('api_extensions', ['all'],
group='compute-feature-enabled')
self._test_requires_ext_helper(expected_to_skip=False,
extension='random_ext',
@@ -224,19 +176,3 @@
self._test_requires_ext_helper,
extension='enabled_ext',
service='bad_service')
-
-
-class TestSimpleNegativeDecorator(BaseDecoratorsTest):
- @test.SimpleNegativeAutoTest
- class FakeNegativeJSONTest(test.NegativeAutoTest):
- _schema = {}
-
- def test_testfunc_exist(self):
- self.assertIn("test_fake_negative", dir(self.FakeNegativeJSONTest))
-
- @mock.patch('tempest.test.NegativeAutoTest.execute')
- def test_testfunc_calls_execute(self, mock):
- obj = self.FakeNegativeJSONTest("test_fake_negative")
- self.assertIn("test_fake_negative", dir(obj))
- obj.test_fake_negative()
- mock.assert_called_once_with(self.FakeNegativeJSONTest._schema)
diff --git a/tempest/tests/test_glance_http.py b/tempest/tests/test_glance_http.py
deleted file mode 100644
index db9db34..0000000
--- a/tempest/tests/test_glance_http.py
+++ /dev/null
@@ -1,156 +0,0 @@
-# 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.
-
-import mock
-from oslotest import mockpatch
-import six
-from six.moves import http_client as httplib
-
-from tempest.common import glance_http
-from tempest import exceptions
-from tempest.tests import base
-from tempest.tests import fake_auth_provider
-from tempest.tests import fake_http
-
-
-class TestGlanceHTTPClient(base.TestCase):
-
- def setUp(self):
- super(TestGlanceHTTPClient, self).setUp()
- self.fake_http = fake_http.fake_httplib2(return_type=200)
- # NOTE(maurosr): using http here implies that we will be using httplib
- # directly. With https glance_client would use an httpS version, but
- # the real backend would still be httplib anyway and since we mock it
- # that there is no reason to care.
- self.endpoint = 'http://fake_url.com'
- self.fake_auth = fake_auth_provider.FakeAuthProvider()
-
- self.fake_auth.base_url = mock.MagicMock(return_value=self.endpoint)
-
- self.useFixture(mockpatch.PatchObject(
- httplib.HTTPConnection,
- 'request',
- side_effect=self.fake_http.request(self.endpoint)[1]))
- self.client = glance_http.HTTPClient(self.fake_auth, {})
-
- def _set_response_fixture(self, header, status, resp_body):
- resp = fake_http.fake_httplib(header, status=status,
- body=six.StringIO(resp_body))
- self.useFixture(mockpatch.PatchObject(httplib.HTTPConnection,
- 'getresponse', return_value=resp))
- return resp
-
- def test_raw_request(self):
- self._set_response_fixture({}, 200, 'fake_response_body')
- resp, body = self.client.raw_request('GET', '/images')
- self.assertEqual(200, resp.status)
- self.assertEqual('fake_response_body', body.read())
-
- def test_raw_request_with_response_chunked(self):
- self._set_response_fixture({}, 200, 'fake_response_body')
- self.useFixture(mockpatch.PatchObject(glance_http,
- 'CHUNKSIZE', 1))
- resp, body = self.client.raw_request('GET', '/images')
- self.assertEqual(200, resp.status)
- self.assertEqual('fake_response_body', body.read())
-
- def test_raw_request_chunked(self):
- self.useFixture(mockpatch.PatchObject(glance_http,
- 'CHUNKSIZE', 1))
- self.useFixture(mockpatch.PatchObject(httplib.HTTPConnection,
- 'endheaders'))
- self.useFixture(mockpatch.PatchObject(httplib.HTTPConnection,
- 'send'))
-
- self._set_response_fixture({}, 200, 'fake_response_body')
- req_body = six.StringIO('fake_request_body')
- resp, body = self.client.raw_request('PUT', '/images', body=req_body)
- self.assertEqual(200, resp.status)
- self.assertEqual('fake_response_body', body.read())
- call_count = httplib.HTTPConnection.send.call_count
- self.assertEqual(call_count - 1, req_body.tell())
-
- def test_get_connection_class_for_https(self):
- conn_class = self.client._get_connection_class('https')
- self.assertEqual(glance_http.VerifiedHTTPSConnection, conn_class)
-
- def test_get_connection_class_for_http(self):
- conn_class = (self.client._get_connection_class('http'))
- self.assertEqual(httplib.HTTPConnection, conn_class)
-
- def test_get_connection_http(self):
- self.assertTrue(isinstance(self.client._get_connection(),
- httplib.HTTPConnection))
-
- def test_get_connection_https(self):
- endpoint = 'https://fake_url.com'
- self.fake_auth.base_url = mock.MagicMock(return_value=endpoint)
- self.client = glance_http.HTTPClient(self.fake_auth, {})
- self.assertTrue(isinstance(self.client._get_connection(),
- glance_http.VerifiedHTTPSConnection))
-
- def test_get_connection_url_not_fount(self):
- self.useFixture(mockpatch.PatchObject(self.client, 'connection_class',
- side_effect=httplib.InvalidURL()
- ))
- self.assertRaises(exceptions.EndpointNotFound,
- self.client._get_connection)
-
- def test_get_connection_kwargs_default_for_http(self):
- kwargs = self.client._get_connection_kwargs('http')
- self.assertEqual(600, kwargs['timeout'])
- self.assertEqual(1, len(kwargs.keys()))
-
- def test_get_connection_kwargs_set_timeout_for_http(self):
- kwargs = self.client._get_connection_kwargs('http', timeout=10,
- ca_certs='foo')
- self.assertEqual(10, kwargs['timeout'])
- # nothing more than timeout is evaluated for http connections
- self.assertEqual(1, len(kwargs.keys()))
-
- def test_get_connection_kwargs_default_for_https(self):
- kwargs = self.client._get_connection_kwargs('https')
- self.assertEqual(600, kwargs['timeout'])
- self.assertIsNone(kwargs['ca_certs'])
- self.assertIsNone(kwargs['cert_file'])
- self.assertIsNone(kwargs['key_file'])
- self.assertEqual(False, kwargs['insecure'])
- self.assertEqual(True, kwargs['ssl_compression'])
- self.assertEqual(6, len(kwargs.keys()))
-
- def test_get_connection_kwargs_set_params_for_https(self):
- kwargs = self.client._get_connection_kwargs('https', timeout=10,
- ca_certs='foo',
- cert_file='/foo/bar.cert',
- key_file='/foo/key.pem',
- insecure=True,
- ssl_compression=False)
- self.assertEqual(10, kwargs['timeout'])
- self.assertEqual('foo', kwargs['ca_certs'])
- self.assertEqual('/foo/bar.cert', kwargs['cert_file'])
- self.assertEqual('/foo/key.pem', kwargs['key_file'])
- self.assertEqual(True, kwargs['insecure'])
- self.assertEqual(False, kwargs['ssl_compression'])
- self.assertEqual(6, len(kwargs.keys()))
-
-
-class TestResponseBodyIterator(base.TestCase):
-
- def test_iter_default_chunk_size_64k(self):
- resp = fake_http.fake_httplib({}, six.StringIO(
- 'X' * (glance_http.CHUNKSIZE + 1)))
- iterator = glance_http.ResponseBodyIterator(resp)
- chunks = list(iterator)
- self.assertEqual(chunks, ['X' * glance_http.CHUNKSIZE, 'X'])
diff --git a/tempest/tests/test_hacking.py b/tempest/tests/test_hacking.py
index 55f00ef..bc3a753 100644
--- a/tempest/tests/test_hacking.py
+++ b/tempest/tests/test_hacking.py
@@ -86,13 +86,13 @@
def test_scenario_tests_need_service_tags(self):
self.assertFalse(checks.scenario_tests_need_service_tags(
'def test_fake:', './tempest/scenario/test_fake.py',
- "@test.services('compute')"))
+ "@utils.services('compute')"))
self.assertFalse(checks.scenario_tests_need_service_tags(
'def test_fake_test:', './tempest/api/compute/test_fake.py',
- "@test.services('image')"))
+ "@utils.services('image')"))
self.assertFalse(checks.scenario_tests_need_service_tags(
'def test_fake:', './tempest/scenario/orchestration/test_fake.py',
- "@test.services('compute')"))
+ "@utils.services('compute')"))
self.assertTrue(checks.scenario_tests_need_service_tags(
'def test_fake_test:', './tempest/scenario/test_fake.py',
'\n'))
@@ -113,12 +113,13 @@
def test_service_tags_not_in_module_path(self):
self.assertTrue(checks.service_tags_not_in_module_path(
- "@test.services('compute')", './tempest/api/compute/fake_test.py'))
+ "@utils.services('compute')",
+ './tempest/api/compute/fake_test.py'))
self.assertFalse(checks.service_tags_not_in_module_path(
- "@test.services('compute')",
+ "@utils.services('compute')",
'./tempest/scenario/compute/fake_test.py'))
self.assertFalse(checks.service_tags_not_in_module_path(
- "@test.services('compute')", './tempest/api/image/fake_test.py'))
+ "@utils.services('compute')", './tempest/api/image/fake_test.py'))
def test_no_hyphen_at_end_of_rand_name(self):
self.assertIsNone(checks.no_hyphen_at_end_of_rand_name(
@@ -147,3 +148,48 @@
" @testtools.skipUnless(CONF.something, 'msg')"))))
self.assertEqual(0, len(list(checks.no_testtools_skip_decorator(
" @testtools.skipIf(CONF.something, 'msg')"))))
+
+ def test_dont_import_local_tempest_code_into_lib(self):
+ self.assertEqual(0, len(list(checks.dont_import_local_tempest_into_lib(
+ "from tempest.common import waiters",
+ './tempest/common/compute.py'))))
+ self.assertEqual(0, len(list(checks.dont_import_local_tempest_into_lib(
+ "from tempest import config",
+ './tempest/common/compute.py'))))
+ self.assertEqual(0, len(list(checks.dont_import_local_tempest_into_lib(
+ "import tempest.exception",
+ './tempest/common/compute.py'))))
+ self.assertEqual(1, len(list(checks.dont_import_local_tempest_into_lib(
+ "from tempest.common import waiters",
+ './tempest/lib/common/compute.py'))))
+ self.assertEqual(1, len(list(checks.dont_import_local_tempest_into_lib(
+ "from tempest import config",
+ './tempest/lib/common/compute.py'))))
+ self.assertEqual(1, len(list(checks.dont_import_local_tempest_into_lib(
+ "import tempest.exception",
+ './tempest/lib/common/compute.py'))))
+
+ def test_dont_use_config_in_tempest_lib(self):
+ self.assertFalse(list(checks.dont_use_config_in_tempest_lib(
+ 'from tempest import config', './tempest/common/compute.py')))
+ self.assertFalse(list(checks.dont_use_config_in_tempest_lib(
+ 'from oslo_concurrency import lockutils',
+ './tempest/lib/auth.py')))
+ self.assertTrue(list(checks.dont_use_config_in_tempest_lib(
+ 'from tempest import config', './tempest/lib/auth.py')))
+ self.assertTrue(list(checks.dont_use_config_in_tempest_lib(
+ 'from oslo_config import cfg', './tempest/lib/decorators.py')))
+ self.assertTrue(list(checks.dont_use_config_in_tempest_lib(
+ 'import tempest.config', './tempest/lib/common/rest_client.py')))
+
+ def test_unsupported_exception_attribute_PY3(self):
+ self.assertEqual(len(list(checks.unsupported_exception_attribute_PY3(
+ "raise TestCase.failureException(e.message)"))), 1)
+ self.assertEqual(len(list(checks.unsupported_exception_attribute_PY3(
+ "raise TestCase.failureException(ex.message)"))), 1)
+ self.assertEqual(len(list(checks.unsupported_exception_attribute_PY3(
+ "raise TestCase.failureException(exc.message)"))), 1)
+ self.assertEqual(len(list(checks.unsupported_exception_attribute_PY3(
+ "raise TestCase.failureException(exception.message)"))), 1)
+ self.assertEqual(len(list(checks.unsupported_exception_attribute_PY3(
+ "raise TestCase.failureException(ee.message)"))), 0)
diff --git a/tempest/tests/test_imports.py b/tempest/tests/test_imports.py
new file mode 100644
index 0000000..6f1cfca
--- /dev/null
+++ b/tempest/tests/test_imports.py
@@ -0,0 +1,69 @@
+# Copyright 2017 IBM Corp.
+#
+# 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 mock
+
+from tempest.tests import base
+
+
+class ConfCounter(object):
+
+ def __init__(self, *args, **kwargs):
+ self.count = 0
+
+ def __getattr__(self, key):
+ self.count += 1
+ return mock.MagicMock()
+
+ def get_counts(self):
+ return self.count
+
+
+class TestImports(base.TestCase):
+ def setUp(self):
+ super(TestImports, self).setUp()
+ self.conf_mock = self.patch('tempest.config.CONF',
+ new_callable=ConfCounter)
+
+ def test_account_generator_command_import(self):
+ from tempest.cmd import account_generator # noqa
+ self.assertEqual(0, self.conf_mock.get_counts())
+
+ def test_cleanup_command_import(self):
+ from tempest.cmd import cleanup # noqa
+ self.assertEqual(0, self.conf_mock.get_counts())
+
+ def test_init_command_import(self):
+ from tempest.cmd import init # noqa
+ self.assertEqual(0, self.conf_mock.get_counts())
+
+ def test_list_plugins_command_import(self):
+ from tempest.cmd import list_plugins # noqa
+ self.assertEqual(0, self.conf_mock.get_counts())
+
+ def test_run_command_import(self):
+ from tempest.cmd import run # noqa
+ self.assertEqual(0, self.conf_mock.get_counts())
+
+ def test_subunit_descibe_command_import(self):
+ from tempest.cmd import subunit_describe_calls # noqa
+ self.assertEqual(0, self.conf_mock.get_counts())
+
+ def test_verify_tempest_config_command_import(self):
+ from tempest.cmd import verify_tempest_config # noqa
+ self.assertEqual(0, self.conf_mock.get_counts())
+
+ def test_workspace_command_import(self):
+ from tempest.cmd import workspace # noqa
+ self.assertEqual(0, self.conf_mock.get_counts())
diff --git a/tempest/tests/test_list_tests.py b/tempest/tests/test_list_tests.py
index 38d4c5c..4af7463 100644
--- a/tempest/tests/test_list_tests.py
+++ b/tempest/tests/test_list_tests.py
@@ -14,20 +14,19 @@
import os
import re
-import six
import subprocess
+import six
+
from tempest.tests import base
class TestTestList(base.TestCase):
- def test_testr_list_tests_no_errors(self):
- # Remove unit test discover path from env to test tempest tests
+ def test_stestr_list_no_errors(self):
test_env = os.environ.copy()
- test_env.pop('OS_TEST_PATH')
import_failures = []
- p = subprocess.Popen(['testr', 'list-tests'], stdout=subprocess.PIPE,
+ p = subprocess.Popen(['stestr', 'list'], stdout=subprocess.PIPE,
env=test_env)
ids, err = p.communicate()
self.assertEqual(0, p.returncode,
diff --git a/tempest/tests/test_microversions.py b/tempest/tests/test_microversions.py
new file mode 100644
index 0000000..ee6db71
--- /dev/null
+++ b/tempest/tests/test_microversions.py
@@ -0,0 +1,154 @@
+# Copyright 2015 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 oslo_config import cfg
+import six
+import testtools
+
+from tempest.api.compute import base as compute_base
+from tempest import config
+from tempest.lib import exceptions
+from tempest.tests import base
+from tempest.tests import fake_config
+
+
+class VersionTestNoneTolatest(compute_base.BaseV2ComputeTest):
+ min_microversion = None
+ max_microversion = 'latest'
+
+
+class VersionTestNoneTo2_2(compute_base.BaseV2ComputeTest):
+ min_microversion = None
+ max_microversion = '2.2'
+
+
+class VersionTest2_3ToLatest(compute_base.BaseV2ComputeTest):
+ min_microversion = '2.3'
+ max_microversion = 'latest'
+
+
+class VersionTest2_5To2_10(compute_base.BaseV2ComputeTest):
+ min_microversion = '2.5'
+ max_microversion = '2.10'
+
+
+class VersionTest2_10To2_10(compute_base.BaseV2ComputeTest):
+ min_microversion = '2.10'
+ max_microversion = '2.10'
+
+
+class InvalidVersionTest(compute_base.BaseV2ComputeTest):
+ min_microversion = '2.11'
+ max_microversion = '2.1'
+
+
+class TestMicroversionsTestsClass(base.TestCase):
+
+ def setUp(self):
+ super(TestMicroversionsTestsClass, self).setUp()
+ self.useFixture(fake_config.ConfigFixture())
+ self.patchobject(config, 'TempestConfigPrivate',
+ fake_config.FakePrivate)
+
+ def _test_version(self, cfg_min, cfg_max,
+ expected_pass_tests,
+ expected_skip_tests):
+ cfg.CONF.set_default('min_microversion',
+ cfg_min, group='compute')
+ cfg.CONF.set_default('max_microversion',
+ cfg_max, group='compute')
+ try:
+ for test_class in expected_pass_tests:
+ test_class.skip_checks()
+ for test_class in expected_skip_tests:
+ self.assertRaises(testtools.TestCase.skipException,
+ test_class.skip_checks)
+ except testtools.TestCase.skipException as e:
+ raise testtools.TestCase.failureException(six.text_type(e))
+
+ def test_config_version_none_none(self):
+ expected_pass_tests = [VersionTestNoneTolatest, VersionTestNoneTo2_2]
+ expected_skip_tests = [VersionTest2_3ToLatest, VersionTest2_5To2_10,
+ VersionTest2_10To2_10]
+ self._test_version(None, None,
+ expected_pass_tests,
+ expected_skip_tests)
+
+ def test_config_version_none_23(self):
+ expected_pass_tests = [VersionTestNoneTolatest, VersionTestNoneTo2_2,
+ VersionTest2_3ToLatest]
+ expected_skip_tests = [VersionTest2_5To2_10, VersionTest2_10To2_10]
+ self._test_version(None, '2.3',
+ expected_pass_tests,
+ expected_skip_tests)
+
+ def test_config_version_22_latest(self):
+ expected_pass_tests = [VersionTestNoneTolatest, VersionTestNoneTo2_2,
+ VersionTest2_3ToLatest, VersionTest2_5To2_10,
+ VersionTest2_10To2_10]
+ expected_skip_tests = []
+ self._test_version('2.2', 'latest',
+ expected_pass_tests,
+ expected_skip_tests)
+
+ def test_config_version_22_23(self):
+ expected_pass_tests = [VersionTestNoneTolatest, VersionTestNoneTo2_2,
+ VersionTest2_3ToLatest]
+ expected_skip_tests = [VersionTest2_5To2_10, VersionTest2_10To2_10]
+ self._test_version('2.2', '2.3',
+ expected_pass_tests,
+ expected_skip_tests)
+
+ def test_config_version_210_210(self):
+ expected_pass_tests = [VersionTestNoneTolatest,
+ VersionTest2_3ToLatest,
+ VersionTest2_5To2_10,
+ VersionTest2_10To2_10]
+ expected_skip_tests = [VersionTestNoneTo2_2]
+ self._test_version('2.10', '2.10',
+ expected_pass_tests,
+ expected_skip_tests)
+
+ def test_config_version_none_latest(self):
+ expected_pass_tests = [VersionTestNoneTolatest, VersionTestNoneTo2_2,
+ VersionTest2_3ToLatest, VersionTest2_5To2_10,
+ VersionTest2_10To2_10]
+ expected_skip_tests = []
+ self._test_version(None, 'latest',
+ expected_pass_tests,
+ expected_skip_tests)
+
+ def test_config_version_latest_latest(self):
+ expected_pass_tests = [VersionTestNoneTolatest, VersionTest2_3ToLatest]
+ expected_skip_tests = [VersionTestNoneTo2_2, VersionTest2_5To2_10,
+ VersionTest2_10To2_10]
+ self._test_version('latest', 'latest',
+ expected_pass_tests,
+ expected_skip_tests)
+
+ def test_config_invalid_version(self):
+ cfg.CONF.set_default('min_microversion',
+ '2.5', group='compute')
+ cfg.CONF.set_default('max_microversion',
+ '2.1', group='compute')
+ self.assertRaises(exceptions.InvalidAPIVersionRange,
+ VersionTestNoneTolatest.skip_checks)
+
+ def test_config_version_invalid_test_version(self):
+ cfg.CONF.set_default('min_microversion',
+ None, group='compute')
+ cfg.CONF.set_default('max_microversion',
+ '2.13', group='compute')
+ self.assertRaises(exceptions.InvalidAPIVersionRange,
+ InvalidVersionTest.skip_checks)
diff --git a/tempest/tests/test_negative_rest_client.py b/tempest/tests/test_negative_rest_client.py
deleted file mode 100644
index ce95739..0000000
--- a/tempest/tests/test_negative_rest_client.py
+++ /dev/null
@@ -1,91 +0,0 @@
-# (c) 2015 Deutsche Telekom AG
-# Copyright 2015 Red Hat, Inc.
-# Copyright 2015 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.
-
-import httplib2
-from oslotest import mockpatch
-
-from tempest.common import negative_rest_client
-from tempest import config
-from tempest.tests import base
-from tempest.tests import fake_auth_provider
-from tempest.tests import fake_config
-from tempest.tests import fake_http
-
-
-class TestNegativeRestClient(base.TestCase):
-
- url = 'fake_endpoint'
-
- def setUp(self):
- self.fake_http = fake_http.fake_httplib2()
- super(TestNegativeRestClient, self).setUp()
- self.useFixture(fake_config.ConfigFixture())
- self.stubs.Set(config, 'TempestConfigPrivate', fake_config.FakePrivate)
- self.stubs.Set(httplib2.Http, 'request', self.fake_http.request)
- self.negative_rest_client = negative_rest_client.NegativeRestClient(
- fake_auth_provider.FakeAuthProvider(), None)
- self.useFixture(mockpatch.PatchObject(self.negative_rest_client,
- '_log_request'))
-
- def test_post(self):
- __, return_dict = self.negative_rest_client.send_request('POST',
- self.url,
- [], {})
- self.assertEqual('POST', return_dict['method'])
-
- def test_get(self):
- __, return_dict = self.negative_rest_client.send_request('GET',
- self.url,
- [])
- self.assertEqual('GET', return_dict['method'])
-
- def test_delete(self):
- __, return_dict = self.negative_rest_client.send_request('DELETE',
- self.url,
- [])
- self.assertEqual('DELETE', return_dict['method'])
-
- def test_patch(self):
- __, return_dict = self.negative_rest_client.send_request('PATCH',
- self.url,
- [], {})
- self.assertEqual('PATCH', return_dict['method'])
-
- def test_put(self):
- __, return_dict = self.negative_rest_client.send_request('PUT',
- self.url,
- [], {})
- self.assertEqual('PUT', return_dict['method'])
-
- def test_head(self):
- self.useFixture(mockpatch.PatchObject(self.negative_rest_client,
- 'response_checker'))
- __, return_dict = self.negative_rest_client.send_request('HEAD',
- self.url,
- [])
- self.assertEqual('HEAD', return_dict['method'])
-
- def test_copy(self):
- __, return_dict = self.negative_rest_client.send_request('COPY',
- self.url,
- [])
- self.assertEqual('COPY', return_dict['method'])
-
- def test_other(self):
- self.assertRaises(AssertionError,
- self.negative_rest_client.send_request,
- 'OTHER', self.url, [])
diff --git a/tempest/tests/test_tempest_plugin.py b/tempest/tests/test_tempest_plugin.py
index c07e98c..ddadef5 100644
--- a/tempest/tests/test_tempest_plugin.py
+++ b/tempest/tests/test_tempest_plugin.py
@@ -13,12 +13,20 @@
# License for the specific language governing permissions and limitations
# under the License.
+from tempest.lib.services import clients
from tempest.test_discover import plugins
from tempest.tests import base
from tempest.tests import fake_tempest_plugin as fake_plugin
+from tempest.tests.lib.services import registry_fixture
class TestPluginDiscovery(base.TestCase):
+
+ def setUp(self):
+ super(TestPluginDiscovery, self).setUp()
+ # Make sure we leave the registry clean
+ self.useFixture(registry_fixture.RegistryFixture())
+
def test_load_tests_with_one_plugin(self):
# we can't mock stevedore since it's a singleton and already executed
# during test discovery. So basically this test covers the plugin loop
@@ -42,3 +50,37 @@
result['fake01'])
self.assertEqual(fake_plugin.FakePlugin.expected_load_test,
result['fake02'])
+
+ def test__register_service_clients_with_one_plugin(self):
+ registry = clients.ClientsRegistry()
+ manager = plugins.TempestTestPluginManager()
+ fake_obj = fake_plugin.FakeStevedoreObj()
+ manager.ext_plugins = [fake_obj]
+ manager._register_service_clients()
+ expected_result = fake_plugin.FakePlugin.expected_service_clients
+ registered_clients = registry.get_service_clients()
+ self.assertIn(fake_obj.name, registered_clients)
+ self.assertEqual(expected_result, registered_clients[fake_obj.name])
+
+ def test__get_service_clients_with_two_plugins(self):
+ registry = clients.ClientsRegistry()
+ manager = plugins.TempestTestPluginManager()
+ obj1 = fake_plugin.FakeStevedoreObj('fake01')
+ obj2 = fake_plugin.FakeStevedoreObj('fake02')
+ manager.ext_plugins = [obj1, obj2]
+ manager._register_service_clients()
+ expected_result = fake_plugin.FakePlugin.expected_service_clients
+ registered_clients = registry.get_service_clients()
+ self.assertIn('fake01', registered_clients)
+ self.assertIn('fake02', registered_clients)
+ self.assertEqual(expected_result, registered_clients['fake01'])
+ self.assertEqual(expected_result, registered_clients['fake02'])
+
+ def test__register_service_clients_one_plugin_no_service_clients(self):
+ registry = clients.ClientsRegistry()
+ manager = plugins.TempestTestPluginManager()
+ fake_obj = fake_plugin.FakeStevedoreObjNoServiceClients()
+ manager.ext_plugins = [fake_obj]
+ manager._register_service_clients()
+ registered_clients = registry.get_service_clients()
+ self.assertNotIn(fake_obj.name, registered_clients)
diff --git a/tempest/tests/test_test.py b/tempest/tests/test_test.py
new file mode 100644
index 0000000..fc50736
--- /dev/null
+++ b/tempest/tests/test_test.py
@@ -0,0 +1,626 @@
+# Copyright 2017 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.
+
+import os
+import sys
+
+import mock
+from oslo_config import cfg
+import testtools
+
+from tempest import clients
+from tempest import config
+from tempest.lib.common import validation_resources as vr
+from tempest.lib import exceptions as lib_exc
+from tempest import test
+from tempest.tests import base
+from tempest.tests import fake_config
+from tempest.tests.lib import fake_credentials
+from tempest.tests.lib.services import registry_fixture
+
+
+if sys.version_info >= (2, 7):
+ import unittest
+else:
+ import unittest2 as unittest
+
+
+class LoggingTestResult(testtools.TestResult):
+
+ def __init__(self, log, *args, **kwargs):
+ super(LoggingTestResult, self).__init__(*args, **kwargs)
+ self.log = log
+
+ def addError(self, test, err=None, details=None):
+ self.log.append((test, err, details))
+
+
+class TestValidationResources(base.TestCase):
+
+ validation_resources_module = 'tempest.lib.common.validation_resources'
+
+ def setUp(self):
+ super(TestValidationResources, self).setUp()
+ self.useFixture(fake_config.ConfigFixture())
+ self.useFixture(registry_fixture.RegistryFixture())
+ self.patchobject(config, 'TempestConfigPrivate',
+ fake_config.FakePrivate)
+
+ class TestTestClass(test.BaseTestCase):
+ pass
+
+ self.test_test_class = TestTestClass
+
+ def test_validation_resources_no_validation(self):
+ cfg.CONF.set_default('run_validation', False, 'validation')
+ creds = fake_credentials.FakeKeystoneV3Credentials()
+ osclients = clients.Manager(creds)
+ vr = self.test_test_class.get_class_validation_resources(osclients)
+ self.assertIsNone(vr)
+
+ def test_validation_resources_exists(self):
+ cfg.CONF.set_default('run_validation', True, 'validation')
+ creds = fake_credentials.FakeKeystoneV3Credentials()
+ osclients = clients.Manager(creds)
+ expected_vr = 'expected_validation_resources'
+ self.test_test_class._validation_resources[osclients] = expected_vr
+ obtained_vr = self.test_test_class.get_class_validation_resources(
+ osclients)
+ self.assertEqual(expected_vr, obtained_vr)
+
+ @mock.patch(validation_resources_module + '.create_validation_resources',
+ autospec=True)
+ def test_validation_resources_new(self, mock_create_vr):
+ cfg.CONF.set_default('run_validation', True, 'validation')
+ cfg.CONF.set_default('neutron', True, 'service_available')
+ creds = fake_credentials.FakeKeystoneV3Credentials()
+ osclients = clients.Manager(creds)
+ expected_vr = {'expected_validation_resources': None}
+ mock_create_vr.return_value = expected_vr
+ with mock.patch.object(
+ self.test_test_class,
+ 'addClassResourceCleanup') as mock_add_class_cleanup:
+ obtained_vr = self.test_test_class.get_class_validation_resources(
+ osclients)
+ self.assertEqual(1, mock_add_class_cleanup.call_count)
+ self.assertEqual(mock.call(vr.clear_validation_resources,
+ osclients,
+ use_neutron=True,
+ **expected_vr),
+ mock_add_class_cleanup.call_args)
+ self.assertEqual(mock_create_vr.call_count, 1)
+ self.assertIn(osclients, mock_create_vr.call_args_list[0][0])
+ self.assertEqual(expected_vr, obtained_vr)
+ self.assertIn(osclients, self.test_test_class._validation_resources)
+ self.assertEqual(expected_vr,
+ self.test_test_class._validation_resources[osclients])
+
+ def test_validation_resources_invalid_config(self):
+ invalid_version = 999
+ cfg.CONF.set_default('run_validation', True, 'validation')
+ cfg.CONF.set_default('ip_version_for_ssh', invalid_version,
+ 'validation')
+ cfg.CONF.set_default('neutron', True, 'service_available')
+ creds = fake_credentials.FakeKeystoneV3Credentials()
+ osclients = clients.Manager(creds)
+ with testtools.ExpectedException(
+ lib_exc.InvalidConfiguration,
+ value_re='^.*\n.*' + str(invalid_version)):
+ self.test_test_class.get_class_validation_resources(osclients)
+
+ @mock.patch(validation_resources_module + '.create_validation_resources',
+ autospec=True)
+ def test_validation_resources_invalid_config_nova_net(self,
+ mock_create_vr):
+ invalid_version = 999
+ cfg.CONF.set_default('run_validation', True, 'validation')
+ cfg.CONF.set_default('ip_version_for_ssh', invalid_version,
+ 'validation')
+ cfg.CONF.set_default('neutron', False, 'service_available')
+ creds = fake_credentials.FakeKeystoneV3Credentials()
+ osclients = clients.Manager(creds)
+ expected_vr = {'expected_validation_resources': None}
+ mock_create_vr.return_value = expected_vr
+ obtained_vr = self.test_test_class.get_class_validation_resources(
+ osclients)
+ self.assertEqual(mock_create_vr.call_count, 1)
+ self.assertIn(osclients, mock_create_vr.call_args_list[0][0])
+ self.assertEqual(expected_vr, obtained_vr)
+ self.assertIn(osclients, self.test_test_class._validation_resources)
+ self.assertEqual(expected_vr,
+ self.test_test_class._validation_resources[osclients])
+
+ @mock.patch(validation_resources_module + '.create_validation_resources',
+ autospec=True)
+ @mock.patch(validation_resources_module + '.clear_validation_resources',
+ autospec=True)
+ def test_validation_resources_fixture(self, mock_clean_vr, mock_create_vr):
+
+ class TestWithRun(self.test_test_class):
+
+ def runTest(self):
+ pass
+
+ cfg.CONF.set_default('run_validation', True, 'validation')
+ test_case = TestWithRun()
+ creds = fake_credentials.FakeKeystoneV3Credentials()
+ osclients = clients.Manager(creds)
+ test_case.get_test_validation_resources(osclients)
+ self.assertEqual(1, mock_create_vr.call_count)
+ self.assertEqual(0, mock_clean_vr.call_count)
+
+
+class TestSetNetworkResources(base.TestCase):
+
+ def setUp(self):
+ super(TestSetNetworkResources, self).setUp()
+
+ class ParentTest(test.BaseTestCase):
+
+ @classmethod
+ def setup_credentials(cls):
+ cls.set_network_resources(dhcp=True)
+ super(ParentTest, cls).setup_credentials()
+
+ def runTest(self):
+ pass
+
+ self.parent_class = ParentTest
+
+ def test_set_network_resources_child_only(self):
+
+ class ChildTest(self.parent_class):
+
+ @classmethod
+ def setup_credentials(cls):
+ cls.set_network_resources(router=True)
+ super(ChildTest, cls).setup_credentials()
+
+ child_test = ChildTest()
+ child_test.setUpClass()
+ # Assert that the parents network resources are not set
+ self.assertFalse(child_test._network_resources['dhcp'])
+ # Assert that the child network resources are set
+ self.assertTrue(child_test._network_resources['router'])
+
+ def test_set_network_resources_right_order(self):
+
+ class ChildTest(self.parent_class):
+
+ @classmethod
+ def setup_credentials(cls):
+ super(ChildTest, cls).setup_credentials()
+ cls.set_network_resources(router=True)
+
+ child_test = ChildTest()
+ with testtools.ExpectedException(RuntimeError,
+ value_re='set_network_resources'):
+ child_test.setUpClass()
+
+ def test_set_network_resources_children(self):
+
+ class ChildTest(self.parent_class):
+
+ @classmethod
+ def setup_credentials(cls):
+ cls.set_network_resources(router=True)
+ super(ChildTest, cls).setup_credentials()
+
+ class GrandChildTest(ChildTest):
+ pass
+
+ # Invoke setupClass on both and check that the setup_credentials
+ # call check mechanism does not report any false negative.
+ child_test = ChildTest()
+ child_test.setUpClass()
+ grandchild_test = GrandChildTest()
+ grandchild_test.setUpClass()
+
+
+class TestTempestBaseTestClass(base.TestCase):
+
+ def setUp(self):
+ super(TestTempestBaseTestClass, self).setUp()
+ self.useFixture(fake_config.ConfigFixture())
+ self.patchobject(config, 'TempestConfigPrivate',
+ fake_config.FakePrivate)
+
+ class ParentTest(test.BaseTestCase):
+
+ def runTest(self):
+ pass
+
+ self.parent_test = ParentTest
+
+ def test_resource_cleanup(self):
+ cfg.CONF.set_default('neutron', False, 'service_available')
+ exp_args = (1, 2,)
+ exp_kwargs = {'a': 1, 'b': 2}
+ mock1 = mock.Mock()
+ mock2 = mock.Mock()
+ exp_functions = [mock1, mock2]
+
+ class TestWithCleanups(self.parent_test):
+
+ @classmethod
+ def resource_setup(cls):
+ for fn in exp_functions:
+ cls.addClassResourceCleanup(fn, *exp_args,
+ **exp_kwargs)
+
+ test_cleanups = TestWithCleanups()
+ suite = unittest.TestSuite((test_cleanups,))
+ log = []
+ result = LoggingTestResult(log)
+ suite.run(result)
+ # No exception raised - error log is empty
+ self.assertFalse(log)
+ # All stacked resource cleanups invoked
+ mock1.assert_called_once_with(*exp_args, **exp_kwargs)
+ mock2.assert_called_once_with(*exp_args, **exp_kwargs)
+ # Cleanup stack is empty
+ self.assertEqual(0, len(test_cleanups._class_cleanups))
+
+ def test_resource_cleanup_failures(self):
+ cfg.CONF.set_default('neutron', False, 'service_available')
+ exp_args = (1, 2,)
+ exp_kwargs = {'a': 1, 'b': 2}
+ mock1 = mock.Mock()
+ mock1.side_effect = Exception('mock1 resource cleanup failure')
+ mock2 = mock.Mock()
+ mock3 = mock.Mock()
+ mock3.side_effect = Exception('mock3 resource cleanup failure')
+ exp_functions = [mock1, mock2, mock3]
+
+ class TestWithFailingCleanups(self.parent_test):
+
+ @classmethod
+ def resource_setup(cls):
+ for fn in exp_functions:
+ cls.addClassResourceCleanup(fn, *exp_args,
+ **exp_kwargs)
+
+ test_cleanups = TestWithFailingCleanups()
+ suite = unittest.TestSuite((test_cleanups,))
+ log = []
+ result = LoggingTestResult(log)
+ suite.run(result)
+ # One multiple exception captured
+ self.assertEqual(1, len(log))
+ # [0]: test, err, details [1] -> exc_info
+ # Type, Exception, traceback [1] -> MultipleException
+ found_exc = log[0][1][1]
+ self.assertTrue(isinstance(found_exc, testtools.MultipleExceptions))
+ self.assertEqual(2, len(found_exc.args))
+ # Each arg is exc_info - match messages and order
+ self.assertIn('mock3 resource', str(found_exc.args[0][1]))
+ self.assertIn('mock1 resource', str(found_exc.args[1][1]))
+ # All stacked resource cleanups invoked
+ mock1.assert_called_once_with(*exp_args, **exp_kwargs)
+ mock2.assert_called_once_with(*exp_args, **exp_kwargs)
+ # Cleanup stack is empty
+ self.assertEqual(0, len(test_cleanups._class_cleanups))
+
+ def test_super_resource_cleanup_not_invoked(self):
+
+ class BadResourceCleanup(self.parent_test):
+
+ @classmethod
+ def resource_cleanup(cls):
+ pass
+
+ bad_class = BadResourceCleanup()
+ suite = unittest.TestSuite((bad_class,))
+ log = []
+ result = LoggingTestResult(log)
+ suite.run(result)
+ # One multiple exception captured
+ self.assertEqual(1, len(log))
+ # [0]: test, err, details [1] -> exc_info
+ # Type, Exception, traceback [1] -> RuntimeError
+ found_exc = log[0][1][1]
+ self.assertTrue(isinstance(found_exc, RuntimeError))
+ self.assertIn(BadResourceCleanup.__name__, str(found_exc))
+
+ def test_super_skip_checks_not_invoked(self):
+
+ class BadSkipChecks(self.parent_test):
+
+ @classmethod
+ def skip_checks(cls):
+ pass
+
+ bad_class = BadSkipChecks()
+ with testtools.ExpectedException(
+ RuntimeError,
+ value_re='^.* ' + BadSkipChecks.__name__):
+ bad_class.setUpClass()
+
+ def test_super_setup_credentials_not_invoked(self):
+
+ class BadSetupCredentials(self.parent_test):
+
+ @classmethod
+ def skip_checks(cls):
+ pass
+
+ bad_class = BadSetupCredentials()
+ with testtools.ExpectedException(
+ RuntimeError,
+ value_re='^.* ' + BadSetupCredentials.__name__):
+ bad_class.setUpClass()
+
+ def test_grandparent_skip_checks_not_invoked(self):
+
+ class BadSkipChecks(self.parent_test):
+
+ @classmethod
+ def skip_checks(cls):
+ pass
+
+ class SonOfBadSkipChecks(BadSkipChecks):
+ pass
+
+ bad_class = SonOfBadSkipChecks()
+ with testtools.ExpectedException(
+ RuntimeError,
+ value_re='^.* ' + SonOfBadSkipChecks.__name__):
+ bad_class.setUpClass()
+
+ @mock.patch('tempest.common.credentials_factory.is_admin_available',
+ autospec=True, return_value=True)
+ def test_skip_checks_admin(self, mock_iaa):
+ identity_version = 'identity_version'
+
+ class NeedAdmin(self.parent_test):
+ credentials = ['admin']
+
+ @classmethod
+ def get_identity_version(cls):
+ return identity_version
+
+ NeedAdmin().skip_checks()
+ mock_iaa.assert_called_once_with('identity_version')
+
+ @mock.patch('tempest.common.credentials_factory.is_admin_available',
+ autospec=True, return_value=False)
+ def test_skip_checks_admin_not_available(self, mock_iaa):
+ identity_version = 'identity_version'
+
+ class NeedAdmin(self.parent_test):
+ credentials = ['admin']
+
+ @classmethod
+ def get_identity_version(cls):
+ return identity_version
+
+ with testtools.ExpectedException(testtools.testcase.TestSkipped):
+ NeedAdmin().skip_checks()
+ mock_iaa.assert_called_once_with('identity_version')
+
+ def test_skip_checks_identity_v2_not_available(self):
+ cfg.CONF.set_default('api_v2', False, 'identity-feature-enabled')
+
+ class NeedV2(self.parent_test):
+ identity_version = 'v2'
+
+ with testtools.ExpectedException(testtools.testcase.TestSkipped):
+ NeedV2().skip_checks()
+
+ def test_skip_checks_identity_v3_not_available(self):
+ cfg.CONF.set_default('api_v3', False, 'identity-feature-enabled')
+
+ class NeedV3(self.parent_test):
+ identity_version = 'v3'
+
+ with testtools.ExpectedException(testtools.testcase.TestSkipped):
+ NeedV3().skip_checks()
+
+ def test_setup_credentials_all(self):
+ expected_creds = ['string', ['list', 'role1', 'role2']]
+
+ class AllCredentials(self.parent_test):
+ credentials = expected_creds
+
+ expected_clients = 'clients'
+ with mock.patch.object(
+ AllCredentials,
+ 'get_client_manager') as mock_get_client_manager:
+ mock_get_client_manager.return_value = expected_clients
+ all_creds = AllCredentials()
+ all_creds.setup_credentials()
+ self.assertTrue(hasattr(all_creds, 'os_string'))
+ self.assertEqual(expected_clients, all_creds.os_string)
+ self.assertTrue(hasattr(all_creds, 'os_roles_list'))
+ self.assertEqual(expected_clients, all_creds.os_roles_list)
+ self.assertEqual(2, mock_get_client_manager.call_count)
+ self.assertEqual(
+ expected_creds[0],
+ mock_get_client_manager.mock_calls[0][2]['credential_type'])
+ self.assertEqual(
+ expected_creds[1][1:],
+ mock_get_client_manager.mock_calls[1][2]['roles'])
+
+ def test_setup_class_overwritten(self):
+
+ class OverridesSetup(self.parent_test):
+
+ @classmethod
+ def setUpClass(cls): # noqa
+ pass
+
+ overrides_setup = OverridesSetup()
+ suite = unittest.TestSuite((overrides_setup,))
+ log = []
+ result = LoggingTestResult(log)
+ suite.run(result)
+ # Record 0, test (error holder). The error generates during test run.
+ self.assertIn('runTest', str(log[0][0]))
+ # Record 0, traceback
+ self.assertRegex(
+ str(log[0][2]['traceback']).replace('\n', ' '),
+ RuntimeError.__name__ + ': .* ' + OverridesSetup.__name__)
+
+
+class TestTempestBaseTestClassFixtures(base.TestCase):
+
+ SETUP_FIXTURES = [test.BaseTestCase.setUpClass.__name__,
+ test.BaseTestCase.skip_checks.__name__,
+ test.BaseTestCase.setup_credentials.__name__,
+ test.BaseTestCase.setup_clients.__name__,
+ test.BaseTestCase.resource_setup.__name__]
+ TEARDOWN_FIXTURES = [test.BaseTestCase.tearDownClass.__name__,
+ test.BaseTestCase.resource_cleanup.__name__,
+ test.BaseTestCase.clear_credentials.__name__]
+
+ def setUp(self):
+ super(TestTempestBaseTestClassFixtures, self).setUp()
+ self.mocks = {}
+ for fix in self.SETUP_FIXTURES + self.TEARDOWN_FIXTURES:
+ self.mocks[fix] = mock.Mock()
+
+ def tracker_builder(name):
+
+ def tracker(cls):
+ # Track that the fixture was invoked
+ cls.fixtures_invoked.append(name)
+ # Run the fixture
+ getattr(super(TestWithClassFixtures, cls), name)()
+ # Run a mock we can use for side effects
+ self.mocks[name]()
+
+ return tracker
+
+ class TestWithClassFixtures(test.BaseTestCase):
+
+ credentials = []
+ fixtures_invoked = []
+
+ def runTest(_self):
+ pass
+
+ # Decorate all test class fixtures with tracker_builder
+ for method_name in self.SETUP_FIXTURES + self.TEARDOWN_FIXTURES:
+ setattr(TestWithClassFixtures, method_name,
+ classmethod(tracker_builder(method_name)))
+
+ self.test = TestWithClassFixtures()
+
+ def test_no_error_flow(self):
+ # If all setup fixtures are executed, all cleanup fixtures are
+ # executed too
+ suite = unittest.TestSuite((self.test,))
+ log = []
+ result = LoggingTestResult(log)
+ suite.run(result)
+ self.assertEqual(self.SETUP_FIXTURES + self.TEARDOWN_FIXTURES,
+ self.test.fixtures_invoked)
+
+ def test_skip_only(self):
+ # If a skip condition is hit in the test, no credentials or resource
+ # is provisioned / cleaned-up
+ self.mocks['skip_checks'].side_effect = (
+ testtools.testcase.TestSkipped())
+ suite = unittest.TestSuite((self.test,))
+ log = []
+ result = LoggingTestResult(log)
+ suite.run(result)
+ # If we trigger a skip condition, teardown is not invoked at all
+ self.assertEqual(self.SETUP_FIXTURES[:2],
+ self.test.fixtures_invoked)
+
+ def test_skip_credentials_fails(self):
+ expected_exc = 'sc exploded'
+ self.mocks['setup_credentials'].side_effect = Exception(expected_exc)
+ suite = unittest.TestSuite((self.test,))
+ log = []
+ result = LoggingTestResult(log)
+ suite.run(result)
+ # If setup_credentials explodes, we invoked teardown class and
+ # clear credentials, and re-raise
+ self.assertEqual((self.SETUP_FIXTURES[:3] +
+ [self.TEARDOWN_FIXTURES[i] for i in (0, 2)]),
+ self.test.fixtures_invoked)
+ found_exc = log[0][1][1]
+ self.assertIn(expected_exc, str(found_exc))
+
+ def test_skip_credentials_fails_clear_fails(self):
+ # If cleanup fails on failure, we log the exception and do not
+ # re-raise it. Note that since the exception happens outside of
+ # the Tempest test setUp, logging is not captured on the Tempest
+ # test side, it will be captured by the unit test instead.
+ expected_exc = 'sc exploded'
+ clear_exc = 'clear exploded'
+ self.mocks['setup_credentials'].side_effect = Exception(expected_exc)
+ self.mocks['clear_credentials'].side_effect = Exception(clear_exc)
+ suite = unittest.TestSuite((self.test,))
+ log = []
+ result = LoggingTestResult(log)
+ suite.run(result)
+ # If setup_credentials explodes, we invoked teardown class and
+ # clear credentials, and re-raise
+ self.assertEqual((self.SETUP_FIXTURES[:3] +
+ [self.TEARDOWN_FIXTURES[i] for i in (0, 2)]),
+ self.test.fixtures_invoked)
+ found_exc = log[0][1][1]
+ self.assertIn(expected_exc, str(found_exc))
+ # Since log capture depends on OS_LOG_CAPTURE, we can only assert if
+ # logging was captured
+ if os.environ.get('OS_LOG_CAPTURE'):
+ self.assertIn(clear_exc, self.log_fixture.logger.output)
+
+ def test_skip_credentials_clients_resources_credentials_clear_fails(self):
+ # If cleanup fails with no previous failure, we re-raise the exception.
+ expected_exc = 'clear exploded'
+ self.mocks['clear_credentials'].side_effect = Exception(expected_exc)
+ suite = unittest.TestSuite((self.test,))
+ log = []
+ result = LoggingTestResult(log)
+ suite.run(result)
+ # If setup_credentials explodes, we invoked teardown class and
+ # clear credentials, and re-raise
+ self.assertEqual(self.SETUP_FIXTURES + self.TEARDOWN_FIXTURES,
+ self.test.fixtures_invoked)
+ found_exc = log[0][1][1]
+ self.assertIn(expected_exc, str(found_exc))
+
+ def test_skip_credentials_clients_fails(self):
+ expected_exc = 'clients exploded'
+ self.mocks['setup_clients'].side_effect = Exception(expected_exc)
+ suite = unittest.TestSuite((self.test,))
+ log = []
+ result = LoggingTestResult(log)
+ suite.run(result)
+ # If setup_clients explodes, we invoked teardown class and
+ # clear credentials, and re-raise
+ self.assertEqual((self.SETUP_FIXTURES[:4] +
+ [self.TEARDOWN_FIXTURES[i] for i in (0, 2)]),
+ self.test.fixtures_invoked)
+ found_exc = log[0][1][1]
+ self.assertIn(expected_exc, str(found_exc))
+
+ def test_skip_credentials_clients_resources_fails(self):
+ expected_exc = 'resource setup exploded'
+ self.mocks['resource_setup'].side_effect = Exception(expected_exc)
+ suite = unittest.TestSuite((self.test,))
+ log = []
+ result = LoggingTestResult(log)
+ suite.run(result)
+ # If resource_setup explodes, we invoked teardown class and
+ # clear credentials and resource cleanup, and re-raise
+ self.assertEqual(self.SETUP_FIXTURES + self.TEARDOWN_FIXTURES,
+ self.test.fixtures_invoked)
+ found_exc = log[0][1][1]
+ self.assertIn(expected_exc, str(found_exc))
diff --git a/tempest/tests/test_wrappers.py b/tempest/tests/test_wrappers.py
deleted file mode 100644
index a4ef699..0000000
--- a/tempest/tests/test_wrappers.py
+++ /dev/null
@@ -1,88 +0,0 @@
-# Copyright 2013 IBM Corp.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-import os
-import shutil
-import subprocess
-import tempfile
-
-import six
-
-from tempest.tests import base
-
-DEVNULL = open(os.devnull, 'wb')
-
-
-class TestWrappers(base.TestCase):
- def setUp(self):
- super(TestWrappers, self).setUp()
- # Setup test dirs
- self.directory = tempfile.mkdtemp(prefix='tempest-unit')
- self.addCleanup(shutil.rmtree, self.directory)
- self.test_dir = os.path.join(self.directory, 'tests')
- os.mkdir(self.test_dir)
- # Setup Test files
- self.testr_conf_file = os.path.join(self.directory, '.testr.conf')
- self.setup_cfg_file = os.path.join(self.directory, 'setup.cfg')
- self.passing_file = os.path.join(self.test_dir, 'test_passing.py')
- self.failing_file = os.path.join(self.test_dir, 'test_failing.py')
- self.init_file = os.path.join(self.test_dir, '__init__.py')
- self.setup_py = os.path.join(self.directory, 'setup.py')
- shutil.copy('tempest/tests/files/testr-conf', self.testr_conf_file)
- shutil.copy('tempest/tests/files/passing-tests', self.passing_file)
- shutil.copy('tempest/tests/files/failing-tests', self.failing_file)
- shutil.copy('setup.py', self.setup_py)
- shutil.copy('tempest/tests/files/setup.cfg', self.setup_cfg_file)
- shutil.copy('tempest/tests/files/__init__.py', self.init_file)
- # copy over the pretty_tox scripts
- shutil.copy('tools/pretty_tox.sh',
- os.path.join(self.directory, 'pretty_tox.sh'))
- shutil.copy('tools/pretty_tox_serial.sh',
- os.path.join(self.directory, 'pretty_tox_serial.sh'))
-
- self.stdout = six.StringIO()
- self.stderr = six.StringIO()
- # Change directory, run wrapper and check result
- self.addCleanup(os.chdir, os.path.abspath(os.curdir))
- os.chdir(self.directory)
-
- def assertRunExit(self, cmd, expected):
- p = subprocess.Popen(
- "bash %s" % cmd, shell=True,
- stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- out, err = p.communicate()
-
- self.assertEqual(
- p.returncode, expected,
- "Stdout: %s; Stderr: %s" % (out, err))
-
- def test_pretty_tox(self):
- # Git init is required for the pbr testr command. pbr requires a git
- # version or an sdist to work. so make the test directory a git repo
- # too.
- subprocess.call(['git', 'init'], stderr=DEVNULL)
- self.assertRunExit('pretty_tox.sh passing', 0)
-
- def test_pretty_tox_fails(self):
- # Git init is required for the pbr testr command. pbr requires a git
- # version or an sdist to work. so make the test directory a git repo
- # too.
- subprocess.call(['git', 'init'], stderr=DEVNULL)
- self.assertRunExit('pretty_tox.sh', 1)
-
- def test_pretty_tox_serial(self):
- self.assertRunExit('pretty_tox_serial.sh passing', 0)
-
- def test_pretty_tox_serial_fails(self):
- self.assertRunExit('pretty_tox_serial.sh', 1)
diff --git a/tempest/tests/utils.py b/tempest/tests/utils.py
new file mode 100644
index 0000000..9c3049d
--- /dev/null
+++ b/tempest/tests/utils.py
@@ -0,0 +1,29 @@
+# Copyright 2016 OpenStack Foundation
+#
+# 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.
+#
+
+
+def generate_timeout_series(timeout):
+ """Generate a series of times that exceeds the given timeout.
+
+ Yields a series of fake time.time() floating point numbers
+ such that the difference between each pair in the series just
+ exceeds the timeout value that is passed in. Useful for
+ mocking time.time() in methods that otherwise wait for timeout
+ seconds.
+ """
+ iteration = 0
+ while True:
+ iteration += 1
+ yield (iteration * timeout) + iteration
diff --git a/tempest/version.py b/tempest/version.py
new file mode 100644
index 0000000..bc9f651
--- /dev/null
+++ b/tempest/version.py
@@ -0,0 +1,18 @@
+# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P.
+#
+# 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 pbr.version
+
+version_info = pbr.version.VersionInfo('tempest')
diff --git a/test-requirements.txt b/test-requirements.txt
index 5b01ea9..e33f207 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,12 +1,8 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
-hacking<0.11,>=0.10.0
-# needed for doc build
-sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2
-python-subunit>=0.0.18
-oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0
-mox>=0.5.3
-mock>=1.2
-coverage>=3.6
-oslotest>=1.10.0 # Apache-2.0
+hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0
+mock>=2.0.0 # BSD
+coverage!=4.4,>=4.0 # Apache-2.0
+oslotest>=3.2.0 # Apache-2.0
+flake8-import-order==0.11 # LGPLv3
diff --git a/tools/check_logs.py b/tools/check_logs.py
index c8d3a1a..b80ccc0 100755
--- a/tools/check_logs.py
+++ b/tools/check_logs.py
@@ -19,13 +19,12 @@
import gzip
import os
import re
-import StringIO
import sys
-import urllib2
+import six
+import six.moves.urllib.request as urlreq
import yaml
-
# DEVSTACK_GATE_GRENADE is either unset if grenade is not running
# or a string describing what type of grenade run to perform.
is_grenade = os.environ.get('DEVSTACK_GATE_GRENADE') is not None
@@ -54,7 +53,6 @@
'q-meta',
'q-metering',
'q-svc',
- 'q-vpn',
's-proxy'])
@@ -64,21 +62,21 @@
for (name, filename) in file_specs:
whitelist = whitelists.get(name, [])
with open(filename) as content:
- if scan_content(name, content, regexp, whitelist):
+ if scan_content(content, regexp, whitelist):
logs_with_errors.append(name)
for (name, url) in url_specs:
whitelist = whitelists.get(name, [])
- req = urllib2.Request(url)
+ req = urlreq.Request(url)
req.add_header('Accept-Encoding', 'gzip')
- page = urllib2.urlopen(req)
- buf = StringIO.StringIO(page.read())
+ page = urlreq.urlopen(req)
+ buf = six.StringIO(page.read())
f = gzip.GzipFile(fileobj=buf)
- if scan_content(name, f.read().splitlines(), regexp, whitelist):
+ if scan_content(f.read().splitlines(), regexp, whitelist):
logs_with_errors.append(name)
return logs_with_errors
-def scan_content(name, content, regexp, whitelist):
+def scan_content(content, regexp, whitelist):
had_errors = False
for line in content:
if not line.startswith("Stderr:") and regexp.match(line):
@@ -96,7 +94,7 @@
def collect_url_logs(url):
- page = urllib2.urlopen(url)
+ page = urlreq.urlopen(url)
content = page.read()
logs = re.findall('(screen-[\w-]+\.txt\.gz)</a>', content)
return logs
@@ -105,7 +103,7 @@
def main(opts):
if opts.directory and opts.url or not (opts.directory or opts.url):
print("Must provide exactly one of -d or -u")
- exit(1)
+ return 1
print("Checking logs...")
WHITELIST_FILE = os.path.join(
os.path.abspath(os.path.dirname(os.path.dirname(__file__))),
@@ -138,7 +136,7 @@
with open(WHITELIST_FILE) as stream:
loaded = yaml.safe_load(stream)
if loaded:
- for (name, l) in loaded.iteritems():
+ for (name, l) in six.iteritems(loaded):
for w in l:
assert 'module' in w, 'no module in %s' % name
assert 'message' in w, 'no message in %s' % name
diff --git a/tools/find_stack_traces.py b/tools/find_stack_traces.py
deleted file mode 100755
index 4862d01..0000000
--- a/tools/find_stack_traces.py
+++ /dev/null
@@ -1,159 +0,0 @@
-#!/usr/bin/env python
-
-# Copyright 2013 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.
-
-import gzip
-import pprint
-import re
-import StringIO
-import sys
-import urllib2
-
-
-pp = pprint.PrettyPrinter()
-
-NOVA_TIMESTAMP = r"\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d"
-
-NOVA_REGEX = r"(?P<timestamp>%s) (?P<pid>\d+ )?(?P<level>(ERROR|TRACE)) " \
- "(?P<module>[\w\.]+) (?P<msg>.*)" % (NOVA_TIMESTAMP)
-
-
-class StackTrace(object):
- timestamp = None
- pid = None
- level = ""
- module = ""
- msg = ""
-
- def __init__(self, timestamp=None, pid=None, level="", module="",
- msg=""):
- self.timestamp = timestamp
- self.pid = pid
- self.level = level
- self.module = module
- self.msg = msg
-
- def append(self, msg):
- self.msg = self.msg + msg
-
- def is_same(self, data):
- return (data['timestamp'] == self.timestamp and
- data['level'] == self.level)
-
- def not_none(self):
- return self.timestamp is not None
-
- def __str__(self):
- buff = "<%s %s %s>\n" % (self.timestamp, self.level, self.module)
- for line in self.msg.splitlines():
- buff = buff + line + "\n"
- return buff
-
-
-def hunt_for_stacktrace(url):
- """Return TRACE or ERROR lines out of logs."""
- req = urllib2.Request(url)
- req.add_header('Accept-Encoding', 'gzip')
- page = urllib2.urlopen(req)
- buf = StringIO.StringIO(page.read())
- f = gzip.GzipFile(fileobj=buf)
- content = f.read()
-
- traces = []
- trace = StackTrace()
- for line in content.splitlines():
- m = re.match(NOVA_REGEX, line)
- if m:
- data = m.groupdict()
- if trace.not_none() and trace.is_same(data):
- trace.append(data['msg'] + "\n")
- else:
- trace = StackTrace(
- timestamp=data.get('timestamp'),
- pid=data.get('pid'),
- level=data.get('level'),
- module=data.get('module'),
- msg=data.get('msg'))
-
- else:
- if trace.not_none():
- traces.append(trace)
- trace = StackTrace()
-
- # once more at the end to pick up any stragglers
- if trace.not_none():
- traces.append(trace)
-
- return traces
-
-
-def log_url(url, log):
- return "%s/%s" % (url, log)
-
-
-def collect_logs(url):
- page = urllib2.urlopen(url)
- content = page.read()
- logs = re.findall('(screen-[\w-]+\.txt\.gz)</a>', content)
- return logs
-
-
-def usage():
- print("""
-Usage: find_stack_traces.py <logurl>
-
-Hunts for stack traces in a devstack run. Must provide it a base log url
-from a tempest devstack run. Should start with http and end with /logs/.
-
-Returns a report listing stack traces out of the various files where
-they are found.
-""")
- sys.exit(0)
-
-
-def print_stats(items, fname, verbose=False):
- errors = len(filter(lambda x: x.level == "ERROR", items))
- traces = len(filter(lambda x: x.level == "TRACE", items))
- print("%d ERRORS found in %s" % (errors, fname))
- print("%d TRACES found in %s" % (traces, fname))
-
- if verbose:
- for item in items:
- print(item)
- print("\n\n")
-
-
-def main():
- if len(sys.argv) == 2:
- url = sys.argv[1]
- loglist = collect_logs(url)
-
- # probably wrong base url
- if not loglist:
- usage()
-
- for log in loglist:
- logurl = log_url(url, log)
- traces = hunt_for_stacktrace(logurl)
-
- if traces:
- print_stats(traces, log, verbose=True)
-
- else:
- usage()
-
-if __name__ == '__main__':
- main()
diff --git a/tools/generate-tempest-plugins-list.py b/tools/generate-tempest-plugins-list.py
new file mode 100644
index 0000000..bbb9019
--- /dev/null
+++ b/tools/generate-tempest-plugins-list.py
@@ -0,0 +1,91 @@
+#! /usr/bin/env python
+
+# Copyright 2016 Hewlett Packard Enterprise Development Company, L.P.
+#
+# 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.
+
+# This script is intended to be run as part of a periodic proposal bot
+# job in OpenStack infrastructure.
+#
+# In order to function correctly, the environment in which the
+# script runs must have
+# * network access to the review.openstack.org Gerrit API
+# working directory
+# * network access to https://git.openstack.org/cgit
+
+import json
+import re
+
+try:
+ # For Python 3.0 and later
+ from urllib.error import HTTPError
+ import urllib.request as urllib
+except ImportError:
+ # Fall back to Python 2's urllib2
+ import urllib2 as urllib
+ from urllib2 import HTTPError
+
+
+url = 'https://review.openstack.org/projects/'
+
+# This is what a project looks like
+'''
+ "openstack-attic/akanda": {
+ "id": "openstack-attic%2Fakanda",
+ "state": "READ_ONLY"
+ },
+'''
+
+
+def is_in_openstack_namespace(proj):
+ return proj.startswith('openstack/')
+
+# Rather than returning a 404 for a nonexistent file, cgit delivers a
+# 0-byte response to a GET request. It also does not provide a
+# Content-Length in a HEAD response, so the way we tell if a file exists
+# is to check the length of the entire GET response body.
+
+
+def has_tempest_plugin(proj):
+ try:
+ r = urllib.urlopen(
+ "https://git.openstack.org/cgit/%s/plain/setup.cfg" % proj)
+ except HTTPError as err:
+ if err.code == 404:
+ return False
+ p = re.compile('^tempest\.test_plugins', re.M)
+ if p.findall(r.read().decode('utf-8')):
+ return True
+ else:
+ False
+
+r = urllib.urlopen(url)
+# Gerrit prepends 4 garbage octets to the JSON, in order to counter
+# cross-site scripting attacks. Therefore we must discard it so the
+# json library won't choke.
+projects = sorted(filter(is_in_openstack_namespace, json.loads(r.read()[4:])))
+
+# 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))
+
+# Every element of the found_plugins list begins with "openstack/".
+# We drop those initial 10 octets when printing the list.
+for project in found_plugins:
+ print(project[10:])
diff --git a/tools/generate-tempest-plugins-list.sh b/tools/generate-tempest-plugins-list.sh
new file mode 100755
index 0000000..20c99b2
--- /dev/null
+++ b/tools/generate-tempest-plugins-list.sh
@@ -0,0 +1,66 @@
+#!/usr/bin/env bash
+
+# Copyright 2016 Hewlett Packard Enterprise Development Company, L.P.
+#
+# 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.
+
+# This script is intended to be run as a periodic proposal bot job
+# in OpenStack infrastructure, though you can run it as a one-off.
+#
+# In order to function correctly, the environment in which the
+# script runs must have
+# * a writable doc/source directory relative to the current
+# working directory
+# AND ( (
+# * git
+# * all git repos meant to be searched for plugins cloned and
+# at the desired level of up-to-datedness
+# * the environment variable git_dir pointing to the location
+# * of said git repositories
+# ) OR (
+# * network access to the review.openstack.org Gerrit API
+# working directory
+# * network access to https://git.openstack.org/cgit
+# ))
+#
+# If a file named doc/source/data/tempest-plugins-registry.header or
+# doc/source/data/tempest-plugins-registry.footer is found relative to the
+# current working directory, it will be prepended or appended to
+# the generated reStructuredText plugins table respectively.
+
+set -ex
+
+(
+declare -A plugins
+
+if [[ -r doc/source/data/tempest-plugins-registry.header ]]; then
+ cat doc/source/data/tempest-plugins-registry.header
+fi
+
+sorted_plugins=$(python tools/generate-tempest-plugins-list.py)
+
+for k in ${sorted_plugins}; do
+ project=${k:0:28}
+ giturl="git://git.openstack.org/openstack/${k:0:26}"
+ printf "|%-28s|%-73s|\n" "${project}" "${giturl}"
+ printf "+----------------------------+-------------------------------------------------------------------------+\n"
+done
+
+if [[ -r doc/source/data/tempest-plugins-registry.footer ]]; then
+ cat doc/source/data/tempest-plugins-registry.footer
+fi
+) > doc/source/plugin-registry.rst
+
+if [[ -n ${1} ]]; then
+ cp doc/source/plugin-registry.rst ${1}/doc/source/plugin-registry.rst
+fi
diff --git a/tools/install_venv.py b/tools/install_venv.py
deleted file mode 100644
index d6d9c8e..0000000
--- a/tools/install_venv.py
+++ /dev/null
@@ -1,71 +0,0 @@
-# Copyright 2010 United States Government as represented by the
-# Administrator of the National Aeronautics and Space Administration.
-# All Rights Reserved.
-#
-# Copyright 2010 OpenStack Foundation
-# Copyright 2013 IBM Corp.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-import os
-import sys
-
-import install_venv_common as install_venv # noqa
-
-
-def print_help(venv, root):
- help = """
- OpenStack development environment setup is complete.
-
- OpenStack development uses virtualenv to track and manage Python
- dependencies while in development and testing.
-
- To activate the OpenStack virtualenv for the extent of your current shell
- session you can run:
-
- $ source %s/bin/activate
-
- Or, if you prefer, you can run commands in the virtualenv on a case by case
- basis by running:
-
- $ %s/tools/with_venv.sh <your command>
-
- Also, make test will automatically use the virtualenv.
- """
- print(help % (venv, root))
-
-
-def main(argv):
- root = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
-
- if os.environ.get('TOOLS_PATH'):
- root = os.environ['TOOLS_PATH']
- venv = os.path.join(root, '.venv')
- if os.environ.get('VENV'):
- venv = os.environ['VENV']
-
- pip_requires = os.path.join(root, 'requirements.txt')
- test_requires = os.path.join(root, 'test-requirements.txt')
- py_version = "python%s.%s" % (sys.version_info[0], sys.version_info[1])
- project = 'Tempest'
- install = install_venv.InstallVenv(root, venv, pip_requires, test_requires,
- py_version, project)
- options = install.parse_args(argv)
- install.check_python_version()
- install.check_dependencies()
- install.create_virtualenv(no_site_packages=options.no_site_packages)
- install.install_dependencies()
- print_help(venv, root)
-
-if __name__ == '__main__':
- main(sys.argv)
diff --git a/tools/install_venv_common.py b/tools/install_venv_common.py
deleted file mode 100644
index e279159..0000000
--- a/tools/install_venv_common.py
+++ /dev/null
@@ -1,172 +0,0 @@
-# Copyright 2013 OpenStack Foundation
-# Copyright 2013 IBM Corp.
-#
-# 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.
-
-"""Provides methods needed by installation script for OpenStack development
-virtual environments.
-
-Since this script is used to bootstrap a virtualenv from the system's Python
-environment, it should be kept strictly compatible with Python 2.6.
-
-Synced in from openstack-common
-"""
-
-from __future__ import print_function
-
-import optparse
-import os
-import subprocess
-import sys
-
-
-class InstallVenv(object):
-
- def __init__(self, root, venv, requirements,
- test_requirements, py_version,
- project):
- self.root = root
- self.venv = venv
- self.requirements = requirements
- self.test_requirements = test_requirements
- self.py_version = py_version
- self.project = project
-
- def die(self, message, *args):
- print(message % args, file=sys.stderr)
- sys.exit(1)
-
- def check_python_version(self):
- if sys.version_info < (2, 6):
- self.die("Need Python Version >= 2.6")
-
- def run_command_with_code(self, cmd, redirect_output=True,
- check_exit_code=True):
- """Runs a command in an out-of-process shell.
-
- Returns the output of that command. Working directory is self.root.
- """
- if redirect_output:
- stdout = subprocess.PIPE
- else:
- stdout = None
-
- proc = subprocess.Popen(cmd, cwd=self.root, stdout=stdout)
- output = proc.communicate()[0]
- if check_exit_code and proc.returncode != 0:
- self.die('Command "%s" failed.\n%s', ' '.join(cmd), output)
- return (output, proc.returncode)
-
- def run_command(self, cmd, redirect_output=True, check_exit_code=True):
- return self.run_command_with_code(cmd, redirect_output,
- check_exit_code)[0]
-
- def get_distro(self):
- if (os.path.exists('/etc/fedora-release') or
- os.path.exists('/etc/redhat-release')):
- return Fedora(
- self.root, self.venv, self.requirements,
- self.test_requirements, self.py_version, self.project)
- else:
- return Distro(
- self.root, self.venv, self.requirements,
- self.test_requirements, self.py_version, self.project)
-
- def check_dependencies(self):
- self.get_distro().install_virtualenv()
-
- def create_virtualenv(self, no_site_packages=True):
- """Creates the virtual environment and installs PIP.
-
- Creates the virtual environment and installs PIP only into the
- virtual environment.
- """
- if not os.path.isdir(self.venv):
- print('Creating venv...', end=' ')
- if no_site_packages:
- self.run_command(['virtualenv', '-q', '--no-site-packages',
- self.venv])
- else:
- self.run_command(['virtualenv', '-q', self.venv])
- print('done.')
- else:
- print("venv already exists...")
- pass
-
- def pip_install(self, *args):
- self.run_command(['tools/with_venv.sh',
- 'pip', 'install', '--upgrade'] + list(args),
- redirect_output=False)
-
- def install_dependencies(self):
- print('Installing dependencies with pip (this can take a while)...')
-
- # First things first, make sure our venv has the latest pip and
- # setuptools and pbr
- self.pip_install('pip>=1.4')
- self.pip_install('setuptools')
- self.pip_install('pbr')
-
- self.pip_install('-r', self.requirements, '-r', self.test_requirements)
-
- def parse_args(self, argv):
- """Parses command-line arguments."""
- parser = optparse.OptionParser()
- parser.add_option('-n', '--no-site-packages',
- action='store_true',
- help="Do not inherit packages from global Python "
- "install.")
- return parser.parse_args(argv[1:])[0]
-
-
-class Distro(InstallVenv):
-
- def check_cmd(self, cmd):
- return bool(self.run_command(['which', cmd],
- check_exit_code=False).strip())
-
- def install_virtualenv(self):
- if self.check_cmd('virtualenv'):
- return
-
- if self.check_cmd('easy_install'):
- print('Installing virtualenv via easy_install...', end=' ')
- if self.run_command(['easy_install', 'virtualenv']):
- print('Succeeded')
- return
- else:
- print('Failed')
-
- self.die('ERROR: virtualenv not found.\n\n%s development'
- ' requires virtualenv, please install it using your'
- ' favorite package management tool' % self.project)
-
-
-class Fedora(Distro):
- """This covers all Fedora-based distributions.
-
- Includes: Fedora, RHEL, CentOS, Scientific Linux
- """
-
- def check_pkg(self, pkg):
- return self.run_command_with_code(['rpm', '-q', pkg],
- check_exit_code=False)[1] == 0
-
- def install_virtualenv(self):
- if self.check_cmd('virtualenv'):
- return
-
- if not self.check_pkg('python-virtualenv'):
- self.die("Please install 'python-virtualenv'.")
-
- super(Fedora, self).install_virtualenv()
diff --git a/tools/pretty_tox.sh b/tools/pretty_tox.sh
deleted file mode 100755
index fb4e6d5..0000000
--- a/tools/pretty_tox.sh
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/usr/bin/env bash
-
-set -o pipefail
-
-TESTRARGS=$1
-python setup.py testr --testr-args="--subunit $TESTRARGS" | subunit-trace --no-failure-debug -f
-retval=$?
-# NOTE(mtreinish) The pipe above would eat the slowest display from pbr's testr
-# wrapper so just manually print the slowest tests.
-echo -e "\nSlowest Tests:\n"
-testr slowest
-exit $retval
diff --git a/tools/pretty_tox_serial.sh b/tools/pretty_tox_serial.sh
deleted file mode 100755
index e0fca0f..0000000
--- a/tools/pretty_tox_serial.sh
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/usr/bin/env bash
-
-set -o pipefail
-
-TESTRARGS=$@
-
-if [ ! -d .testrepository ]; then
- testr init
-fi
-testr run --subunit $TESTRARGS | subunit-trace -f -n
-retval=$?
-testr slowest
-
-exit $retval
diff --git a/tools/skip_tracker.py b/tools/skip_tracker.py
index a47e217..44f5874 100755
--- a/tools/skip_tracker.py
+++ b/tools/skip_tracker.py
@@ -20,125 +20,10 @@
is fixed but a skip is still in the Tempest test code
"""
-import logging
-import os
-import re
-
-from launchpadlib import launchpad
-
-BASEDIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
-TESTDIR = os.path.join(BASEDIR, 'tempest')
-LPCACHEDIR = os.path.expanduser('~/.launchpadlib/cache')
-
-
-def info(msg, *args, **kwargs):
- logging.info(msg, *args, **kwargs)
-
-
-def debug(msg, *args, **kwargs):
- logging.debug(msg, *args, **kwargs)
-
-
-def find_skips(start=TESTDIR):
- """Find skipped tests
-
- Returns a list of tuples (method, bug) that represent
- test methods that have been decorated to skip because of
- a particular bug.
- """
- results = {}
- debug("Searching in %s", start)
- for root, _dirs, files in os.walk(start):
- for name in files:
- if name.startswith('test_') and name.endswith('py'):
- path = os.path.join(root, name)
- debug("Searching in %s", path)
- temp_result = find_skips_in_file(path)
- for method_name, bug_no in temp_result:
- if results.get(bug_no):
- result_dict = results.get(bug_no)
- if result_dict.get(name):
- result_dict[name].append(method_name)
- else:
- result_dict[name] = [method_name]
- results[bug_no] = result_dict
- else:
- results[bug_no] = {name: [method_name]}
- return results
-
-
-def find_skips_in_file(path):
- """Return the skip tuples in a test file"""
- BUG_RE = re.compile(r'\s*@.*skip_because\(bug=[\'"](\d+)[\'"]')
- DEF_RE = re.compile(r'\s*def (\w+)\(')
- bug_found = False
- results = []
- lines = open(path, 'rb').readlines()
- for x, line in enumerate(lines):
- if not bug_found:
- res = BUG_RE.match(line)
- if res:
- bug_no = int(res.group(1))
- debug("Found bug skip %s on line %d", bug_no, x + 1)
- bug_found = True
- else:
- res = DEF_RE.match(line)
- if res:
- method = res.group(1)
- debug("Found test method %s skips for bug %d", method, bug_no)
- results.append((method, bug_no))
- bug_found = False
- return results
-
-
-def get_results(result_dict):
- results = []
- for bug_no in result_dict.keys():
- for method in result_dict[bug_no]:
- results.append((method, bug_no))
- return results
+from tempest.lib.cmd import skip_tracker
if __name__ == '__main__':
- logging.basicConfig(format='%(levelname)s: %(message)s',
- level=logging.INFO)
- results = find_skips()
- unique_bugs = sorted(set([bug for (method, bug) in get_results(results)]))
- unskips = []
- duplicates = []
- info("Total bug skips found: %d", len(results))
- info("Total unique bugs causing skips: %d", len(unique_bugs))
- lp = launchpad.Launchpad.login_anonymously('grabbing bugs',
- 'production',
- LPCACHEDIR)
- for bug_no in unique_bugs:
- bug = lp.bugs[bug_no]
- duplicate = bug.duplicate_of_link
- if duplicate is not None:
- dup_id = duplicate.split('/')[-1]
- duplicates.append((bug_no, dup_id))
- for task in bug.bug_tasks:
- info("Bug #%7s (%12s - %12s)", bug_no,
- task.importance, task.status)
- if task.status in ('Fix Released', 'Fix Committed'):
- unskips.append(bug_no)
-
- for bug_id, dup_id in duplicates:
- if bug_id not in unskips:
- dup_bug = lp.bugs[dup_id]
- for task in dup_bug.bug_tasks:
- info("Bug #%7s is a duplicate of Bug#%7s (%12s - %12s)",
- bug_id, dup_id, task.importance, task.status)
- if task.status in ('Fix Released', 'Fix Committed'):
- unskips.append(bug_id)
-
- unskips = sorted(set(unskips))
- if unskips:
- print("The following bugs have been fixed and the corresponding skips")
- print("should be removed from the test cases:")
- print()
- for bug in unskips:
- message = " %7s in " % bug
- locations = ["%s" % x for x in results[bug].keys()]
- message += " and ".join(locations)
- print(message)
+ print("DEPRECATED: `skip_tracker.py` is already deprecated, "
+ "use `skip-tracker` command instead.")
+ skip_tracker.main()
diff --git a/tools/tempest-plugin-sanity.sh b/tools/tempest-plugin-sanity.sh
new file mode 100644
index 0000000..8b4f913
--- /dev/null
+++ b/tools/tempest-plugin-sanity.sh
@@ -0,0 +1,127 @@
+#!/usr/bin/env bash
+
+# Copyright 2017 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.
+
+# This script is intended to check the sanity of tempest plugins against
+# tempest master.
+# What it does:
+# * Creates the virtualenv
+# * Install tempest
+# * Retrieve the project lists having tempest plugin if project name is
+# given.
+# * For each project in a list, It does:
+# * Clone the Project
+# * Install the Project and also installs dependencies from
+# test-requirements.txt.
+# * Create Tempest workspace
+# * List tempest plugins
+# * List tempest plugins tests
+# * Uninstall the project and its dependencies
+# * Again Install tempest
+# * Again repeat the step from cloning project
+#
+# If one of the step fails, The script will exit with failure.
+
+if [ "$1" == "-h" ]; then
+ echo -e "This script performs the sanity of tempest plugins to find
+configuration and dependency issues with the tempest.\n
+Usage: sh ./tools/tempest-plugin-sanity.sh [Run sanity on tempest plugins]"
+ exit 0
+fi
+
+set -ex
+
+# retrieve a list of projects having tempest plugins
+PROJECT_LIST="$(python tools/generate-tempest-plugins-list.py)"
+# List of projects having tempest plugin stale or unmaintained from long time
+BLACKLIST="trio2o"
+
+# Function to clone project using zuul-cloner or from git
+function clone_project() {
+ if [ -e /usr/zuul-env/bin/zuul-cloner ]; then
+ /usr/zuul-env/bin/zuul-cloner --cache-dir /opt/git \
+ git://git.openstack.org \
+ openstack/"$1"
+
+ elif [ -e /usr/bin/git ]; then
+ /usr/bin/git clone git://git.openstack.org/openstack/"$1" \
+ openstack/"$1"
+
+ fi
+}
+
+# Create virtualenv to perform sanity operation
+SANITY_DIR=$(pwd)
+virtualenv "$SANITY_DIR"/.venv
+export TVENV="$SANITY_DIR/tools/with_venv.sh"
+cd "$SANITY_DIR"
+
+# Install tempest in a venv
+"$TVENV" pip install .
+
+# Function to install project
+function install_project() {
+ "$TVENV" pip install "$SANITY_DIR"/openstack/"$1"
+ # Check for test-requirements.txt file in a project then install it.
+ if [ -e "$SANITY_DIR"/openstack/"$1"/test-requirements.txt ]; then
+ "$TVENV" pip install -r "$SANITY_DIR"/openstack/"$1"/test-requirements.txt
+ fi
+}
+
+# Function to perform sanity checking on Tempest plugin
+function tempest_sanity() {
+ "$TVENV" tempest init "$SANITY_DIR"/tempest_sanity
+ cd "$SANITY_DIR"/tempest_sanity
+ "$TVENV" tempest list-plugins
+ "$TVENV" tempest run -l
+ # Delete tempest workspace
+ "$TVENV" tempest workspace remove --name tempest_sanity --rmdir
+ cd "$SANITY_DIR"
+}
+
+# Function to uninstall project
+function uninstall_project() {
+ "$TVENV" pip uninstall -y "$SANITY_DIR"/openstack/"$1"
+ # Check for *requirements.txt file in a project then uninstall it.
+ if [ -e "$SANITY_DIR"/openstack/"$1"/*requirements.txt ]; then
+ "$TVENV" pip uninstall -y -r "$SANITY_DIR"/openstack/"$1"/*requirements.txt
+ fi
+ # Remove the project directory after sanity run
+ rm -fr "$SANITY_DIR"/openstack/"$1"
+}
+
+# Function to run sanity check on each project
+function plugin_sanity_check() {
+ clone_project "$1" && install_project "$1" && tempest_sanity "$1" \
+ && uninstall_project "$1" && "$TVENV" pip install .
+}
+
+# Log status
+passed_plugin=''
+failed_plugin=''
+# Perform sanity on all tempest plugin projects
+for project in $PROJECT_LIST; do
+ # Remove blacklisted tempest plugins
+ if ! [[ `echo $BLACKLIST | grep -c $project ` -gt 0 ]]; then
+ plugin_sanity_check $project && passed_plugin+=", $project" || \
+ failed_plugin+=", $project"
+ fi
+done
+
+# Check for failed status
+if [[ -n $failed_plugin ]]; then
+ exit 1
+fi
diff --git a/tools/use_tempest_lib.sh b/tools/use_tempest_lib.sh
deleted file mode 100755
index ca62c4a..0000000
--- a/tools/use_tempest_lib.sh
+++ /dev/null
@@ -1,141 +0,0 @@
-#!/bin/bash
-#
-# Use this script to use interfaces/files from tempest-lib.
-# Many files have been migrated to tempest-lib and tempest has
-# its own copy too.
-# This script helps to remove those from tempest and make use of tempest-lib.
-# It adds the change-id of each file on which they were migrated in lib.
-# This should only be done for files which were migrated to lib with
-# "Migrated" in commit message as done by tempest-lib/tools/migrate_from_tempest.sh script.
-# "Migrated" keyword is used to fetch the migration commit history from lib.
-# To use:
-# 1. Create a new branch in the tempest repo so not to destroy your current
-# working branch
-# 2. Run the script from the repo dir and specify the file paths relative to
-# the root tempest dir(only code and unit tests):
-#
-# tools/use_tempest_lib.sh.sh tempest/file1.py tempest/file2.py
-
-
-function usage {
- echo "Usage: $0 [OPTION] file1 file2 .."
- echo "Use files from tempest-lib"
- echo -e "Input files should be tempest files with path. \n Example- tempest/file1.py tempest/file2.py .."
- echo ""
- echo "-s, --service_client Specify if files are service clients."
- echo "-u, --tempest_lib_git_url Specify the repo to clone tempest-lib from."
-}
-
-function check_all_files_valid {
- failed=0
- for file in $files; do
- # Get the latest change-id for each file
- latest_commit_id=`git log -n1 -- $file | grep "^commit" | awk '{print $2}'`
- cd $tmpdir
- filename=`basename $file`
- lib_path=`find ./ -name $filename`
- if [ -z $lib_path ]; then
- echo "ERROR: $filename does not exist in tempest-lib."
- failed=$(( failed + 1))
- cd - > /dev/null
- continue
- fi
- # Get the CHANGE_ID of tempest-lib patch where file was migrated
- migration_change_id=`git log -n1 --grep "Migrated" -- $lib_path | grep "Change-Id: " | awk '{print $2}'`
- MIGRATION_IDS=`echo -e "$MIGRATION_IDS\n * $filename: $migration_change_id"`
- # Get tempest CHANGE_ID of file which was migrated to lib
- migrated_change_id=`git log -n1 --grep "Migrated" -- $lib_path | grep "* $filename"`
- migrated_change_id=${migrated_change_id#*:}
- cd - > /dev/null
- # Get the commit-id of tempest which was migrated to tempest-lib
- migrated_commit_id=`git log --grep "$migrated_change_id" -- $file | grep "^commit" | awk '{print $2}'`
- DIFF=$(git diff $latest_commit_id $migrated_commit_id $file)
- if [ "$DIFF" != "" ]; then
- echo "ERROR: $filename in tempest has been updated after migration to tempest-lib. First sync the file to tempest-lib."
- failed=$(( failed + 1))
- fi
- done
- if [[ $failed -gt 0 ]]; then
- echo "$failed files had issues"
- exit $failed
- fi
-}
-
-set -e
-
-service_client=0
-file_list=''
-
-while [ $# -gt 0 ]; do
- case "$1" in
- -h|--help) usage; exit;;
- -u|--tempest_lib_git_url) tempest_lib_git_url="$2"; shift;;
- -s|--service_client) service_client=1;;
- *) files="$files $1";;
- esac
- shift
-done
-
-if [ -z "$files" ]; then
- usage; exit
-fi
-
-TEMPEST_LIB_GIT_URL=${tempest_lib_git_url:-git://git.openstack.org/openstack/tempest-lib}
-
-tmpdir=$(mktemp -d -t use-tempest-lib.XXXX)
-
-# Clone tempest-lib
-git clone $TEMPEST_LIB_GIT_URL $tmpdir
-
-# Checks all provided files are present in lib and
-# not updated in tempest after migration to lib.
-check_all_files_valid
-
-for file in $files; do
- rm -f $file
- tempest_dir=`pwd`
- tempest_dir="$tempest_dir/tempest/"
- tempest_dirname=`dirname $file`
- lib_dirname=`echo $tempest_dirname | sed s@tempest\/@tempest_lib/\@`
- # Convert tempest dirname to import string
- tempest_import="${tempest_dirname//\//.}"
- tempest_import=${tempest_import:2:${#tempest_import}}
- if [ $service_client -eq 1 ]; then
- # Remove /json path because tempest-lib supports JSON only without XML
- lib_dirname=`echo $lib_dirname | sed s@\/json@@`
- fi
- # Convert tempest-lib dirname to import string
- tempest_lib_import="${lib_dirname//\//.}"
- tempest_lib_import=${tempest_lib_import:2:${#tempest_lib_import}}
- module_name=`basename $file .py`
- tempest_import1="from $tempest_import.$module_name"
- tempest_lib_import1="from $tempest_lib_import.$module_name"
- tempest_import2="from $tempest_import import $module_name"
- tempest_lib_import2="from $tempest_lib_import import $module_name"
- set +e
- grep -rl "$tempest_import1" $tempest_dir | xargs sed -i'' s/"$tempest_import1"/"$tempest_lib_import1"/g 2> /dev/null
- grep -rl "$tempest_import2" $tempest_dir | xargs sed -i'' s/"$tempest_import2"/"$tempest_lib_import2"/g 2> /dev/null
- set -e
- if [[ -z "$file_list" ]]; then
- file_list="$module_name"
- else
- tmp_file_list="$file_list, $module_name"
- char_size=`echo $tmp_file_list | wc -c`
- if [ $char_size -lt 27 ]; then
- file_list="$file_list, $module_name"
- fi
- fi
-done
-
-rm -rf $tmpdir
-echo "Completed. Run pep8 and fix error if any"
-
-git add -A tempest/
-# Generate a migration commit
-commit_message="Use $file_list from tempest-lib"
-pre_list=$"The files below have been migrated to tempest-lib\n"
-pre_list=`echo -e $pre_list`
-post_list=$"Now Tempest-lib provides those as stable interfaces. So Tempest should\nstart using those from lib and remove its own copy."
-post_list=`echo -e $post_list`
-
-git commit -m "$commit_message" -m "$pre_list" -m "$MIGRATION_IDS" -m "$post_list"
diff --git a/tools/with_venv.sh b/tools/with_venv.sh
index 165c883..408b5f1 100755
--- a/tools/with_venv.sh
+++ b/tools/with_venv.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
TOOLS_PATH=${TOOLS_PATH:-$(dirname $0)/../}
VENV_PATH=${VENV_PATH:-${TOOLS_PATH}}
VENV_DIR=${VENV_DIR:-/.venv}
diff --git a/tox.ini b/tox.ini
index fedd04c..da0233a 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,92 +1,130 @@
[tox]
-envlist = pep8,py34,py27
-minversion = 1.6
+envlist = pep8,py35,py27,pip-check-reqs
+minversion = 2.3.1
skipsdist = True
[tempestenv]
sitepackages = False
-setenv = VIRTUAL_ENV={envdir}
- OS_TEST_PATH=./tempest/test_discover
-deps = setuptools
- -r{toxinidir}/requirements.txt
+setenv =
+ VIRTUAL_ENV={envdir}
+ OS_TEST_PATH=./tempest/test_discover
+deps =
+ -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt}
+ -r{toxinidir}/requirements.txt
[testenv]
-setenv = VIRTUAL_ENV={envdir}
- OS_TEST_PATH=./tempest/tests
-passenv = OS_STDOUT_CAPTURE OS_STDERR_CAPTURE OS_TEST_TIMEOUT OS_TEST_LOCK_PATH OS_TEST_PATH TEMPEST_CONFIG TEMPEST_CONFIG_DIR http_proxy HTTP_PROXY https_proxy HTTPS_PROXY no_proxy NO_PROXY
+setenv =
+ VIRTUAL_ENV={envdir}
+ OS_LOG_CAPTURE=1
+ OS_STDOUT_CAPTURE=1
+ OS_STDERR_CAPTURE=1
+ OS_TEST_TIMEOUT=160
+ PYTHONWARNINGS=default::DeprecationWarning
+passenv = OS_STDOUT_CAPTURE OS_STDERR_CAPTURE OS_TEST_TIMEOUT OS_TEST_LOCK_PATH TEMPEST_CONFIG TEMPEST_CONFIG_DIR http_proxy HTTP_PROXY https_proxy HTTPS_PROXY no_proxy NO_PROXY ZUUL_CACHE_DIR REQUIREMENTS_PIP_LOCATION GENERATE_TEMPEST_PLUGIN_LIST
usedevelop = True
-install_command = pip install -U {opts} {packages}
+install_command = pip install {opts} {packages}
whitelist_externals = *
-deps = -r{toxinidir}/requirements.txt
- -r{toxinidir}/test-requirements.txt
+deps =
+ -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt}
+ -r{toxinidir}/requirements.txt
+ -r{toxinidir}/test-requirements.txt
commands =
- find . -type f -name "*.pyc" -delete
- bash tools/pretty_tox.sh '{posargs}'
+ find . -type f -name "*.pyc" -delete
+ stestr --test-path ./tempest/tests run {posargs}
[testenv:genconfig]
-commands = oslo-config-generator --config-file etc/config-generator.tempest.conf
+commands = oslo-config-generator --config-file tempest/cmd/config-generator.tempest.conf
[testenv:cover]
-setenv = OS_TEST_PATH=./tempest/tests
-commands = python setup.py testr --coverage --testr-arg='tempest\.tests {posargs}'
+setenv =
+ {[testenv]setenv}
+ PYTHON=coverage run --source tempest --parallel-mode
+commands =
+ coverage erase
+ find . -type f -name "*.pyc" -delete
+ stestr --test-path ./tempest/tests run {posargs}
+ coverage combine
+ coverage html -d cover
+ coverage xml -o cover/coverage.xml
+ coverage report
[testenv:all]
+envdir = .tox/tempest
sitepackages = {[tempestenv]sitepackages}
# 'all' includes slow tests
-setenv = {[tempestenv]setenv}
- OS_TEST_TIMEOUT=1200
+setenv =
+ {[tempestenv]setenv}
+ OS_TEST_TIMEOUT={env:OS_TEST_TIMEOUT:1200}
deps = {[tempestenv]deps}
commands =
- find . -type f -name "*.pyc" -delete
- bash tools/pretty_tox.sh '{posargs}'
+ find . -type f -name "*.pyc" -delete
+ tempest run --regex {posargs}
[testenv:all-plugin]
sitepackages = True
# 'all' includes slow tests
-setenv = {[tempestenv]setenv}
- OS_TEST_TIMEOUT=1200
+setenv =
+ {[tempestenv]setenv}
+ OS_TEST_TIMEOUT={env:OS_TEST_TIMEOUT:1200}
deps = {[tempestenv]deps}
commands =
- find . -type f -name "*.pyc" -delete
- bash tools/pretty_tox.sh '{posargs}'
+ find . -type f -name "*.pyc" -delete
+ tempest run --regex {posargs}
[testenv:full]
+envdir = .tox/tempest
sitepackages = {[tempestenv]sitepackages}
setenv = {[tempestenv]setenv}
deps = {[tempestenv]deps}
# The regex below is used to select which tests to run and exclude the slow tag:
# See the testrepository bug: https://bugs.launchpad.net/testrepository/+bug/1208610
commands =
- find . -type f -name "*.pyc" -delete
- bash tools/pretty_tox.sh '(?!.*\[.*\bslow\b.*\])(^tempest\.(api|scenario|thirdparty)) {posargs}'
+ find . -type f -name "*.pyc" -delete
+ 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}
setenv = {[tempestenv]setenv}
deps = {[tempestenv]deps}
# The regex below is used to select which tests to run and exclude the slow tag:
# See the testrepository bug: https://bugs.launchpad.net/testrepository/+bug/1208610
commands =
- find . -type f -name "*.pyc" -delete
- bash tools/pretty_tox_serial.sh '(?!.*\[.*\bslow\b.*\])(^tempest\.(api|scenario|thirdparty)) {posargs}'
+ find . -type f -name "*.pyc" -delete
+ tempest run --serial --regex '(?!.*\[.*\bslow\b.*\])(^tempest\.(api|scenario))' {posargs}
-[testenv:large-ops]
+[testenv:scenario]
+envdir = .tox/tempest
sitepackages = {[tempestenv]sitepackages}
setenv = {[tempestenv]setenv}
deps = {[tempestenv]deps}
+# The regex below is used to select all scenario tests
commands =
- find . -type f -name "*.pyc" -delete
- python setup.py testr --slowest --testr-args='tempest.scenario.test_large_ops {posargs}'
+ find . -type f -name "*.pyc" -delete
+ tempest run --serial --regex '(^tempest\.scenario)' {posargs}
[testenv:smoke]
+envdir = .tox/tempest
sitepackages = {[tempestenv]sitepackages}
setenv = {[tempestenv]setenv}
deps = {[tempestenv]deps}
commands =
- find . -type f -name "*.pyc" -delete
- bash tools/pretty_tox.sh '\[.*\bsmoke\b.*\] {posargs}'
+ find . -type f -name "*.pyc" -delete
+ tempest run --regex '\[.*\bsmoke\b.*\]' {posargs}
[testenv:smoke-serial]
+envdir = .tox/tempest
sitepackages = {[tempestenv]sitepackages}
setenv = {[tempestenv]setenv}
deps = {[tempestenv]deps}
@@ -94,31 +132,41 @@
# https://bugs.launchpad.net/tempest/+bug/1216076 so the neutron smoke
# job would fail if we moved it to parallel.
commands =
- find . -type f -name "*.pyc" -delete
- bash tools/pretty_tox_serial.sh '\[.*\bsmoke\b.*\] {posargs}'
+ find . -type f -name "*.pyc" -delete
+ tempest run --serial --regex '\[.*\bsmoke\b.*\]' {posargs}
-[testenv:stress]
+[testenv:venv]
+deps =
+ -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt}
+ -r{toxinidir}/requirements.txt
+ -r{toxinidir}/doc/requirements.txt
+commands = {posargs}
+
+[testenv:venv-tempest]
+envdir = .tox/tempest
sitepackages = {[tempestenv]sitepackages}
setenv = {[tempestenv]setenv}
deps = {[tempestenv]deps}
-commands =
- run-tempest-stress {posargs}
-
-[testenv:venv]
commands = {posargs}
[testenv:docs]
+deps =
+ -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt}
+ -r{toxinidir}/requirements.txt
+ -r{toxinidir}/doc/requirements.txt
commands =
- python setup.py build_sphinx {posargs}
+ rm -rf doc/build
+ sphinx-build -W -b html doc/source doc/build/html
+whitelist_externals = rm
[testenv:pep8]
commands =
- flake8 {posargs}
- check-uuid
+ flake8 {posargs}
+ check-uuid
[testenv:uuidgen]
commands =
- check-uuid --fix
+ check-uuid --fix
[hacking]
local-check-factory = tempest.hacking.checks.factory
@@ -128,7 +176,45 @@
# E125 is a won't fix until https://github.com/jcrocholl/pep8/issues/126 is resolved. For further detail see https://review.openstack.org/#/c/36788/
# E123 skipped because it is ignored by default in the default pep8
# E129 skipped because it is too limiting when combined with other rules
-# Skipped because of new hacking 0.9: H405
ignore = E125,E123,E129
show-source = True
-exclude = .git,.venv,.tox,dist,doc,openstack,*egg
+exclude = .git,.venv,.tox,dist,doc,*egg,build
+enable-extensions = H106,H203,H904
+import-order-style = pep8
+
+[testenv:releasenotes]
+deps =
+ -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt}
+ -r{toxinidir}/requirements.txt
+ -r{toxinidir}/doc/requirements.txt
+commands =
+ rm -rf releasenotes/build
+ sphinx-build -a -E -W -d releasenotes/build/doctrees \
+ -b html releasenotes/source releasenotes/build/html
+whitelist_externals = rm
+
+[testenv:pip-check-reqs]
+# Do not install test-requirements as that will pollute the virtualenv for
+# determining missing packages.
+# This also means that pip-check-reqs must be installed separately, outside
+# of the requirements.txt files
+deps = pip_check_reqs
+ -r{toxinidir}/requirements.txt
+commands=
+ pip-extra-reqs -d --ignore-file=tempest/tests/* tempest
+ pip-missing-reqs -d --ignore-file=tempest/tests/* tempest
+
+
+[testenv:bindep]
+# Do not install any requirements. We want this to be fast and work even if
+# system dependencies are missing, since it's used to tell you what system
+# dependencies are missing! This also means that bindep must be installed
+# separately, outside of the requirements files.
+deps = bindep
+commands = bindep test
+
+[testenv:plugin-sanity-check]
+# perform tempest plugin sanity
+whitelist_externals = bash
+commands =
+ bash tools/tempest-plugin-sanity.sh