Merge "add some negative tests for force_delete/restore"
diff --git a/.mailmap b/.mailmap
index 9a22c71..5c37a5e 100644
--- a/.mailmap
+++ b/.mailmap
@@ -2,6 +2,7 @@
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>
diff --git a/README.rst b/README.rst
index 4161cc6..0996022 100644
--- a/README.rst
+++ b/README.rst
@@ -6,6 +6,33 @@
Scenarios, and other specific tests useful in validating an OpenStack
deployment.
+Design Principles
+----------
+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
+ 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),
+ public CLI or libraries.
+- 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
+ included in the OpenStack base. If there is some feature of
+ OpenStack that is not verifiable through standard interfaces, this
+ should be considered a possible enhancement.
+- Tempest strives for complete coverage of the OpenStack API and
+ common scenarios that demonstrate a working cloud.
+- Tempest drives load in an OpenStack cloud. By including a broad
+ array of API and scenario tests Tempest can be reused in whole or in
+ parts as load generation for an OpenStack cloud.
+- Tempest should attempt to clean up after itself, whenever possible
+ we should tear down resources when done.
+- Tempest should be self testing.
Quickstart
----------
diff --git a/doc/source/conf.py b/doc/source/conf.py
index cf838c0..e5444ae 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -51,17 +51,6 @@
project = u'Tempest'
copyright = u'2013, OpenStack QA Team'
-# The version info for the project you're documenting, acts as replacement for
-# |version| and |release|, also used in various other places throughout the
-# built documents.
-#
-# The short X.Y version.
-import pbr.version
-version_info = pbr.version.VersionInfo('tempest')
-version = version_info.version_string()
-# The full version, including alpha/beta/rc tags.
-release = version_info.release_string()
-
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample
index 115a2b5..87bf758 100644
--- a/etc/tempest.conf.sample
+++ b/etc/tempest.conf.sample
@@ -2,7 +2,7 @@
#log_config = /opt/stack/tempest/etc/logging.conf.sample
# disable logging to the stderr
-use_stderr = False
+use_stderr = false
# log file
log_file = tempest.log
@@ -23,7 +23,7 @@
catalog_type = identity
# Ignore SSL certificate validation failures? Use when in testing
# environments that have self-signed SSL certs.
-disable_ssl_certificate_validation = False
+disable_ssl_certificate_validation = false
# URL for where to find the OpenStack Identity API endpoint (Keystone)
uri = http://127.0.0.1:5000/v2.0/
# URL for where to find the OpenStack V3 Identity API endpoint (Keystone)
@@ -131,7 +131,7 @@
ssh_channel_timeout = 60
# Dose the SSH uses Floating IP?
-use_floatingip_for_ssh = True
+use_floatingip_for_ssh = true
# The type of endpoint for a Compute API service. Unless you have a
# custom Keystone service catalog implementation, you probably want to leave
@@ -143,36 +143,37 @@
# catalog, the first found one is used.
#region = RegionOne
+# Expected first device name when a volume is attached to an instance
+volume_device_name = vdb
+
+[compute-feature-enabled]
# Does the Compute API support creation of images?
-create_image_enabled = true
+create_image = true
# For resize to work with libvirt/kvm, one of the following must be true:
-# Single node: allow_resize_to_same_host=True must be set in nova.conf
+# Single node: allow_resize_to_same_host=true must be set in nova.conf
# Cluster: the 'nova' user must have scp access between cluster nodes
-resize_available = true
+resize = true
# Does the compute API support changing the admin password?
-change_password_available=true
+change_password = false
# Run live migration tests (requires 2 hosts)
-live_migration_available = false
+live_migration = false
# Use block live migration (Otherwise, non-block migration will be
# performed, which requires XenServer pools in case of using XS)
-use_block_migration_for_live_migration = false
+block_migration_for_live_migration = false
# Supports iSCSI block migration - depends on a XAPI supporting
# relax-xsm-sr-check
-block_migrate_supports_cinder_iscsi = false
+block_migrate_cinder_iscsi = false
# When set to false, disk config tests are forced to skip
-disk_config_enabled = true
+disk_config = true
# When set to false, flavor extra data tests are forced to skip
-flavor_extra_enabled = true
-
-# Expected first device name when a volume is attached to an instance
-volume_device_name = vdb
+flavor_extra = true
[compute-admin]
# This should be the username of a user WITH administrative privileges
@@ -197,12 +198,15 @@
# catalog, the first found one is used.
#region = RegionOne
-# The version of the OpenStack Images API to use
-api_version = 1
-
# HTTP image to use for glance http image testing
http_image = http://download.cirros-cloud.net/0.3.1/cirros-0.3.1-x86_64-uec.tar.gz
+[image-feature-enabled]
+# Is the image api_v1 enabled
+api_v1 = True
+# Is the image api_v2 enabled
+api_v2 = True
+
[network]
# This section contains configuration options used when executing tests
# against the OpenStack Network API.
@@ -258,10 +262,8 @@
# Number of seconds to time out on waiting for a volume
# to be available or reach an expected status
build_timeout = 300
-# Runs Cinder multi-backend tests (requires 2 backends declared in cinder.conf)
-# They must have different volume_backend_name (backend1_name and backend2_name
-# have to be different)
-multi_backend_enabled = false
+# If multi_backend is enabled there must be 2 volume_backend_names (
+# backend1_name and backend2_name) which have to be different)
backend1_name = BACKEND_1
backend2_name = BACKEND_2
# Protocol and vendor of volume backend to target when testing volume-types.
@@ -269,6 +271,10 @@
storage_protocol = iSCSI
vendor_name = Open Source
+[volume-feature-enabled]
+#Runs Cinder multi-backend tests (requires 2 backends declared in cinder.conf)
+multi_backend = false
+
[object-storage]
# This section contains configuration options used when executing tests
# against the OpenStack Object Storage API.
@@ -291,14 +297,16 @@
# Number of seconds to wait while looping to check the status of a
# container to container synchronization
container_sync_interval = 5
-# Set to True if the Account Quota middleware is enabled
-accounts_quotas_available = True
-# Set to True if the Container Quota middleware is enabled
-container_quotas_available = True
# Set operator role for tests that require creating a container
operator_role = Member
+[object-feature-enabled]
+# Set to True if the Account Quota middleware is enabled
+accounts_quotas = True
+# Set to True if the Container Quota middleware is enabled
+container_quotas = True
+
[boto]
# This section contains configuration options used when executing tests
# with boto.
@@ -403,7 +411,7 @@
[cli]
# Enable cli tests
-enabled = True
+enabled = true
# directory where python client binaries are located
cli_dir = /usr/local/bin
# Number of seconds to wait on a CLI timeout
@@ -411,19 +419,21 @@
[service_available]
# Whether or not cinder is expected to be available
-cinder = True
+cinder = true
# Whether or not neutron is expected to be available
neutron = false
# Whether or not glance is expected to be available
-glance = True
+glance = true
# Whether or not swift is expected to be available
-swift = True
+swift = true
# Whether or not nova is expected to be available
-nova = True
+nova = true
# Whether or not Heat is expected to be available
heat = false
+# Whether or not Ceilometer is expected to be available
+ceilometer = True
# Whether or not horizon is expected to be available
-horizon = True
+horizon = true
[stress]
# Maximum number of instances to create during test
@@ -431,8 +441,8 @@
# Time (in seconds) between log file error checks
log_check_interval = 60
# The default number of threads created while stress test
-default_thread_number_per_action=4
+default_thread_number_per_action = 4
[debug]
# Enable diagnostic commands
-enable = True
+enable = true
diff --git a/etc/whitelist.yaml b/etc/whitelist.yaml
index 6762f9f..e6d28f5 100644
--- a/etc/whitelist.yaml
+++ b/etc/whitelist.yaml
@@ -21,6 +21,16 @@
message: "Instance failed to spawn"
- module: "nova.compute.manager"
message: "Error: Unexpected error while running command"
+ - module: "nova.virt.libvirt.driver"
+ message: "Error from libvirt during destroy"
+ - module: "nova.virt.libvirt.vif"
+ message: "Failed while unplugging vif"
+ - module: "nova.openstack.common.loopingcal"
+ message: "in fixed duration looping call"
+ - module: "nova.virt.libvirt.driver"
+ message: "Getting disk size of instance"
+ - module: "nova.virt.libvirt.driver"
+ message: "No such file or directory: '/opt/stack/data/nova/instances"
g-api:
- module: "glance.store.sheepdog"
@@ -34,6 +44,12 @@
message: "Requested operation is not valid: domain is not running"
- module: "ceilometer.compute.pollsters.disk"
message: "Domain not found: no domain with matching uuid"
+ - module: "ceilometer.compute.pollsters.net"
+ message: "No module named libvirt"
+
+ceilometer-alarm-evaluator:
+ - module: "ceilometer.alarm.service"
+ message: "alarm evaluation cycle failed"
h-api:
- module: "root"
@@ -63,6 +79,8 @@
message: "ServersTest"
- module: "nova.compute.api"
message: "\\{u'kernel_id'.*u'ramdisk_id':"
+ - module: "nova.api.openstack.wsgi"
+ message: "takes exactly 4 arguments"
n-cond:
- module: "nova.notifications"
@@ -75,6 +93,16 @@
c-api:
- module: "cinder.api.middleware.fault"
message: "Caught error: Volume .* could not be found"
+ - module: "cinder.api.middleware.fault"
+ message: "Caught error: Snapshot .* could not be found"
+
+c-vol:
+ - module: "cinder.brick.iscsi.iscsi"
+ message: "Failed to create iscsi target for volume id"
+ - module: "cinder.brick.local_dev.lvm"
+ message: "/dev/dm-1: stat failed: No such file or directory"
+ - module: "cinder.brick.local_dev.lvm"
+ message: "Can't remove open logical volume"
q-dhpc:
- module: "neutron.common.legacy"
@@ -89,6 +117,8 @@
message: ".*"
- module: "ceilometer.collector.dispatcher.database"
message: "duplicate key value violates unique constraint"
+ - module: "ceilometer.collector.dispatcher.database"
+ message: "Failed to record metering data: QueuePool limit"
q-agt:
- module: "neutron.agent.linux.ovs_lib"
@@ -108,6 +138,9 @@
- module: "neutron.agent.l3_agent"
message: "Failed synchronizing routers"
+q-vpn:
+ - module: "neutron.common.legacy"
+ message: "Skipping unknown group key: firewall_driver"
q-lbaas:
- module: "neutron.common.legacy"
@@ -123,9 +156,7 @@
- module: "neutron.openstack.common.rpc.amqp"
message: "Exception during message handling"
- module: "neutron.openstack.common.rpc.common"
- message: "Network .* could not be found"
- - module: "neutron.openstack.common.rpc.common"
- message: "Pool .* could not be found"
+ message: "(Network|Pool|Subnet|Agent|Port) .* could not be found"
- module: "neutron.api.v2.resource"
- message: "show failed"
+ message: ".* failed"
diff --git a/openstack-common.conf b/openstack-common.conf
index ff84404..dabf5a0 100644
--- a/openstack-common.conf
+++ b/openstack-common.conf
@@ -5,6 +5,7 @@
module=lockutils
module=log
module=importlib
+module=fixture
# The base module to hold the copy of openstack.common
base=tempest
diff --git a/requirements.txt b/requirements.txt
index 4dea533..4f6a1d3 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,17 +5,17 @@
jsonschema>=1.3.0,!=1.4.0
testtools>=0.9.32
lxml>=2.3
-boto>=2.4.0
+boto>=2.4.0,!=2.13.0
paramiko>=1.8.0
netaddr
python-glanceclient>=0.9.0
-python-keystoneclient>=0.3.0
-python-novaclient>=2.12.0
-python-neutronclient>=2.2.3,<3
-python-cinderclient>=1.0.4
+python-keystoneclient>=0.4.1
+python-novaclient>=2.15.0
+python-neutronclient>=2.3.0,<3
+python-cinderclient>=1.0.6
python-heatclient>=0.2.3
testresources>=0.2.4
-keyring>=1.6.1
+keyring>=1.6.1,<2.0
testrepository>=0.0.17
oslo.config>=1.2.0
eventlet>=0.13.0
diff --git a/setup.cfg b/setup.cfg
index a4cf118..23a97ff 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
[metadata]
name = tempest
-version = 2013.2
+version = 2014.1
summary = OpenStack Integration Testing
description-file =
README.rst
@@ -17,20 +17,7 @@
Programming Language :: Python :: 2
Programming Language :: Python :: 2.7
-[global]
-setup-hooks =
- pbr.hooks.setup_hook
-
[build_sphinx]
all_files = 1
build-dir = doc/build
source-dir = doc/source
-
-[nosetests]
-# NOTE(jkoelker) To run the test suite under nose install the following
-# coverage http://pypi.python.org/pypi/coverage
-# openstack-nose https://github.com/openstack-dev/openstack-nose
-verbosity=2
-
-[pbr]
-autodoc_tree_index_modules=true
diff --git a/setup.py b/setup.py
index 59a0090..70c2b3f 100755
--- a/setup.py
+++ b/setup.py
@@ -14,8 +14,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
import setuptools
setuptools.setup(
- setup_requires=['d2to1', 'pbr'],
- d2to1=True)
+ setup_requires=['pbr'],
+ pbr=True)
diff --git a/tempest/api/compute/__init__.py b/tempest/api/compute/__init__.py
index 2c21740..a528754 100644
--- a/tempest/api/compute/__init__.py
+++ b/tempest/api/compute/__init__.py
@@ -22,11 +22,11 @@
LOG = logging.getLogger(__name__)
CONFIG = config.TempestConfig()
-CREATE_IMAGE_ENABLED = CONFIG.compute.create_image_enabled
-RESIZE_AVAILABLE = CONFIG.compute.resize_available
-CHANGE_PASSWORD_AVAILABLE = CONFIG.compute.change_password_available
-DISK_CONFIG_ENABLED = CONFIG.compute.disk_config_enabled
-FLAVOR_EXTRA_DATA_ENABLED = CONFIG.compute.flavor_extra_enabled
+CREATE_IMAGE_ENABLED = CONFIG.compute_feature_enabled.create_image
+RESIZE_AVAILABLE = CONFIG.compute_feature_enabled.resize
+CHANGE_PASSWORD_AVAILABLE = CONFIG.compute_feature_enabled.change_password
+DISK_CONFIG_ENABLED = CONFIG.compute_feature_enabled.disk_config
+FLAVOR_EXTRA_DATA_ENABLED = CONFIG.compute_feature_enabled.flavor_extra
MULTI_USER = True
diff --git a/tempest/api/compute/admin/test_aggregates.py b/tempest/api/compute/admin/test_aggregates.py
index e8acec5..4ff6b07 100644
--- a/tempest/api/compute/admin/test_aggregates.py
+++ b/tempest/api/compute/admin/test_aggregates.py
@@ -85,7 +85,7 @@
aggregates))
@attr(type='gate')
- def test_aggregate_create_get_details(self):
+ def test_aggregate_create_update_metadata_get_details(self):
# Create an aggregate and ensure its details are returned.
aggregate_name = rand_name(self.aggregate_name_prefix)
resp, aggregate = self.client.create_aggregate(aggregate_name)
@@ -96,6 +96,18 @@
self.assertEqual(aggregate['name'], body['name'])
self.assertEqual(aggregate['availability_zone'],
body['availability_zone'])
+ self.assertEqual({}, body["metadata"])
+
+ # set the metadata of the aggregate
+ meta = {"key": "value"}
+ resp, body = self.client.set_metadata(aggregate['id'], meta)
+ self.assertEqual(200, resp.status)
+ self.assertEqual(meta, body["metadata"])
+
+ # verify the metadata has been set
+ resp, body = self.client.get_aggregate(aggregate['id'])
+ self.assertEqual(200, resp.status)
+ self.assertEqual(meta, body["metadata"])
@attr(type='gate')
def test_aggregate_create_update_with_az(self):
diff --git a/tempest/api/compute/admin/test_flavors.py b/tempest/api/compute/admin/test_flavors.py
index 75b8dad..05bb457 100644
--- a/tempest/api/compute/admin/test_flavors.py
+++ b/tempest/api/compute/admin/test_flavors.py
@@ -80,13 +80,7 @@
self.assertEqual(flavor['rxtx_factor'], self.rxtx)
self.assertEqual(flavor['OS-FLV-EXT-DATA:ephemeral'],
self.ephemeral)
- if self._interface == "xml":
- XMLNS_OS_FLV_ACCESS = "http://docs.openstack.org/compute/ext/"\
- "flavor_access/api/v2"
- key = "{" + XMLNS_OS_FLV_ACCESS + "}is_public"
- self.assertEqual(flavor[key], "True")
- if self._interface == "json":
- self.assertEqual(flavor['os-flavor-access:is_public'], True)
+ self.assertEqual(flavor['os-flavor-access:is_public'], True)
# Verify flavor is retrieved
resp, flavor = self.client.get_flavor_details(new_flavor_id)
@@ -156,6 +150,14 @@
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
+
+ def verify_flavor_response_extension(flavor):
+ # 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-flavor-access:is_public'], True)
+
flavor_name = rand_name(self.flavor_name_prefix)
new_flavor_id = rand_int_id(start=1000)
@@ -171,26 +173,20 @@
self.assertEqual(flavor['vcpus'], self.vcpus)
self.assertEqual(flavor['disk'], self.disk)
self.assertEqual(int(flavor['id']), new_flavor_id)
- self.assertEqual(flavor['swap'], '')
- self.assertEqual(int(flavor['rxtx_factor']), 1)
- self.assertEqual(int(flavor['OS-FLV-EXT-DATA:ephemeral']), 0)
- if self._interface == "xml":
- XMLNS_OS_FLV_ACCESS = "http://docs.openstack.org/compute/ext/"\
- "flavor_access/api/v2"
- key = "{" + XMLNS_OS_FLV_ACCESS + "}is_public"
- self.assertEqual(flavor[key], "True")
- if self._interface == "json":
- self.assertEqual(flavor['os-flavor-access:is_public'], True)
+ verify_flavor_response_extension(flavor)
# Verify flavor is retrieved
resp, flavor = self.client.get_flavor_details(new_flavor_id)
self.assertEqual(resp.status, 200)
self.assertEqual(flavor['name'], flavor_name)
+ verify_flavor_response_extension(flavor)
+
# Check if flavor is present in list
- resp, flavors = self.client.list_flavors_with_detail()
+ resp, flavors = self.user_client.list_flavors_with_detail()
self.assertEqual(resp.status, 200)
for flavor in flavors:
if flavor['name'] == flavor_name:
+ verify_flavor_response_extension(flavor)
flag = True
self.assertTrue(flag)
diff --git a/tempest/api/compute/admin/test_flavors_extra_specs.py b/tempest/api/compute/admin/test_flavors_extra_specs.py
index 0fd4d11..403a946 100644
--- a/tempest/api/compute/admin/test_flavors_extra_specs.py
+++ b/tempest/api/compute/admin/test_flavors_extra_specs.py
@@ -19,7 +19,6 @@
from tempest.api.compute import base
from tempest.common.utils.data_utils import rand_int_id
from tempest.common.utils.data_utils import rand_name
-from tempest import exceptions
from tempest.test import attr
@@ -64,9 +63,9 @@
super(FlavorsExtraSpecsTestJSON, cls).tearDownClass()
@attr(type='gate')
- def test_flavor_set_get_unset_keys(self):
- # Test to SET, GET UNSET flavor extra spec as a user
- # with admin privileges.
+ 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
@@ -79,55 +78,55 @@
self.client.get_flavor_extra_spec(self.flavor['id'])
self.assertEqual(get_resp.status, 200)
self.assertEqual(get_body, specs)
+
+ # UPDATE the value of the extra specs key1
+ update_resp, update_body = \
+ self.client.update_flavor_extra_spec(self.flavor['id'],
+ "key1",
+ key1="value")
+ self.assertEqual(update_resp.status, 200)
+ self.assertEqual({"key1": "value"}, update_body)
+
# GET a key value and verify
- get_resp, get_body = \
+ show_resp, get_body = \
self.client.get_flavor_extra_spec_with_key(self.flavor['id'],
- "key2")
+ "key1")
+ self.assertEqual(show_resp.status, 200)
+ self.assertEqual(get_body, 'value')
+
+ # GET extra specs and verify the value of the key2
+ # is the same as before
+ get_resp, get_body = \
+ self.client.get_flavor_extra_spec(self.flavor['id'])
self.assertEqual(get_resp.status, 200)
- self.assertEqual(get_body, specs['key2'])
+ self.assertEqual(get_body, {"key1": "value", "key2": "value2"})
+
# UNSET extra specs that were set in this test
unset_resp, _ = \
self.client.unset_flavor_extra_spec(self.flavor['id'], "key1")
self.assertEqual(unset_resp.status, 200)
-
- @attr(type=['negative', 'gate'])
- def test_flavor_non_admin_set_keys(self):
- # Test to SET flavor extra spec as a user without admin privileges.
- specs = {"key1": "value1", "key2": "value2"}
- self.assertRaises(exceptions.Unauthorized,
- self.flavors_client.set_flavor_extra_spec,
- self.flavor['id'],
- specs)
+ unset_resp, _ = \
+ self.client.unset_flavor_extra_spec(self.flavor['id'], "key2")
+ self.assertEqual(unset_resp.status, 200)
@attr(type='gate')
- def test_flavor_non_admin_get_keys(self):
+ def test_flavor_non_admin_get_all_keys_and_specified_key(self):
specs = {"key1": "value1", "key2": "value2"}
set_resp, set_body = self.client.set_flavor_extra_spec(
self.flavor['id'], specs)
resp, body = self.flavors_client.get_flavor_extra_spec(
self.flavor['id'])
self.assertEqual(resp.status, 200)
+
for key in specs:
self.assertEqual(body[key], specs[key])
- @attr(type=['negative', 'gate'])
- def test_flavor_non_admin_unset_keys(self):
- specs = {"key1": "value1", "key2": "value2"}
- set_resp, set_body = self.client.set_flavor_extra_spec(
- self.flavor['id'], specs)
-
- self.assertRaises(exceptions.Unauthorized,
- self.flavors_client.unset_flavor_extra_spec,
- self.flavor['id'],
- 'key1')
-
- @attr(type=['negative', 'gate'])
- def test_flavor_unset_nonexistent_key(self):
- nonexistent_key = rand_name('flavor_key')
- self.assertRaises(exceptions.NotFound,
- self.client.unset_flavor_extra_spec,
- self.flavor['id'],
- nonexistent_key)
+ get_resp, get_body = \
+ self.flavors_client.get_flavor_extra_spec_with_key(
+ self.flavor['id'],
+ "key1")
+ self.assertEqual(get_resp.status, 200)
+ self.assertEqual("value1", get_body)
class FlavorsExtraSpecsTestXML(FlavorsExtraSpecsTestJSON):
diff --git a/tempest/api/compute/admin/test_flavors_extra_specs_negative.py b/tempest/api/compute/admin/test_flavors_extra_specs_negative.py
new file mode 100644
index 0000000..8d62a2a
--- /dev/null
+++ b/tempest/api/compute/admin/test_flavors_extra_specs_negative.py
@@ -0,0 +1,118 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack Foundation
+# All Rights Reserved.
+# 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 tempest.api import compute
+from tempest.api.compute import base
+from tempest.common.utils.data_utils import rand_int_id
+from tempest.common.utils.data_utils import rand_name
+from tempest import exceptions
+from tempest.test import attr
+
+
+class FlavorsExtraSpecsNegativeTestJSON(base.BaseV2ComputeAdminTest):
+ """the Negative tests for FlavorsExtraSpecs."""
+
+ _interface = 'json'
+
+ @classmethod
+ def setUpClass(cls):
+ super(FlavorsExtraSpecsNegativeTestJSON, cls).setUpClass()
+ if not compute.FLAVOR_EXTRA_DATA_ENABLED:
+ msg = "FlavorExtraData extension not enabled."
+ raise cls.skipException(msg)
+
+ cls.client = cls.os_adm.flavors_client
+ flavor_name = rand_name('test_flavor')
+ ram = 512
+ vcpus = 1
+ disk = 10
+ ephemeral = 10
+ cls.new_flavor_id = rand_int_id(start=1000)
+ swap = 1024
+ rxtx = 1
+ # Create a flavor
+ resp, cls.flavor = cls.client.create_flavor(flavor_name,
+ ram, vcpus,
+ disk,
+ cls.new_flavor_id,
+ ephemeral=ephemeral,
+ swap=swap, rxtx=rxtx)
+
+ @classmethod
+ def tearDownClass(cls):
+ resp, body = cls.client.delete_flavor(cls.flavor['id'])
+ cls.client.wait_for_resource_deletion(cls.flavor['id'])
+ super(FlavorsExtraSpecsNegativeTestJSON, cls).tearDownClass()
+
+ @attr(type=['negative', 'gate'])
+ def test_flavor_non_admin_set_keys(self):
+ # Test to SET flavor extra spec as a user without admin privileges.
+ specs = {"key1": "value1", "key2": "value2"}
+ self.assertRaises(exceptions.Unauthorized,
+ self.flavors_client.set_flavor_extra_spec,
+ self.flavor['id'],
+ specs)
+
+ @attr(type=['negative', 'gate'])
+ def test_flavor_non_admin_unset_keys(self):
+ specs = {"key1": "value1", "key2": "value2"}
+ set_resp, set_body = self.client.set_flavor_extra_spec(
+ self.flavor['id'], specs)
+
+ self.assertRaises(exceptions.Unauthorized,
+ self.flavors_client.unset_flavor_extra_spec,
+ self.flavor['id'],
+ 'key1')
+
+ @attr(type=['negative', 'gate'])
+ def test_flavor_unset_nonexistent_key(self):
+ nonexistent_key = rand_name('flavor_key')
+ self.assertRaises(exceptions.NotFound,
+ self.client.unset_flavor_extra_spec,
+ self.flavor['id'],
+ nonexistent_key)
+
+ @attr(type=['negative', 'gate'])
+ def test_flavor_get_nonexistent_key(self):
+ self.assertRaises(exceptions.NotFound,
+ self.flavors_client.get_flavor_extra_spec_with_key,
+ self.flavor['id'],
+ "nonexistent_key")
+
+ @attr(type=['negative', 'gate'])
+ def test_flavor_update_mismatch_key(self):
+ # the key will be updated should be match the key in the body
+ self.assertRaises(exceptions.BadRequest,
+ self.client.update_flavor_extra_spec,
+ self.flavor['id'],
+ "key2",
+ key1="value")
+
+ @attr(type=['negative', 'gate'])
+ def test_flavor_update_more_key(self):
+ # there should be just one item in the request body
+ self.assertRaises(exceptions.BadRequest,
+ self.client.update_flavor_extra_spec,
+ self.flavor['id'],
+ "key1",
+ key1="value",
+ key2="value")
+
+
+class FlavorsExtraSpecsNegativeTestXML(FlavorsExtraSpecsNegativeTestJSON):
+ _interface = 'xml'
diff --git a/tempest/api/compute/admin/test_hosts.py b/tempest/api/compute/admin/test_hosts.py
index aa769ba..8e451a0 100644
--- a/tempest/api/compute/admin/test_hosts.py
+++ b/tempest/api/compute/admin/test_hosts.py
@@ -16,8 +16,6 @@
from tempest.api.compute import base
from tempest.common import tempest_fixtures as fixtures
-from tempest.common.utils.data_utils import rand_name
-from tempest import exceptions
from tempest.test import attr
@@ -33,7 +31,13 @@
def setUpClass(cls):
super(HostsAdminTestJSON, cls).setUpClass()
cls.client = cls.os_adm.hosts_client
- cls.non_admin_client = cls.os.hosts_client
+
+ def _get_host_name(self):
+ resp, hosts = self.client.list_hosts()
+ self.assertEqual(200, resp.status)
+ self.assertTrue(len(hosts) >= 1)
+ hostname = hosts[0]['host_name']
+ return hostname
@attr(type='gate')
def test_list_hosts(self):
@@ -53,14 +57,7 @@
self.assertTrue(len(hosts) >= 1)
self.assertIn(host, hosts)
- @attr(type='negative')
- def test_list_hosts_with_non_existent_zone(self):
- params = {'zone': 'xxx'}
- resp, hosts = self.client.list_hosts(params)
- self.assertEqual(0, len(hosts))
- self.assertEqual(200, resp.status)
-
- @attr(type='negative')
+ @attr(type='gate')
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
@@ -69,17 +66,18 @@
self.assertNotEqual(0, len(hosts))
self.assertEqual(200, resp.status)
- @attr(type=['negative', 'gate'])
- def test_list_hosts_with_non_admin_user(self):
- self.assertRaises(exceptions.Unauthorized,
- self.non_admin_client.list_hosts)
+ @attr(type='gate')
+ 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
+ params = {'zone': 'xxx'}
+ resp, hosts = self.client.list_hosts(params)
+ self.assertEqual(0, len(hosts))
+ self.assertEqual(200, resp.status)
@attr(type='gate')
def test_show_host_detail(self):
- resp, hosts = self.client.list_hosts()
- self.assertEqual(200, resp.status)
- self.assertTrue(len(hosts) >= 1)
- hostname = hosts[0]['host_name']
+ hostname = self._get_host_name()
resp, resources = self.client.show_host_detail(hostname)
self.assertEqual(200, resp.status)
@@ -92,12 +90,6 @@
self.assertIsNotNone(host_resource['project'])
self.assertEqual(hostname, host_resource['host'])
- @attr(type='negative')
- def test_show_host_detail_with_nonexist_hostname(self):
- hostname = rand_name('rand_hostname')
- self.assertRaises(exceptions.NotFound,
- self.client.show_host_detail, hostname)
-
class HostsAdminTestXML(HostsAdminTestJSON):
_interface = 'xml'
diff --git a/tempest/api/compute/admin/test_hosts_negative.py b/tempest/api/compute/admin/test_hosts_negative.py
new file mode 100644
index 0000000..6b24e72
--- /dev/null
+++ b/tempest/api/compute/admin/test_hosts_negative.py
@@ -0,0 +1,174 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Huawei Technologies Co.,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.
+
+from tempest.api.compute import base
+from tempest.common.utils import data_utils
+from tempest import exceptions
+from tempest.test import attr
+
+
+class HostsAdminNegativeTestJSON(base.BaseV2ComputeAdminTest):
+
+ """
+ Tests hosts API using admin privileges.
+ """
+
+ _interface = 'json'
+
+ @classmethod
+ def setUpClass(cls):
+ super(HostsAdminNegativeTestJSON, cls).setUpClass()
+ cls.client = cls.os_adm.hosts_client
+ cls.non_admin_client = cls.os.hosts_client
+
+ def _get_host_name(self):
+ resp, hosts = self.client.list_hosts()
+ self.assertEqual(200, resp.status)
+ self.assertTrue(len(hosts) >= 1)
+ hostname = hosts[0]['host_name']
+ return hostname
+
+ @attr(type=['negative', 'gate'])
+ def test_list_hosts_with_non_admin_user(self):
+ self.assertRaises(exceptions.Unauthorized,
+ self.non_admin_client.list_hosts)
+
+ @attr(type=['negative', 'gate'])
+ def test_show_host_detail_with_nonexistent_hostname(self):
+ nonexitent_hostname = data_utils.rand_name('rand_hostname')
+ self.assertRaises(exceptions.NotFound,
+ self.client.show_host_detail, nonexitent_hostname)
+
+ @attr(type=['negative', 'gate'])
+ def test_show_host_detail_with_non_admin_user(self):
+ hostname = self._get_host_name()
+
+ self.assertRaises(exceptions.Unauthorized,
+ self.non_admin_client.show_host_detail,
+ hostname)
+
+ @attr(type=['negative', 'gate'])
+ def test_update_host_with_non_admin_user(self):
+ hostname = self._get_host_name()
+
+ self.assertRaises(exceptions.Unauthorized,
+ self.non_admin_client.update_host,
+ hostname)
+
+ @attr(type=['negative', 'gate'])
+ def test_update_host_with_extra_param(self):
+ # only 'status' and 'maintenance_mode' are the valid params.
+ hostname = self._get_host_name()
+
+ self.assertRaises(exceptions.BadRequest,
+ self.client.update_host,
+ hostname,
+ status='enable',
+ maintenance_mode='enable',
+ param='XXX')
+
+ @attr(type=['negative', 'gate'])
+ def test_update_host_with_invalid_status(self):
+ # 'status' can only be 'enable' or 'disable'
+ hostname = self._get_host_name()
+
+ self.assertRaises(exceptions.BadRequest,
+ self.client.update_host,
+ hostname,
+ status='invalid',
+ maintenance_mode='enable')
+
+ @attr(type=['negative', 'gate'])
+ def test_update_host_with_invalid_maintenance_mode(self):
+ # 'maintenance_mode' can only be 'enable' or 'disable'
+ hostname = self._get_host_name()
+
+ self.assertRaises(exceptions.BadRequest,
+ self.client.update_host,
+ hostname,
+ status='enable',
+ maintenance_mode='invalid')
+
+ @attr(type=['negative', 'gate'])
+ def test_update_host_without_param(self):
+ # 'status' or 'maintenance_mode' needed for host update
+ hostname = self._get_host_name()
+
+ self.assertRaises(exceptions.BadRequest,
+ self.client.update_host,
+ hostname)
+
+ @attr(type=['negative', 'gate'])
+ def test_update_nonexistent_host(self):
+ nonexitent_hostname = data_utils.rand_name('rand_hostname')
+
+ self.assertRaises(exceptions.NotFound,
+ self.client.update_host,
+ nonexitent_hostname,
+ status='enable',
+ maintenance_mode='enable')
+
+ @attr(type=['negative', 'gate'])
+ def test_startup_nonexistent_host(self):
+ nonexitent_hostname = data_utils.rand_name('rand_hostname')
+
+ self.assertRaises(exceptions.NotFound,
+ self.client.startup_host,
+ nonexitent_hostname)
+
+ @attr(type=['negative', 'gate'])
+ def test_startup_host_with_non_admin_user(self):
+ hostname = self._get_host_name()
+
+ self.assertRaises(exceptions.Unauthorized,
+ self.non_admin_client.startup_host,
+ hostname)
+
+ @attr(type=['negative', 'gate'])
+ def test_shutdown_nonexistent_host(self):
+ nonexitent_hostname = data_utils.rand_name('rand_hostname')
+
+ self.assertRaises(exceptions.NotFound,
+ self.client.shutdown_host,
+ nonexitent_hostname)
+
+ @attr(type=['negative', 'gate'])
+ def test_shutdown_host_with_non_admin_user(self):
+ hostname = self._get_host_name()
+
+ self.assertRaises(exceptions.Unauthorized,
+ self.non_admin_client.shutdown_host,
+ hostname)
+
+ @attr(type=['negative', 'gate'])
+ def test_reboot_nonexistent_host(self):
+ nonexitent_hostname = data_utils.rand_name('rand_hostname')
+
+ self.assertRaises(exceptions.NotFound,
+ self.client.reboot_host,
+ nonexitent_hostname)
+
+ @attr(type=['negative', 'gate'])
+ def test_reboot_host_with_non_admin_user(self):
+ hostname = self._get_host_name()
+
+ self.assertRaises(exceptions.Unauthorized,
+ self.non_admin_client.reboot_host,
+ hostname)
+
+
+class HostsAdminNegativeTestXML(HostsAdminNegativeTestJSON):
+ _interface = 'xml'
diff --git a/tempest/api/compute/admin/test_hypervisor_negative.py b/tempest/api/compute/admin/test_hypervisor_negative.py
index 69b8d9c..c6455b5 100644
--- a/tempest/api/compute/admin/test_hypervisor_negative.py
+++ b/tempest/api/compute/admin/test_hypervisor_negative.py
@@ -23,7 +23,7 @@
from tempest.test import attr
-class HypervisorAdminNegativeTestJSON(base.BaseComputeAdminTest):
+class HypervisorAdminNegativeTestJSON(base.BaseV2ComputeAdminTest):
"""
Tests Hypervisors API that require admin privileges
diff --git a/tempest/api/compute/base.py b/tempest/api/compute/base.py
index 37573c4..5679a45 100644
--- a/tempest/api/compute/base.py
+++ b/tempest/api/compute/base.py
@@ -19,7 +19,6 @@
from tempest.api import compute
from tempest import clients
-from tempest.common import isolated_creds
from tempest.common.utils.data_utils import parse_image_id
from tempest.common.utils.data_utils import rand_name
from tempest import exceptions
@@ -42,21 +41,10 @@
if not cls.config.service_available.nova:
skip_msg = ("%s skipped as nova is not available" % cls.__name__)
raise cls.skipException(skip_msg)
- cls.isolated_creds = isolated_creds.IsolatedCreds(cls.__name__)
- if (cls.config.compute.allow_tenant_isolation or
- cls.force_tenant_isolation is True):
- creds = cls.isolated_creds.get_primary_creds()
- username, tenant_name, password = creds
- os = clients.Manager(username=username,
- password=password,
- tenant_name=tenant_name,
- interface=cls._interface)
- else:
- os = clients.Manager(interface=cls._interface)
+ os = cls.get_client_manager()
cls.os = os
-
cls.build_interval = cls.config.compute.build_interval
cls.build_timeout = cls.config.compute.build_timeout
cls.ssh_user = cls.config.compute.ssh_user
@@ -113,7 +101,7 @@
def tearDownClass(cls):
cls.clear_images()
cls.clear_servers()
- cls.isolated_creds.clear_isolated_creds()
+ cls.clear_isolated_creds()
super(BaseComputeTest, cls).tearDownClass()
@classmethod
diff --git a/tempest/api/compute/servers/test_list_servers_negative.py b/tempest/api/compute/servers/test_list_servers_negative.py
index bef45a7..088d3ae 100644
--- a/tempest/api/compute/servers/test_list_servers_negative.py
+++ b/tempest/api/compute/servers/test_list_servers_negative.py
@@ -188,7 +188,9 @@
# changes-since returns all instances, including deleted.
num_expected = (len(self.existing_fixtures) +
len(self.deleted_fixtures))
- self.assertEqual(num_expected, len(body['servers']))
+ self.assertEqual(num_expected, len(body['servers']),
+ "Number of servers %d is wrong in %s" %
+ (num_expected, body['servers']))
@attr(type=['negative', 'gate'])
def test_list_servers_by_changes_since_invalid_date(self):
diff --git a/tempest/api/compute/servers/test_server_actions.py b/tempest/api/compute/servers/test_server_actions.py
index 6f50a02..961737a 100644
--- a/tempest/api/compute/servers/test_server_actions.py
+++ b/tempest/api/compute/servers/test_server_actions.py
@@ -32,7 +32,8 @@
class ServerActionsTestJSON(base.BaseV2ComputeTest):
_interface = 'json'
- resize_available = tempest.config.TempestConfig().compute.resize_available
+ resize_available = tempest.config.TempestConfig().\
+ compute_feature_enabled.resize
run_ssh = tempest.config.TempestConfig().compute.run_ssh
def setUp(self):
@@ -252,7 +253,6 @@
self.assertEqual(202, resp.status)
self.servers_client.wait_for_server_status(self.server_id, 'ACTIVE')
- @skip_because(bug="1233026")
@attr(type='gate')
def test_lock_unlock_server(self):
# Lock the server,try server stop(exceptions throw),unlock it and retry
@@ -262,7 +262,7 @@
self.assertEqual(200, resp.status)
self.assertEqual(server['status'], 'ACTIVE')
# Locked server is not allowed to be stopped by non-admin user
- self.assertRaises(exceptions.BadRequest,
+ self.assertRaises(exceptions.Conflict,
self.servers_client.stop, self.server_id)
resp, server = self.servers_client.unlock_server(self.server_id)
self.assertEqual(202, resp.status)
diff --git a/tempest/api/compute/test_live_block_migration.py b/tempest/api/compute/test_live_block_migration.py
index bb8b372..7f68ab5 100644
--- a/tempest/api/compute/test_live_block_migration.py
+++ b/tempest/api/compute/test_live_block_migration.py
@@ -59,7 +59,8 @@
def _migrate_server_to(self, server_id, dest_host):
_resp, body = self.admin_servers_client.live_migrate_server(
server_id, dest_host,
- self.config.compute.use_block_migration_for_live_migration)
+ self.config.compute_feature_enabled.
+ block_migration_for_live_migration)
return body
def _get_host_other_than(self, host):
@@ -97,7 +98,7 @@
self.volumes_client.wait_for_volume_status(volume_id, 'available')
self.volumes_client.delete_volume(volume_id)
- @testtools.skipIf(not CONF.compute.live_migration_available,
+ @testtools.skipIf(not CONF.compute_feature_enabled.live_migration,
'Live migration not available')
@attr(type='gate')
def test_live_block_migration(self):
@@ -112,7 +113,7 @@
self.servers_client.wait_for_server_status(server_id, 'ACTIVE')
self.assertEqual(target_host, self._get_host_for_server(server_id))
- @testtools.skipIf(not CONF.compute.live_migration_available,
+ @testtools.skipIf(not CONF.compute_feature_enabled.live_migration,
'Live migration not available')
@attr(type='gate')
def test_invalid_host_for_migration(self):
@@ -124,10 +125,12 @@
server_id, target_host)
self.assertEqual('ACTIVE', self._get_server_status(server_id))
- @testtools.skipIf(not CONF.compute.live_migration_available or
- not CONF.compute.use_block_migration_for_live_migration,
+ @testtools.skipIf(not CONF.compute_feature_enabled.live_migration or not
+ CONF.compute_feature_enabled.
+ block_migration_for_live_migration,
'Block Live migration not available')
- @testtools.skipIf(not CONF.compute.block_migrate_supports_cinder_iscsi,
+ @testtools.skipIf(not CONF.compute_feature_enabled.
+ block_migrate_cinder_iscsi,
'Block Live migration not configured for iSCSI')
@attr(type='gate')
def test_iscsi_volume(self):
diff --git a/tempest/api/compute/test_quotas.py b/tempest/api/compute/test_quotas.py
index 4617bd2..475d055 100644
--- a/tempest/api/compute/test_quotas.py
+++ b/tempest/api/compute/test_quotas.py
@@ -58,6 +58,16 @@
sorted(quota_set.keys()))
self.assertEqual(quota_set['id'], self.tenant_id)
+ @attr(type='smoke')
+ def test_compare_tenant_quotas_with_default_quotas(self):
+ # Tenants are created with the default quota values
+ resp, defualt_quota_set = \
+ self.client.get_default_quota_set(self.tenant_id)
+ self.assertEqual(200, resp.status)
+ resp, tenant_quota_set = self.client.get_quota_set(self.tenant_id)
+ self.assertEqual(200, resp.status)
+ self.assertEqual(defualt_quota_set, tenant_quota_set)
+
class QuotasTestXML(QuotasTestJSON):
_interface = 'xml'
diff --git a/tempest/api/image/base.py b/tempest/api/image/base.py
index 4f54a15..ab0cb00 100644
--- a/tempest/api/image/base.py
+++ b/tempest/api/image/base.py
@@ -74,17 +74,6 @@
cls.created_images.append(image['id'])
return resp, image
- @classmethod
- def _check_version(cls, version):
- __, versions = cls.client.get_versions()
- if version == 'v2.0':
- if 'v2.0' in versions:
- return True
- elif version == 'v1.0':
- if 'v1.1' in versions or 'v1.0' in versions:
- return True
- return False
-
class BaseV1ImageTest(BaseImageTest):
@@ -92,7 +81,7 @@
def setUpClass(cls):
super(BaseV1ImageTest, cls).setUpClass()
cls.client = cls.os.image_client
- if not cls._check_version('v1.0'):
+ if not cls.config.image_feature_enabled.api_v1:
msg = "Glance API v1 not supported"
raise cls.skipException(msg)
@@ -103,6 +92,6 @@
def setUpClass(cls):
super(BaseV2ImageTest, cls).setUpClass()
cls.client = cls.os.image_client_v2
- if not cls._check_version('v2.0'):
+ if not cls.config.image_feature_enabled.api_v2:
msg = "Glance API v2 not supported"
raise cls.skipException(msg)
diff --git a/tempest/api/image/v2/test_images.py b/tempest/api/image/v2/test_images.py
index eb3535f..ee6d656 100644
--- a/tempest/api/image/v2/test_images.py
+++ b/tempest/api/image/v2/test_images.py
@@ -20,6 +20,7 @@
import random
from tempest.api.image import base
+from tempest.common.utils import data_utils
from tempest import exceptions
from tempest.test import attr
@@ -42,29 +43,46 @@
'test', 'bare', 'wrong')
@attr(type='gate')
- def test_register_then_upload(self):
- # Register, then upload an image
- resp, body = self.create_image(name='New Name',
+ def test_register_upload_get_image_file(self):
+
+ """
+ Here we test these functionalities - Register image,
+ upload the image file, get image and get image file api's
+ """
+
+ image_name = data_utils.rand_name('image')
+ resp, body = self.create_image(name=image_name,
container_format='bare',
disk_format='raw',
visibility='public')
self.assertIn('id', body)
image_id = body.get('id')
self.assertIn('name', body)
- self.assertEqual('New Name', body.get('name'))
+ self.assertEqual(image_name, body['name'])
self.assertIn('visibility', body)
- self.assertTrue(body.get('visibility') == 'public')
+ self.assertEqual('public', body['visibility'])
self.assertIn('status', body)
- self.assertEqual('queued', body.get('status'))
+ self.assertEqual('queued', body['status'])
# Now try uploading an image file
- image_file = StringIO.StringIO(('*' * 1024))
+ file_content = '*' * 1024
+ image_file = StringIO.StringIO(file_content)
resp, body = self.client.store_image(image_id, image_file)
self.assertEqual(resp.status, 204)
- resp, body = self.client.get_image_metadata(image_id)
+
+ # Now try to get image details
+ resp, body = self.client.get_image(image_id)
+ self.assertEqual(200, resp.status)
+ self.assertEqual(image_id, body['id'])
+ self.assertEqual(image_name, body['name'])
self.assertIn('size', body)
self.assertEqual(1024, body.get('size'))
+ # Now try get image file
+ resp, body = self.client.get_image_file(image_id)
+ self.assertEqual(200, resp.status)
+ self.assertEqual(file_content, body)
+
class ListImagesTest(base.BaseV2ImageTest):
@@ -107,6 +125,6 @@
self.assertIn(image, image_list)
@attr(type=['negative', 'gate'])
- def test_get_image_meta_by_null_id(self):
+ def test_get_image_by_null_id(self):
self.assertRaises(exceptions.NotFound,
- self.client.get_image_metadata, '')
+ self.client.get_image, '')
diff --git a/tempest/api/image/v2/test_images_tags.py b/tempest/api/image/v2/test_images_tags.py
new file mode 100644
index 0000000..e37e462
--- /dev/null
+++ b/tempest/api/image/v2/test_images_tags.py
@@ -0,0 +1,45 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR 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.common.utils import data_utils
+from tempest.test import attr
+
+
+class ImagesTagsTest(base.BaseV2ImageTest):
+
+ @attr(type='gate')
+ def test_update_delete_tags_for_image(self):
+ resp, body = self.create_image(container_format='bare',
+ disk_format='raw',
+ visibility='public')
+ image_id = body['id']
+ tag = data_utils.rand_name('tag-')
+ self.addCleanup(self.client.delete_image, image_id)
+
+ # Creating image tag and verify it.
+ resp, body = self.client.add_image_tag(image_id, tag)
+ self.assertEqual(resp.status, 204)
+ resp, body = self.client.get_image(image_id)
+ self.assertEqual(resp.status, 200)
+ self.assertIn(tag, body['tags'])
+
+ # Deleting image tag and verify it.
+ resp = self.client.delete_image_tag(image_id, tag)
+ self.assertEqual(resp.status, 204)
+ resp, body = self.client.get_image(image_id)
+ self.assertEqual(resp.status, 200)
+ 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
new file mode 100644
index 0000000..e0d84de
--- /dev/null
+++ b/tempest/api/image/v2/test_images_tags_negative.py
@@ -0,0 +1,46 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# 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 tempest.api.image import base
+from tempest.common.utils import data_utils
+from tempest import exceptions
+from tempest.test import attr
+
+
+class ImagesTagsNegativeTest(base.BaseV2ImageTest):
+
+ @attr(type=['negative', 'gate'])
+ 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())
+ self.assertRaises(exceptions.NotFound, self.client.add_image_tag,
+ non_exist_image, tag)
+
+ @attr(type=['negative', 'gate'])
+ def test_delete_non_existing_tag(self):
+ # Delete non existing tag.
+ resp, body = self.create_image(container_format='bare',
+ disk_format='raw',
+ is_public=True,
+ )
+ image_id = body['id']
+ tag = data_utils.rand_name('non-exist-tag-')
+ self.addCleanup(self.client.delete_image, image_id)
+ self.assertRaises(exceptions.NotFound, self.client.delete_image_tag,
+ image_id, tag)
diff --git a/tempest/api/network/base.py b/tempest/api/network/base.py
index b6c2679..ed915c1 100644
--- a/tempest/api/network/base.py
+++ b/tempest/api/network/base.py
@@ -68,75 +68,39 @@
@classmethod
def tearDownClass(cls):
- has_exception = False
+ # Clean up vpn services
for vpnservice in cls.vpnservices:
- try:
- cls.client.delete_vpn_service(vpnservice['id'])
- except Exception as exc:
- LOG.exception(exc)
- has_exception = True
-
+ cls.client.delete_vpn_service(vpnservice['id'])
+ # Clean up routers
for router in cls.routers:
- try:
- resp, body = cls.client.list_router_interfaces(router['id'])
- interfaces = body['ports']
- for i in interfaces:
- cls.client.remove_router_interface_with_subnet_id(
- router['id'], i['fixed_ips'][0]['subnet_id'])
- except Exception as exc:
- LOG.exception(exc)
- has_exception = True
- try:
- cls.client.delete_router(router['id'])
- except Exception as exc:
- LOG.exception(exc)
- has_exception = True
-
+ resp, body = cls.client.list_router_interfaces(router['id'])
+ interfaces = body['ports']
+ for i in interfaces:
+ cls.client.remove_router_interface_with_subnet_id(
+ router['id'], i['fixed_ips'][0]['subnet_id'])
+ cls.client.delete_router(router['id'])
+ # Clean up health monitors
for health_monitor in cls.health_monitors:
- try:
- cls.client.delete_health_monitor(health_monitor['id'])
- except Exception as exc:
- LOG.exception(exc)
- has_exception = True
+ cls.client.delete_health_monitor(health_monitor['id'])
+ # Clean up members
for member in cls.members:
- try:
- cls.client.delete_member(member['id'])
- except Exception as exc:
- LOG.exception(exc)
- has_exception = True
+ cls.client.delete_member(member['id'])
+ # Clean up vips
for vip in cls.vips:
- try:
- cls.client.delete_vip(vip['id'])
- except Exception as exc:
- LOG.exception(exc)
- has_exception = True
+ cls.client.delete_vip(vip['id'])
+ # Clean up pools
for pool in cls.pools:
- try:
- cls.client.delete_pool(pool['id'])
- except Exception as exc:
- LOG.exception(exc)
- has_exception = True
+ cls.client.delete_pool(pool['id'])
+ # Clean up ports
for port in cls.ports:
- try:
- cls.client.delete_port(port['id'])
- except Exception as exc:
- LOG.exception(exc)
- has_exception = True
+ cls.client.delete_port(port['id'])
+ # Clean up subnets
for subnet in cls.subnets:
- try:
- cls.client.delete_subnet(subnet['id'])
- except Exception as exc:
- LOG.exception(exc)
- has_exception = True
+ cls.client.delete_subnet(subnet['id'])
+ # Clean up networks
for network in cls.networks:
- try:
- cls.client.delete_network(network['id'])
- except Exception as exc:
- LOG.exception(exc)
- has_exception = True
+ cls.client.delete_network(network['id'])
super(BaseNetworkTest, cls).tearDownClass()
- if has_exception:
- raise exceptions.TearDownException()
@classmethod
def create_network(cls, network_name=None):
@@ -251,3 +215,19 @@
vpnservice = body['vpnservice']
cls.vpnservices.append(vpnservice)
return vpnservice
+
+
+class BaseAdminNetworkTest(BaseNetworkTest):
+
+ @classmethod
+ def setUpClass(cls):
+ super(BaseAdminNetworkTest, cls).setUpClass()
+ admin_username = cls.config.compute_admin.username
+ admin_password = cls.config.compute_admin.password
+ admin_tenant = cls.config.compute_admin.tenant_name
+ if not (admin_username and admin_password and admin_tenant):
+ msg = ("Missing Administrative Network API credentials "
+ "in configuration.")
+ raise cls.skipException(msg)
+ cls.admin_manager = clients.AdminManager(interface=cls._interface)
+ cls.admin_client = cls.admin_manager.network_client
diff --git a/tempest/api/network/base_security_groups.py b/tempest/api/network/base_security_groups.py
new file mode 100644
index 0000000..5ab1748
--- /dev/null
+++ b/tempest/api/network/base_security_groups.py
@@ -0,0 +1,60 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# 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
+from tempest.common.utils import data_utils
+
+
+class BaseSecGroupTest(base.BaseNetworkTest):
+
+ @classmethod
+ def setUpClass(cls):
+ super(BaseSecGroupTest, cls).setUpClass()
+
+ def _create_security_group(self):
+ # Create a security group
+ name = data_utils.rand_name('secgroup-')
+ resp, group_create_body = self.client.create_security_group(name)
+ self.assertEqual('201', resp['status'])
+ self.addCleanup(self._delete_security_group,
+ group_create_body['security_group']['id'])
+ self.assertEqual(group_create_body['security_group']['name'], name)
+ return group_create_body, name
+
+ def _delete_security_group(self, secgroup_id):
+ resp, _ = self.client.delete_security_group(secgroup_id)
+ self.assertEqual(204, resp.status)
+ # Asserting that the security group is not found in the list
+ # after deletion
+ resp, list_body = self.client.list_security_groups()
+ self.assertEqual('200', resp['status'])
+ secgroup_list = list()
+ for secgroup in list_body['security_groups']:
+ secgroup_list.append(secgroup['id'])
+ self.assertNotIn(secgroup_id, secgroup_list)
+
+ def _delete_security_group_rule(self, rule_id):
+ resp, _ = self.client.delete_security_group_rule(rule_id)
+ self.assertEqual(204, resp.status)
+ # Asserting that the security group is not found in the list
+ # after deletion
+ resp, list_body = self.client.list_security_group_rules()
+ self.assertEqual('200', resp['status'])
+ rules_list = list()
+ for rule in list_body['security_group_rules']:
+ rules_list.append(rule['id'])
+ self.assertNotIn(rule_id, rules_list)
diff --git a/tempest/api/network/test_routers.py b/tempest/api/network/test_routers.py
index 2cfbf61..512d065 100644
--- a/tempest/api/network/test_routers.py
+++ b/tempest/api/network/test_routers.py
@@ -20,7 +20,10 @@
from tempest.test import attr
-class RoutersTest(base.BaseNetworkTest):
+class RoutersTest(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
_interface = 'json'
@classmethod
@@ -130,3 +133,99 @@
interface['port_id'])
self.assertEqual(show_port_body['port']['device_id'],
router['id'])
+
+ def _verify_router_gateway(self, router_id, exp_ext_gw_info=None):
+ resp, show_body = self.client.show_router(router_id)
+ self.assertEqual('200', resp['status'])
+ 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.iteritems():
+ self.assertEqual(v, actual_ext_gw_info[k])
+
+ def _verify_gateway_port(self, router_id):
+ resp, list_body = self.admin_client.list_ports(
+ network_id=self.network_cfg.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.assertEqual(len(fixed_ips), 1)
+ resp, public_net_body = self.admin_client.show_network(
+ self.network_cfg.public_network_id)
+ public_subnet_id = public_net_body['network']['subnets'][0]
+ self.assertEqual(fixed_ips[0]['subnet_id'], public_subnet_id)
+
+ @attr(type='smoke')
+ def test_update_router_set_gateway(self):
+ router = self.create_router(rand_name('router-'))
+ self.client.update_router(
+ router['id'],
+ external_gateway_info={
+ 'network_id': self.network_cfg.public_network_id})
+ # Verify operation - router
+ resp, show_body = self.client.show_router(router['id'])
+ self.assertEqual('200', resp['status'])
+ self._verify_router_gateway(
+ router['id'],
+ {'network_id': self.network_cfg.public_network_id})
+ self._verify_gateway_port(router['id'])
+
+ @attr(type='smoke')
+ def test_update_router_set_gateway_with_snat_explicit(self):
+ router = self.create_router(rand_name('router-'))
+ self.admin_client.update_router_with_snat_gw_info(
+ router['id'],
+ external_gateway_info={
+ 'network_id': self.network_cfg.public_network_id,
+ 'enable_snat': True})
+ self._verify_router_gateway(
+ router['id'],
+ {'network_id': self.network_cfg.public_network_id,
+ 'enable_snat': True})
+ self._verify_gateway_port(router['id'])
+
+ @attr(type='smoke')
+ def test_update_router_set_gateway_without_snat(self):
+ router = self.create_router(rand_name('router-'))
+ self.admin_client.update_router_with_snat_gw_info(
+ router['id'],
+ external_gateway_info={
+ 'network_id': self.network_cfg.public_network_id,
+ 'enable_snat': False})
+ self._verify_router_gateway(
+ router['id'],
+ {'network_id': self.network_cfg.public_network_id,
+ 'enable_snat': False})
+ self._verify_gateway_port(router['id'])
+
+ @attr(type='smoke')
+ def test_update_router_unset_gateway(self):
+ router = self.create_router(
+ rand_name('router-'),
+ external_network_id=self.network_cfg.public_network_id)
+ self.client.update_router(router['id'], external_gateway_info={})
+ self._verify_router_gateway(router['id'])
+ # No gateway port expected
+ resp, list_body = self.admin_client.list_ports(
+ network_id=self.network_cfg.public_network_id,
+ device_id=router['id'])
+ self.assertFalse(list_body['ports'])
+
+ @attr(type='smoke')
+ def test_update_router_reset_gateway_without_snat(self):
+ router = self.create_router(
+ rand_name('router-'),
+ external_network_id=self.network_cfg.public_network_id)
+ self.admin_client.update_router_with_snat_gw_info(
+ router['id'],
+ external_gateway_info={
+ 'network_id': self.network_cfg.public_network_id,
+ 'enable_snat': False})
+ self._verify_router_gateway(
+ router['id'],
+ {'network_id': self.network_cfg.public_network_id,
+ 'enable_snat': False})
+ self._verify_gateway_port(router['id'])
diff --git a/tempest/api/network/test_security_groups.py b/tempest/api/network/test_security_groups.py
index 9218f0c..9b0a3de 100644
--- a/tempest/api/network/test_security_groups.py
+++ b/tempest/api/network/test_security_groups.py
@@ -15,42 +15,13 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest.api.network import base
-from tempest.common.utils import data_utils
+from tempest.api.network import base_security_groups as base
from tempest.test import attr
-class SecGroupTest(base.BaseNetworkTest):
+class SecGroupTest(base.BaseSecGroupTest):
_interface = 'json'
- @classmethod
- def setUpClass(cls):
- super(SecGroupTest, cls).setUpClass()
-
- def _delete_security_group(self, secgroup_id):
- resp, _ = self.client.delete_security_group(secgroup_id)
- self.assertEqual(204, resp.status)
- # Asserting that the security group is not found in the list
- # after deletion
- resp, list_body = self.client.list_security_groups()
- self.assertEqual('200', resp['status'])
- secgroup_list = list()
- for secgroup in list_body['security_groups']:
- secgroup_list.append(secgroup['id'])
- self.assertNotIn(secgroup_id, secgroup_list)
-
- def _delete_security_group_rule(self, rule_id):
- resp, _ = self.client.delete_security_group_rule(rule_id)
- self.assertEqual(204, resp.status)
- # Asserting that the security group is not found in the list
- # after deletion
- resp, list_body = self.client.list_security_group_rules()
- self.assertEqual('200', resp['status'])
- rules_list = list()
- for rule in list_body['security_group_rules']:
- rules_list.append(rule['id'])
- self.assertNotIn(rule_id, rules_list)
-
@attr(type='smoke')
def test_list_security_groups(self):
# Verify the that security group belonging to tenant exist in list
@@ -66,13 +37,7 @@
@attr(type='smoke')
def test_create_show_delete_security_group(self):
- # Create a security group
- name = data_utils.rand_name('secgroup-')
- resp, group_create_body = self.client.create_security_group(name)
- self.assertEqual('201', resp['status'])
- self.addCleanup(self._delete_security_group,
- group_create_body['security_group']['id'])
- self.assertEqual(group_create_body['security_group']['name'], name)
+ group_create_body, name = self._create_security_group()
# Show details of the created security group
resp, show_body = self.client.show_security_group(
@@ -90,13 +55,7 @@
@attr(type='smoke')
def test_create_show_delete_security_group_rule(self):
- # Create a security group
- name = data_utils.rand_name('secgroup-')
- resp, group_create_body = self.client.create_security_group(name)
- self.assertEqual('201', resp['status'])
- self.addCleanup(self._delete_security_group,
- group_create_body['security_group']['id'])
- self.assertEqual(group_create_body['security_group']['name'], name)
+ group_create_body, _ = self._create_security_group()
# Create rules for each protocol
protocols = ['tcp', 'udp', 'icmp']
diff --git a/tempest/api/network/test_security_groups_negative.py b/tempest/api/network/test_security_groups_negative.py
index daeb89f..cb0c247 100644
--- a/tempest/api/network/test_security_groups_negative.py
+++ b/tempest/api/network/test_security_groups_negative.py
@@ -15,46 +15,39 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest.api.network import test_security_groups as base
-from tempest.common.utils import data_utils
+from tempest.api.network import base_security_groups as base
from tempest import exceptions
from tempest.test import attr
import uuid
-class NegativeSecGroupTest(base.SecGroupTest):
+class NegativeSecGroupTest(base.BaseSecGroupTest):
_interface = 'json'
- @attr(type=['negative', 'smoke'])
+ @attr(type=['negative', 'gate'])
def test_show_non_existent_security_group(self):
non_exist_id = str(uuid.uuid4())
self.assertRaises(exceptions.NotFound, self.client.show_security_group,
non_exist_id)
- @attr(type=['negative', 'smoke'])
+ @attr(type=['negative', 'gate'])
def test_show_non_existent_security_group_rule(self):
non_exist_id = str(uuid.uuid4())
self.assertRaises(exceptions.NotFound,
self.client.show_security_group_rule,
non_exist_id)
- @attr(type=['negative', 'smoke'])
+ @attr(type=['negative', 'gate'])
def test_delete_non_existent_security_group(self):
- non_exist_id = 'fictional-id'
+ non_exist_id = str(uuid.uuid4())
self.assertRaises(exceptions.NotFound,
self.client.delete_security_group,
non_exist_id
)
- @attr(type=['negative', 'smoke'])
+ @attr(type=['negative', 'gate'])
def test_create_security_group_rule_with_bad_protocol(self):
- # Create a security group
- name = data_utils.rand_name('secgroup-')
- resp, group_create_body = self.client.create_security_group(name)
- self.assertEqual('201', resp['status'])
- self.addCleanup(self._delete_security_group,
- group_create_body['security_group']['id'])
- self.assertEqual(group_create_body['security_group']['name'], name)
+ group_create_body, _ = self._create_security_group()
#Create rule with bad protocol name
pname = 'bad_protocol_name'
@@ -63,20 +56,15 @@
group_create_body['security_group']['id'],
protocol=pname)
- @attr(type=['negative', 'smoke'])
+ @attr(type=['negative', 'gate'])
def test_create_security_group_rule_with_invalid_ports(self):
- # Create a security group
- name = data_utils.rand_name('secgroup-')
- resp, group_create_body = self.client.create_security_group(name)
- self.assertEqual('201', resp['status'])
- self.addCleanup(self._delete_security_group,
- group_create_body['security_group']['id'])
- self.assertEqual(group_create_body['security_group']['name'], name)
+ group_create_body, _ = self._create_security_group()
#Create rule with invalid ports
states = [(-16, 80, 'Invalid value for port -16'),
(80, 79, 'port_range_min must be <= port_range_max'),
- (80, 65536, 'Invalid value for port 65536')]
+ (80, 65536, 'Invalid value for port 65536'),
+ (-16, 65536, 'Invalid value for port')]
for pmin, pmax, msg in states:
ex = self.assertRaises(exceptions.BadRequest,
self.client.create_security_group_rule,
diff --git a/tempest/api/object_storage/test_account_quotas.py b/tempest/api/object_storage/test_account_quotas.py
index 65fe1ac..a90d3f4 100644
--- a/tempest/api/object_storage/test_account_quotas.py
+++ b/tempest/api/object_storage/test_account_quotas.py
@@ -20,14 +20,14 @@
from tempest import clients
from tempest.common.utils.data_utils import arbitrary_string
from tempest.common.utils.data_utils import rand_name
-import tempest.config
+from tempest import config
from tempest import exceptions
from tempest.test import attr
class AccountQuotasTest(base.BaseObjectTest):
accounts_quotas_available = \
- tempest.config.TempestConfig().object_storage.accounts_quotas_available
+ config.TempestConfig().object_storage_feature_enabled.accounts_quotas
@classmethod
def setUpClass(cls):
diff --git a/tempest/api/object_storage/test_account_services.py b/tempest/api/object_storage/test_account_services.py
index 1a3f775..90b0914 100644
--- a/tempest/api/object_storage/test_account_services.py
+++ b/tempest/api/object_storage/test_account_services.py
@@ -101,7 +101,7 @@
'limit': limit}
resp, container_list = \
self.account_client.list_account_containers(params=params)
- self.assertLessEqual(len(container_list), limit)
+ self.assertTrue(len(container_list) <= limit, str(container_list))
@attr(type='smoke')
def test_list_account_metadata(self):
diff --git a/tempest/api/object_storage/test_container_quotas.py b/tempest/api/object_storage/test_container_quotas.py
index 31fe711..2e0d76a 100644
--- a/tempest/api/object_storage/test_container_quotas.py
+++ b/tempest/api/object_storage/test_container_quotas.py
@@ -33,7 +33,7 @@
class ContainerQuotasTest(base.BaseObjectTest):
"""Attemps to test the perfect behavior of quotas in a container."""
container_quotas_available = \
- config.TempestConfig().object_storage.container_quotas_available
+ config.TempestConfig().object_storage_feature_enabled.container_quotas
def setUp(self):
"""Creates and sets a container with quotas.
diff --git a/tempest/api/object_storage/test_healthcheck.py b/tempest/api/object_storage/test_healthcheck.py
new file mode 100644
index 0000000..798ea4f
--- /dev/null
+++ b/tempest/api/object_storage/test_healthcheck.py
@@ -0,0 +1,65 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (C) 2013 eNovance SAS <licensing@enovance.com>
+#
+# Author: Joe H. Rahme <joe.hakim.rahme@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.object_storage import base
+from tempest import clients
+from tempest.test import attr
+from tempest.test import HTTP_SUCCESS
+
+
+class HealthcheckTest(base.BaseObjectTest):
+
+ @classmethod
+ def setUpClass(cls):
+ super(HealthcheckTest, cls).setUpClass()
+
+ # creates a test user. The test user will set its base_url to the Swift
+ # endpoint and test the healthcheck feature.
+ cls.data.setup_test_user()
+
+ cls.os_test_user = clients.Manager(
+ cls.data.test_user,
+ cls.data.test_password,
+ cls.data.test_tenant)
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.data.teardown_all()
+ super(HealthcheckTest, cls).tearDownClass()
+
+ def setUp(self):
+ super(HealthcheckTest, self).setUp()
+ client = self.os_test_user.account_client
+ client._set_auth()
+
+ # Turning http://.../v1/foobar into http://.../
+ client.base_url = "/".join(client.base_url.split("/")[:-2])
+
+ def tearDown(self):
+ # clear the base_url for subsequent requests
+ self.os_test_user.account_client.base_url = None
+ super(HealthcheckTest, self).tearDown()
+
+ @attr('gate')
+ def test_get_healthcheck(self):
+
+ resp, _ = self.os_test_user.account_client.get("healthcheck", {})
+
+ # The status is expected to be 200
+ self.assertIn(int(resp['status']), HTTP_SUCCESS)
diff --git a/tempest/api/volume/admin/test_multi_backend.py b/tempest/api/volume/admin/test_multi_backend.py
index 797aa71..eada639 100644
--- a/tempest/api/volume/admin/test_multi_backend.py
+++ b/tempest/api/volume/admin/test_multi_backend.py
@@ -28,7 +28,7 @@
@classmethod
def setUpClass(cls):
super(VolumeMultiBackendTest, cls).setUpClass()
- if not cls.config.volume.multi_backend_enabled:
+ if not cls.config.volume_feature_enabled.multi_backend:
raise cls.skipException("Cinder multi-backend feature disabled")
cls.backend1_name = cls.config.volume.backend1_name
diff --git a/tempest/api/volume/admin/test_volumes_actions.py b/tempest/api/volume/admin/test_volumes_actions.py
new file mode 100644
index 0000000..4063eef
--- /dev/null
+++ b/tempest/api/volume/admin/test_volumes_actions.py
@@ -0,0 +1,89 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Huawei 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.base import BaseVolumeAdminTest
+from tempest.common.utils import data_utils as utils
+from tempest.test import attr
+
+
+class VolumesActionsTest(BaseVolumeAdminTest):
+ _interface = "json"
+
+ @classmethod
+ def setUpClass(cls):
+ super(VolumesActionsTest, cls).setUpClass()
+ cls.client = cls.volumes_client
+
+ # Create admin volume client
+ cls.admin_volume_client = cls.os_adm.volumes_client
+
+ # Create a test shared volume for tests
+ vol_name = utils.rand_name(cls.__name__ + '-Volume-')
+
+ resp, cls.volume = cls.client.create_volume(size=1,
+ display_name=vol_name)
+ cls.client.wait_for_volume_status(cls.volume['id'], 'available')
+
+ @classmethod
+ def tearDownClass(cls):
+ # Delete the test volume
+ cls.client.delete_volume(cls.volume['id'])
+ cls.client.wait_for_resource_deletion(cls.volume['id'])
+
+ super(VolumesActionsTest, cls).tearDownClass()
+
+ def _reset_volume_status(self, volume_id, status):
+ #Reset the volume status
+ resp, body = self.admin_volume_client.reset_volume_status(volume_id,
+ status)
+ return resp, body
+
+ def tearDown(self):
+ # Set volume's status to available after test
+ self._reset_volume_status(self.volume['id'], 'available')
+ super(VolumesActionsTest, self).tearDown()
+
+ @attr(type='gate')
+ def test_volume_reset_status(self):
+ # test volume reset status : available->error->available
+ resp, body = self._reset_volume_status(self.volume['id'], 'error')
+ self.assertEqual(202, resp.status)
+ resp_get, volume_get = self.admin_volume_client.get_volume(
+ self.volume['id'])
+ self.assertEqual('error', volume_get['status'])
+
+ @attr(type='gate')
+ def test_volume_begin_detaching(self):
+ # test volume begin detaching : available -> detaching -> available
+ resp, body = self.client.volume_begin_detaching(self.volume['id'])
+ self.assertEqual(202, resp.status)
+ resp_get, volume_get = self.client.get_volume(self.volume['id'])
+ self.assertEqual('detaching', volume_get['status'])
+
+ @attr(type='gate')
+ def test_volume_roll_detaching(self):
+ # test volume roll detaching : detaching -> in-use -> available
+ resp, body = self.client.volume_begin_detaching(self.volume['id'])
+ self.assertEqual(202, resp.status)
+ resp, body = self.client.volume_roll_detaching(self.volume['id'])
+ self.assertEqual(202, resp.status)
+ resp_get, volume_get = self.client.get_volume(self.volume['id'])
+ self.assertEqual('in-use', volume_get['status'])
+
+
+class VolumesActionsTestXML(VolumesActionsTest):
+ _interface = "xml"
diff --git a/tempest/api/volume/base.py b/tempest/api/volume/base.py
index 98694c5..cdf8638 100644
--- a/tempest/api/volume/base.py
+++ b/tempest/api/volume/base.py
@@ -18,7 +18,6 @@
import time
from tempest import clients
-from tempest.common import isolated_creds
from tempest.openstack.common import log as logging
import tempest.test
@@ -32,21 +31,12 @@
@classmethod
def setUpClass(cls):
super(BaseVolumeTest, cls).setUpClass()
- cls.isolated_creds = isolated_creds.IsolatedCreds(cls.__name__)
if not cls.config.service_available.cinder:
skip_msg = ("%s skipped as Cinder is not available" % cls.__name__)
raise cls.skipException(skip_msg)
- if cls.config.compute.allow_tenant_isolation:
- creds = cls.isolated_creds.get_primary_creds()
- username, tenant_name, password = creds
- os = clients.Manager(username=username,
- password=password,
- tenant_name=tenant_name,
- interface=cls._interface)
- else:
- os = clients.Manager(interface=cls._interface)
+ os = cls.get_client_manager()
cls.os = os
cls.volumes_client = os.volumes_client
@@ -69,7 +59,7 @@
def tearDownClass(cls):
cls.clear_snapshots()
cls.clear_volumes()
- cls.isolated_creds.clear_isolated_creds()
+ cls.clear_isolated_creds()
super(BaseVolumeTest, cls).tearDownClass()
@classmethod
diff --git a/tempest/api/volume/test_volumes_actions.py b/tempest/api/volume/test_volumes_actions.py
index 09131e2..f12d4bb 100644
--- a/tempest/api/volume/test_volumes_actions.py
+++ b/tempest/api/volume/test_volumes_actions.py
@@ -16,7 +16,7 @@
# under the License.
from tempest.api.volume.base import BaseVolumeTest
-from tempest.common.utils.data_utils import rand_name
+from tempest.common.utils import data_utils
from tempest.test import attr
from tempest.test import services
from tempest.test import stresstest
@@ -32,8 +32,8 @@
cls.image_client = cls.os.image_client
# Create a test shared instance and volume for attach/detach tests
- srv_name = rand_name(cls.__name__ + '-Instance-')
- vol_name = rand_name(cls.__name__ + '-Volume-')
+ srv_name = data_utils.rand_name(cls.__name__ + '-Instance-')
+ vol_name = data_utils.rand_name(cls.__name__ + '-Volume-')
resp, cls.server = cls.servers_client.create_server(srv_name,
cls.image_ref,
cls.flavor_ref)
@@ -102,7 +102,7 @@
# 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 = rand_name('Image-')
+ image_name = data_utils.rand_name('Image-')
resp, body = self.client.upload_volume(self.volume['id'],
image_name,
self.config.volume.disk_format)
@@ -112,6 +112,17 @@
self.image_client.wait_for_image_status(image_id, 'active')
self.client.wait_for_volume_status(self.volume['id'], 'available')
+ @attr(type='gate')
+ def test_volume_extend(self):
+ # Extend Volume Test.
+ extend_size = int(self.volume['size']) + 1
+ resp, body = self.client.extend_volume(self.volume['id'], extend_size)
+ self.assertEqual(202, resp.status)
+ self.client.wait_for_volume_status(self.volume['id'], 'available')
+ resp, volume = self.client.get_volume(self.volume['id'])
+ self.assertEqual(200, resp.status)
+ self.assertEqual(int(volume['size']), extend_size)
+
class VolumesActionsTestXML(VolumesActionsTest):
_interface = "xml"
diff --git a/tempest/api/volume/test_volumes_negative.py b/tempest/api/volume/test_volumes_negative.py
index 02adc5d..9bab9a0 100644
--- a/tempest/api/volume/test_volumes_negative.py
+++ b/tempest/api/volume/test_volumes_negative.py
@@ -177,6 +177,41 @@
self.client.detach_volume,
'xxx')
+ @attr(type=['negative', 'gate'])
+ def test_volume_extend_with_size_smaller_than_original_size(self):
+ # Extend volume with smaller size than original size.
+ extend_size = 0
+ self.assertRaises(exceptions.BadRequest, self.client.extend_volume,
+ self.volume['id'], extend_size)
+
+ @attr(type=['negative', 'gate'])
+ def test_volume_extend_with_non_number_size(self):
+ # Extend volume when size is non number.
+ extend_size = 'abc'
+ self.assertRaises(exceptions.BadRequest, self.client.extend_volume,
+ self.volume['id'], extend_size)
+
+ @attr(type=['negative', 'gate'])
+ def test_volume_extend_with_None_size(self):
+ # Extend volume with None size.
+ extend_size = None
+ self.assertRaises(exceptions.BadRequest, self.client.extend_volume,
+ self.volume['id'], extend_size)
+
+ @attr(type=['negative', 'gate'])
+ 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(exceptions.NotFound, self.client.extend_volume,
+ str(uuid.uuid4()), extend_size)
+
+ @attr(type=['negative', 'gate'])
+ 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(exceptions.NotFound, self.client.extend_volume,
+ None, extend_size)
+
class VolumesNegativeTestXML(VolumesNegativeTest):
_interface = 'xml'
diff --git a/tempest/api/volume/test_volumes_snapshots_negative.py b/tempest/api/volume/test_volumes_snapshots_negative.py
new file mode 100644
index 0000000..04a4774
--- /dev/null
+++ b/tempest/api/volume/test_volumes_snapshots_negative.py
@@ -0,0 +1,44 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# 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 tempest.api.volume import base
+from tempest.common.utils import data_utils
+from tempest import exceptions
+from tempest.test import attr
+
+
+class VolumesSnapshotNegativeTest(base.BaseVolumeTest):
+ _interface = "json"
+
+ @attr(type=['negative', 'gate'])
+ def test_create_snapshot_with_nonexistent_volume_id(self):
+ # Create a snapshot with nonexistent volume id
+ s_name = data_utils.rand_name('snap')
+ self.assertRaises(exceptions.NotFound,
+ self.snapshots_client.create_snapshot,
+ str(uuid.uuid4()), display_name=s_name)
+
+ @attr(type=['negative', 'gate'])
+ def test_create_snapshot_without_passing_volume_id(self):
+ # Create a snapshot without passing volume id
+ s_name = data_utils.rand_name('snap')
+ self.assertRaises(exceptions.NotFound,
+ self.snapshots_client.create_snapshot,
+ None, display_name=s_name)
+
+
+class VolumesSnapshotNegativeTestXML(VolumesSnapshotNegativeTest):
+ _interface = "xml"
diff --git a/tempest/cli/__init__.py b/tempest/cli/__init__.py
index b082b1e..bd1b44f 100644
--- a/tempest/cli/__init__.py
+++ b/tempest/cli/__init__.py
@@ -33,7 +33,7 @@
default=True,
help="enable cli tests"),
cfg.StrOpt('cli_dir',
- default='/usr/local/bin/',
+ default='/usr/local/bin',
help="directory where python client binaries are located"),
cfg.IntOpt('timeout',
default=15,
@@ -80,6 +80,12 @@
return self.cmd_with_auth(
'glance', action, flags, params, admin, fail_ok)
+ def ceilometer(self, action, flags='', params='', admin=True,
+ fail_ok=False):
+ """Executes ceilometer command for the given action."""
+ return self.cmd_with_auth(
+ 'ceilometer', action, flags, params, admin, fail_ok)
+
def cinder(self, action, flags='', params='', admin=True, fail_ok=False):
"""Executes cinder command for the given action."""
return self.cmd_with_auth(
diff --git a/tempest/cli/simple_read_only/test_ceilometer.py b/tempest/cli/simple_read_only/test_ceilometer.py
new file mode 100644
index 0000000..7f2864f
--- /dev/null
+++ b/tempest/cli/simple_read_only/test_ceilometer.py
@@ -0,0 +1,51 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# 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.config import cfg
+
+import tempest.cli
+from tempest.openstack.common import log as logging
+
+CONF = cfg.CONF
+
+LOG = logging.getLogger(__name__)
+
+
+class SimpleReadOnlyCeilometerClientTest(tempest.cli.ClientTestBase):
+ """Basic, read-only tests for Ceilometer CLI client.
+
+ Checks return values and output of read-only commands.
+ These tests do not presume any content, nor do they create
+ their own. They only verify the structure of output if present.
+ """
+
+ @classmethod
+ def setUpClass(cls):
+ if (not CONF.service_available.ceilometer):
+ msg = ("Skiping all Ceilometer cli tests because it is"
+ "not available")
+ raise cls.skipException(msg)
+ super(SimpleReadOnlyCeilometerClientTest, cls).setUpClass()
+
+ def test_ceilometer_meter_list(self):
+ self.ceilometer('meter-list')
+
+ def test_ceilometer_resource_list(self):
+ self.ceilometer('resource-list')
+
+ def test_ceilometermeter_alarm_list(self):
+ self.ceilometer('alarm-list')
diff --git a/tempest/cli/simple_read_only/test_glance.py b/tempest/cli/simple_read_only/test_glance.py
index d02c60b..a5a229c 100644
--- a/tempest/cli/simple_read_only/test_glance.py
+++ b/tempest/cli/simple_read_only/test_glance.py
@@ -18,9 +18,13 @@
import re
import subprocess
+from oslo.config import cfg
+
import tempest.cli
from tempest.openstack.common import log as logging
+CONF = cfg.CONF
+
LOG = logging.getLogger(__name__)
@@ -45,6 +49,14 @@
'ID', 'Name', 'Disk Format', 'Container Format',
'Size', 'Status'])
+ def test_glance_member_list(self):
+ tenant_name = '--tenant-id %s' % self.identity.admin_tenant_name
+ out = self.glance('member-list',
+ params=tenant_name)
+ endpoints = self.parser.listing(out)
+ self.assertTableStruct(endpoints,
+ ['Image ID', 'Member ID', 'Can Share'])
+
def test_glance_help(self):
help_text = self.glance('help')
lines = help_text.split('\n')
@@ -64,3 +76,14 @@
'member-add', 'member-create', 'member-delete',
'member-list'))
self.assertFalse(wanted_commands - commands)
+
+ # Optional arguments:
+
+ def test_glance_version(self):
+ self.glance('', flags='--version')
+
+ def test_glance_debug_list(self):
+ self.glance('image-list', flags='--debug')
+
+ def test_glance_timeout(self):
+ self.glance('image-list', flags='--timeout %d' % CONF.cli.timeout)
diff --git a/tempest/cli/simple_read_only/test_nova_manage.py b/tempest/cli/simple_read_only/test_nova_manage.py
index 9a33556..524db5d 100644
--- a/tempest/cli/simple_read_only/test_nova_manage.py
+++ b/tempest/cli/simple_read_only/test_nova_manage.py
@@ -55,11 +55,11 @@
self.nova_manage('', '--version', merge_stderr=True))
def test_debug_flag(self):
- self.assertNotEqual("", self.nova_manage('instance_type list',
+ self.assertNotEqual("", self.nova_manage('flavor list',
'--debug'))
def test_verbose_flag(self):
- self.assertNotEqual("", self.nova_manage('instance_type list',
+ self.assertNotEqual("", self.nova_manage('flavor list',
'--verbose'))
# test actions
@@ -68,8 +68,6 @@
def test_flavor_list(self):
self.assertNotEqual("", self.nova_manage('flavor list'))
- self.assertEqual(self.nova_manage('instance_type list'),
- self.nova_manage('flavor list'))
def test_db_archive_deleted_rows(self):
# make sure command doesn't error out
diff --git a/tempest/common/tempest_fixtures.py b/tempest/common/tempest_fixtures.py
index ebc9ad3..73c02e8 100644
--- a/tempest/common/tempest_fixtures.py
+++ b/tempest/common/tempest_fixtures.py
@@ -15,7 +15,7 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest.openstack.common import lockutils
+from tempest.openstack.common.fixture import lockutils
class LockFixture(lockutils.LockFixture):
diff --git a/tempest/config.py b/tempest/config.py
index db923e9..925c4bd 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -137,28 +137,6 @@
default="password",
help="Password used to authenticate to an instance using "
"the alternate image."),
- cfg.BoolOpt('resize_available',
- default=False,
- help="Does the test environment support resizing?"),
- cfg.BoolOpt('live_migration_available',
- default=False,
- help="Does the test environment support live migration "
- "available?"),
- cfg.BoolOpt('use_block_migration_for_live_migration',
- default=False,
- help="Does the test environment use block devices for live "
- "migration"),
- cfg.BoolOpt('block_migrate_supports_cinder_iscsi',
- default=False,
- help="Does the test environment block migration support "
- "cinder iSCSI volumes"),
- cfg.BoolOpt('change_password_available',
- default=False,
- help="Does the test environment support changing the admin "
- "password?"),
- cfg.BoolOpt('create_image_enabled',
- default=False,
- help="Does the test environment support snapshots?"),
cfg.IntOpt('build_interval',
default=10,
help="Time in seconds between build status checks."),
@@ -212,18 +190,47 @@
default=None,
help="Path to a private key file for SSH access to remote "
"hosts"),
- cfg.BoolOpt('disk_config_enabled',
- default=True,
- help="If false, skip disk config tests"),
- cfg.BoolOpt('flavor_extra_enabled',
- default=True,
- help="If false, skip flavor extra data test"),
cfg.StrOpt('volume_device_name',
default='vdb',
help="Expected device name when a volume is attached to "
"an instance")
]
+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"),
+ cfg.BoolOpt('flavor_extra',
+ default=True,
+ help="If false, skip flavor extra data test"),
+ cfg.BoolOpt('change_password',
+ default=False,
+ help="Does the test environment support changing the admin "
+ "password?"),
+ cfg.BoolOpt('create_image',
+ default=False,
+ help="Does the test environment support snapshots?"),
+ cfg.BoolOpt('resize',
+ default=False,
+ help="Does the test environment support resizing?"),
+ cfg.BoolOpt('live_migration',
+ default=False,
+ help="Does the test environment support live migration "
+ "available?"),
+ cfg.BoolOpt('block_migration_for_live_migration',
+ default=False,
+ help="Does the test environment use block devices for live "
+ "migration"),
+ cfg.BoolOpt('block_migrate_cinder_iscsi',
+ default=False,
+ help="Does the test environment block migration support "
+ "cinder iSCSI volumes")
+]
+
+
compute_admin_group = cfg.OptGroup(name='compute-admin',
title="Compute Admin Options")
@@ -245,9 +252,6 @@
title="Image Service Options")
ImageGroup = [
- cfg.StrOpt('api_version',
- default='1',
- help="Version of the API"),
cfg.StrOpt('catalog_type',
default='image',
help='Catalog type of the Image service.'),
@@ -263,6 +267,17 @@
help='http accessible image')
]
+image_feature_group = cfg.OptGroup(name='image-feature-enabled',
+ title='Enabled image service features')
+
+ImageFeaturesGroup = [
+ cfg.BoolOpt('api_v2',
+ default=True,
+ help="Is the v2 image API enabled"),
+ cfg.BoolOpt('api_v1',
+ default=True,
+ help="Is the v1 image API enabled"),
+]
network_group = cfg.OptGroup(name='network',
title='Network Service Options')
@@ -309,7 +324,7 @@
help='Timeout in seconds to wait for a volume to become'
'available.'),
cfg.StrOpt('catalog_type',
- default='Volume',
+ default='volume',
help="Catalog type of the Volume Service"),
cfg.StrOpt('region',
default='',
@@ -317,9 +332,6 @@
"of identity.region is used instead. If no such region "
"is found in the service catalog, the first found one is "
"used."),
- cfg.BoolOpt('multi_backend_enabled',
- default=False,
- help="Runs Cinder multi-backend test (requires 2 backends)"),
cfg.StrOpt('backend1_name',
default='BACKEND_1',
help="Name of the backend1 (must be declared in cinder.conf)"),
@@ -337,6 +349,15 @@
help='Disk format to use when copying a volume to image'),
]
+volume_feature_group = cfg.OptGroup(name='volume-feature-enabled',
+ title='Enabled Cinder Features')
+
+VolumeFeaturesGroup = [
+ cfg.BoolOpt('multi_backend',
+ default=False,
+ help="Runs Cinder multi-backend test (requires 2 backends)")
+]
+
object_storage_group = cfg.OptGroup(name='object-storage',
title='Object Storage Service Options')
@@ -351,27 +372,34 @@
"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('container_sync_timeout',
+ cfg.IntOpt('container_sync_timeout',
default=120,
help="Number of seconds to time on waiting for a container"
"to container synchronization complete."),
- cfg.StrOpt('container_sync_interval',
+ cfg.IntOpt('container_sync_interval',
default=5,
help="Number of seconds to wait while looping to check the"
"status of a container to container synchronization"),
- cfg.BoolOpt('accounts_quotas_available',
- default=True,
- help="Set to True if the Account Quota middleware is enabled"),
- cfg.BoolOpt('container_quotas_available',
- default=True,
- help="Set to True if the container quota middleware "
- "is enabled"),
cfg.StrOpt('operator_role',
default='Member',
help="Role to add to users created for swift tests to "
"enable creating containers"),
]
+object_storage_feature_group = cfg.OptGroup(
+ name='object-storage-feature-enabled',
+ title='Enabled object-storage features')
+
+ObjectStoreFeaturesGroup = [
+ cfg.BoolOpt('container_quotas',
+ default=True,
+ help="Set to True if the container quota middleware "
+ "is enabled"),
+ cfg.BoolOpt('accounts_quotas',
+ default=True,
+ help="Set to True if the Account Quota middleware is enabled"),
+]
+
orchestration_group = cfg.OptGroup(name='orchestration',
title='Orchestration Service Options')
@@ -410,7 +438,7 @@
default=None,
help="Name of existing keypair to launch servers with."),
cfg.IntOpt('max_template_size',
- default=32768,
+ default=524288,
help="Value must match heat configuration of the same name."),
]
@@ -499,10 +527,10 @@
cfg.StrOpt('target_logfiles',
default=None,
help='regexp for list of log files.'),
- cfg.StrOpt('log_check_interval',
+ cfg.IntOpt('log_check_interval',
default=60,
help='time (in seconds) between log file error checks.'),
- cfg.StrOpt('default_thread_number_per_action',
+ cfg.IntOpt('default_thread_number_per_action',
default=4,
help='The number of threads created while stress test.')
]
@@ -557,6 +585,9 @@
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('horizon',
default=True,
help="Whether or not Horizon is expected to be available"),
@@ -611,11 +642,18 @@
LOG.info("Using tempest config file %s" % path)
register_opt_group(cfg.CONF, compute_group, ComputeGroup)
+ register_opt_group(cfg.CONF, compute_features_group,
+ ComputeFeaturesGroup)
register_opt_group(cfg.CONF, identity_group, IdentityGroup)
register_opt_group(cfg.CONF, image_group, ImageGroup)
+ register_opt_group(cfg.CONF, image_feature_group, ImageFeaturesGroup)
register_opt_group(cfg.CONF, network_group, NetworkGroup)
register_opt_group(cfg.CONF, volume_group, VolumeGroup)
+ register_opt_group(cfg.CONF, volume_feature_group,
+ VolumeFeaturesGroup)
register_opt_group(cfg.CONF, object_storage_group, ObjectStoreGroup)
+ register_opt_group(cfg.CONF, object_storage_feature_group,
+ ObjectStoreFeaturesGroup)
register_opt_group(cfg.CONF, orchestration_group, OrchestrationGroup)
register_opt_group(cfg.CONF, dashboard_group, DashboardGroup)
register_opt_group(cfg.CONF, boto_group, BotoGroup)
@@ -626,11 +664,16 @@
ServiceAvailableGroup)
register_opt_group(cfg.CONF, debug_group, DebugGroup)
self.compute = cfg.CONF.compute
+ self.compute_feature_enabled = cfg.CONF['compute-feature-enabled']
self.identity = cfg.CONF.identity
self.images = cfg.CONF.image
+ self.image_feature_enabled = cfg.CONF['image-feature-enabled']
self.network = cfg.CONF.network
self.volume = cfg.CONF.volume
+ self.volume_feature_enabled = cfg.CONF['volume-feature-enabled']
self.object_storage = cfg.CONF['object-storage']
+ self.object_storage_feature_enabled = cfg.CONF[
+ 'object-storage-feature-enabled']
self.orchestration = cfg.CONF.orchestration
self.dashboard = cfg.CONF.dashboard
self.boto = cfg.CONF.boto
diff --git a/tempest/exceptions.py b/tempest/exceptions.py
index 8d05fd8..67406b0 100644
--- a/tempest/exceptions.py
+++ b/tempest/exceptions.py
@@ -81,6 +81,10 @@
message = "Server %(server_id)s failed to build and is in ERROR status"
+class ImageKilledException(TempestException):
+ message = "Image %(image_id)s 'killed' while waiting for %(status)s"
+
+
class AddImageException(TempestException):
message = "Image %(image_id)s failed to become ACTIVE in the allotted time"
diff --git a/tempest/openstack/common/fixture/__init__.py b/tempest/openstack/common/fixture/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/openstack/common/fixture/__init__.py
diff --git a/tempest/openstack/common/fixture/config.py b/tempest/openstack/common/fixture/config.py
new file mode 100644
index 0000000..7b044ef
--- /dev/null
+++ b/tempest/openstack/common/fixture/config.py
@@ -0,0 +1,46 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013 Mirantis, Inc.
+# 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 fixtures
+from oslo.config import cfg
+import six
+
+
+class Config(fixtures.Fixture):
+ """Override some configuration values.
+
+ The keyword arguments are the names of configuration options to
+ override and their values.
+
+ If a group argument is supplied, the overrides are applied to
+ the specified configuration option group.
+
+ All overrides are automatically cleared at the end of the current
+ test by the reset() method, which is registred by addCleanup().
+ """
+
+ def __init__(self, conf=cfg.CONF):
+ self.conf = conf
+
+ def setUp(self):
+ super(Config, self).setUp()
+ self.addCleanup(self.conf.reset)
+
+ def config(self, **kw):
+ group = kw.pop('group', None)
+ for k, v in six.iteritems(kw):
+ self.conf.set_override(k, v, group)
diff --git a/tempest/openstack/common/fixture/lockutils.py b/tempest/openstack/common/fixture/lockutils.py
new file mode 100644
index 0000000..21b4a48
--- /dev/null
+++ b/tempest/openstack/common/fixture/lockutils.py
@@ -0,0 +1,53 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 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 fixtures
+
+from tempest.openstack.common.lockutils import lock
+
+
+class LockFixture(fixtures.Fixture):
+ """External locking fixture.
+
+ This fixture is basically an alternative to the synchronized decorator with
+ the external flag so that tearDowns and addCleanups will be included in
+ the lock context for locking between tests. The fixture is recommended to
+ be the first line in a test method, like so::
+
+ def test_method(self):
+ self.useFixture(LockFixture)
+ ...
+
+ or the first line in setUp if all the test methods in the class are
+ required to be serialized. Something like::
+
+ class TestCase(testtools.testcase):
+ def setUp(self):
+ self.useFixture(LockFixture)
+ super(TestCase, self).setUp()
+ ...
+
+ This is because addCleanups are put on a LIFO queue that gets run after the
+ test method exits. (either by completing or raising an exception)
+ """
+ def __init__(self, name, lock_file_prefix=None):
+ self.mgr = lock(name, lock_file_prefix, True)
+
+ def setUp(self):
+ super(LockFixture, self).setUp()
+ self.addCleanup(self.mgr.__exit__, None, None, None)
+ self.mgr.__enter__()
diff --git a/tempest/openstack/common/fixture/mockpatch.py b/tempest/openstack/common/fixture/mockpatch.py
new file mode 100644
index 0000000..cd0d6ca
--- /dev/null
+++ b/tempest/openstack/common/fixture/mockpatch.py
@@ -0,0 +1,51 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# 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 fixtures
+import mock
+
+
+class PatchObject(fixtures.Fixture):
+ """Deal with code around mock."""
+
+ def __init__(self, obj, attr, **kwargs):
+ self.obj = obj
+ self.attr = attr
+ self.kwargs = kwargs
+
+ def setUp(self):
+ super(PatchObject, self).setUp()
+ _p = mock.patch.object(self.obj, self.attr, **self.kwargs)
+ self.mock = _p.start()
+ self.addCleanup(_p.stop)
+
+
+class Patch(fixtures.Fixture):
+
+ """Deal with code around mock.patch."""
+
+ def __init__(self, obj, **kwargs):
+ self.obj = obj
+ self.kwargs = kwargs
+
+ def setUp(self):
+ super(Patch, self).setUp()
+ _p = mock.patch(self.obj, **self.kwargs)
+ self.mock = _p.start()
+ self.addCleanup(_p.stop)
diff --git a/tempest/openstack/common/fixture/moxstubout.py b/tempest/openstack/common/fixture/moxstubout.py
new file mode 100644
index 0000000..a0e74fd
--- /dev/null
+++ b/tempest/openstack/common/fixture/moxstubout.py
@@ -0,0 +1,34 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# 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 fixtures
+import mox
+
+
+class MoxStubout(fixtures.Fixture):
+ """Deal with code around mox and stubout as a fixture."""
+
+ def setUp(self):
+ super(MoxStubout, self).setUp()
+ # emulate some of the mox stuff, we can't use the metaclass
+ # because it screws with our generators
+ self.mox = mox.Mox()
+ self.stubs = self.mox.stubs
+ self.addCleanup(self.mox.UnsetStubs)
+ self.addCleanup(self.mox.VerifyAll)
diff --git a/tempest/scenario/test_server_advanced_ops.py b/tempest/scenario/test_server_advanced_ops.py
index 853b1ba..112c8a2 100644
--- a/tempest/scenario/test_server_advanced_ops.py
+++ b/tempest/scenario/test_server_advanced_ops.py
@@ -35,7 +35,7 @@
def setUpClass(cls):
super(TestServerAdvancedOps, cls).setUpClass()
- if not cls.config.compute.resize_available:
+ if not cls.config.compute_feature_enabled.resize:
msg = "Skipping test - resize not available on this host"
raise cls.skipException(msg)
diff --git a/tempest/services/compute/json/aggregates_client.py b/tempest/services/compute/json/aggregates_client.py
index 75ce9ff..b7c6bf1 100644
--- a/tempest/services/compute/json/aggregates_client.py
+++ b/tempest/services/compute/json/aggregates_client.py
@@ -97,3 +97,14 @@
post_body, self.headers)
body = json.loads(body)
return resp, body['aggregate']
+
+ def set_metadata(self, aggregate_id, meta):
+ """Replaces the aggregate's existing metadata with new metadata."""
+ post_body = {
+ 'metadata': meta,
+ }
+ post_body = json.dumps({'set_metadata': post_body})
+ resp, body = self.post('os-aggregates/%s/action' % aggregate_id,
+ post_body, self.headers)
+ body = json.loads(body)
+ return resp, body['aggregate']
diff --git a/tempest/services/compute/json/flavors_client.py b/tempest/services/compute/json/flavors_client.py
index dc05e3e..305a77b 100644
--- a/tempest/services/compute/json/flavors_client.py
+++ b/tempest/services/compute/json/flavors_client.py
@@ -109,6 +109,14 @@
body = json.loads(body)
return resp, body[key]
+ def update_flavor_extra_spec(self, flavor_id, key, **kwargs):
+ """Gets specified extra Specs details of the mentioned flavor."""
+ resp, body = self.put('flavors/%s/os-extra_specs/%s' %
+ (flavor_id, key),
+ json.dumps(kwargs), self.headers)
+ body = json.loads(body)
+ return resp, body
+
def unset_flavor_extra_spec(self, flavor_id, key):
"""Unsets extra Specs from the mentioned flavor."""
return self.delete('flavors/%s/os-extra_specs/%s' % (str(flavor_id),
diff --git a/tempest/services/compute/json/hosts_client.py b/tempest/services/compute/json/hosts_client.py
index 30a3f7b..f51879d 100644
--- a/tempest/services/compute/json/hosts_client.py
+++ b/tempest/services/compute/json/hosts_client.py
@@ -44,3 +44,39 @@
resp, body = self.get("os-hosts/%s" % str(hostname))
body = json.loads(body)
return resp, body['host']
+
+ def update_host(self, hostname, **kwargs):
+ """Update a host."""
+
+ request_body = {
+ 'status': None,
+ 'maintenance_mode': None,
+ }
+ request_body.update(**kwargs)
+ request_body = json.dumps(request_body)
+
+ resp, body = self.put("os-hosts/%s" % str(hostname), request_body,
+ self.headers)
+ body = json.loads(body)
+ return resp, body
+
+ def startup_host(self, hostname):
+ """Startup a host."""
+
+ resp, body = self.get("os-hosts/%s/startup" % str(hostname))
+ body = json.loads(body)
+ return resp, body['host']
+
+ def shutdown_host(self, hostname):
+ """Shutdown a host."""
+
+ resp, body = self.get("os-hosts/%s/shutdown" % str(hostname))
+ body = json.loads(body)
+ return resp, body['host']
+
+ def reboot_host(self, hostname):
+ """reboot a host."""
+
+ resp, body = self.get("os-hosts/%s/reboot" % str(hostname))
+ body = json.loads(body)
+ return resp, body['host']
diff --git a/tempest/services/compute/xml/aggregates_client.py b/tempest/services/compute/xml/aggregates_client.py
index 8ef0af6..5faaff5 100644
--- a/tempest/services/compute/xml/aggregates_client.py
+++ b/tempest/services/compute/xml/aggregates_client.py
@@ -21,6 +21,7 @@
from tempest import exceptions
from tempest.services.compute.xml.common import Document
from tempest.services.compute.xml.common import Element
+from tempest.services.compute.xml.common import Text
from tempest.services.compute.xml.common import xml_to_json
@@ -112,3 +113,18 @@
self.headers)
aggregate = self._format_aggregate(etree.fromstring(body))
return resp, aggregate
+
+ def set_metadata(self, aggregate_id, meta):
+ """Replaces the aggregate's existing metadata with new metadata."""
+ post_body = Element("set_metadata")
+ metadata = Element("metadata")
+ post_body.append(metadata)
+ for k, v in meta.items():
+ meta = Element(k)
+ meta.append(Text(v))
+ metadata.append(meta)
+ resp, body = self.post('os-aggregates/%s/action' % aggregate_id,
+ str(Document(post_body)),
+ self.headers)
+ aggregate = self._format_aggregate(etree.fromstring(body))
+ return resp, aggregate
diff --git a/tempest/services/compute/xml/flavors_client.py b/tempest/services/compute/xml/flavors_client.py
index c7ed044..12e24d0 100644
--- a/tempest/services/compute/xml/flavors_client.py
+++ b/tempest/services/compute/xml/flavors_client.py
@@ -22,6 +22,7 @@
from tempest.common.rest_client import RestClientXML
from tempest.services.compute.xml.common import Document
from tempest.services.compute.xml.common import Element
+from tempest.services.compute.xml.common import Text
from tempest.services.compute.xml.common import xml_to_json
from tempest.services.compute.xml.common import XMLNS_11
@@ -29,7 +30,7 @@
XMLNS_OS_FLV_EXT_DATA = \
"http://docs.openstack.org/compute/ext/flavor_extra_data/api/v1.1"
XMLNS_OS_FLV_ACCESS = \
- "http://docs.openstack.org/compute/ext/flavor_access/api/v1.1"
+ "http://docs.openstack.org/compute/ext/flavor_access/api/v2"
class FlavorsClientXML(RestClientXML):
@@ -49,6 +50,10 @@
if k == '{%s}ephemeral' % XMLNS_OS_FLV_EXT_DATA:
k = 'OS-FLV-EXT-DATA:ephemeral'
+ if k == '{%s}is_public' % XMLNS_OS_FLV_ACCESS:
+ k = 'os-flavor-access:is_public'
+ v = True if v == 'True' else False
+
if k == 'extra_specs':
k = 'OS-FLV-WITH-EXT-SPECS:extra_specs'
flavor[k] = dict(v)
@@ -155,6 +160,21 @@
body = xml_to_json(etree.fromstring(body))
return resp, body
+ def update_flavor_extra_spec(self, flavor_id, key, **kwargs):
+ """Gets specified extra Specs details of the mentioned flavor."""
+ doc = Document()
+ for (k, v) in kwargs.items():
+ element = Element(k)
+ doc.append(element)
+ value = Text(v)
+ element.append(value)
+
+ resp, body = self.put('flavors/%s/os-extra_specs/%s' %
+ (flavor_id, key),
+ str(doc), self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, {key: body}
+
def unset_flavor_extra_spec(self, flavor_id, key):
"""Unsets an extra spec based on the mentioned flavor and key."""
return self.delete('flavors/%s/os-extra_specs/%s' % (str(flavor_id),
diff --git a/tempest/services/compute/xml/hosts_client.py b/tempest/services/compute/xml/hosts_client.py
index 9743143..f7d7b0a 100644
--- a/tempest/services/compute/xml/hosts_client.py
+++ b/tempest/services/compute/xml/hosts_client.py
@@ -18,6 +18,8 @@
from lxml import etree
from tempest.common.rest_client import RestClientXML
+from tempest.services.compute.xml.common import Document
+from tempest.services.compute.xml.common import Element
from tempest.services.compute.xml.common import xml_to_json
@@ -47,3 +49,46 @@
node = etree.fromstring(body)
body = [xml_to_json(x) for x in node.getchildren()]
return resp, body
+
+ def update_host(self, hostname, status=None, maintenance_mode=None,
+ **kwargs):
+ """Update a host."""
+
+ request_body = Element(status=status,
+ maintenance_mode=maintenance_mode)
+ if kwargs:
+ for k, v in kwargs.iteritem():
+ request_body.add_attr(k, v)
+ resp, body = self.put("os-hosts/%s" % str(hostname),
+ str(Document(request_body)),
+ self.headers)
+ node = etree.fromstring(body)
+ body = [xml_to_json(x) for x in node.getchildren()]
+ return resp, body
+
+ def startup_host(self, hostname):
+ """Startup a host."""
+
+ resp, body = self.get("os-hosts/%s/startup" % str(hostname),
+ self.headers)
+ node = etree.fromstring(body)
+ body = [xml_to_json(x) for x in node.getchildren()]
+ return resp, body
+
+ def shutdown_host(self, hostname):
+ """Shutdown a host."""
+
+ resp, body = self.get("os-hosts/%s/shutdown" % str(hostname),
+ self.headers)
+ node = etree.fromstring(body)
+ body = [xml_to_json(x) for x in node.getchildren()]
+ return resp, body
+
+ def reboot_host(self, hostname):
+ """Reboot a host."""
+
+ resp, body = self.get("os-hosts/%s/reboot" % str(hostname),
+ self.headers)
+ node = etree.fromstring(body)
+ body = [xml_to_json(x) for x in node.getchildren()]
+ return resp, body
diff --git a/tempest/services/image/v1/json/image_client.py b/tempest/services/image/v1/json/image_client.py
index 1921d78..41b9e81 100644
--- a/tempest/services/image/v1/json/image_client.py
+++ b/tempest/services/image/v1/json/image_client.py
@@ -265,9 +265,12 @@
LOG.info('Value transition from "%s" to "%s"'
'in %d second(s).', old_value,
value, dtime)
- if (value == status):
+ if value == status:
return value
+ if value == 'killed':
+ raise exceptions.ImageKilledException(image_id=image_id,
+ status=value)
if dtime > self.build_timeout:
message = ('Time Limit Exceeded! (%ds)'
'while waiting for %s, '
diff --git a/tempest/services/image/v2/json/image_client.py b/tempest/services/image/v2/json/image_client.py
index f0531ec..342a09c 100644
--- a/tempest/services/image/v2/json/image_client.py
+++ b/tempest/services/image/v2/json/image_client.py
@@ -100,7 +100,7 @@
self._validate_schema(body, type='images')
return resp, body['images']
- def get_image_metadata(self, image_id):
+ def get_image(self, image_id):
url = 'v2/images/%s' % image_id
resp, body = self.get(url)
body = json.loads(body)
@@ -108,7 +108,7 @@
def is_resource_deleted(self, id):
try:
- self.get_image_metadata(id)
+ self.get_image(id)
except exceptions.NotFound:
return True
return False
@@ -124,3 +124,13 @@
url = 'v2/images/%s/file' % image_id
resp, body = self.get(url)
return 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, headers=self.headers)
+ return resp, body
+
+ def delete_image_tag(self, image_id, tag):
+ url = 'v2/images/%s/tags/%s' % (image_id, tag)
+ resp, _ = self.delete(url)
+ return resp
diff --git a/tempest/services/network/json/network_client.py b/tempest/services/network/json/network_client.py
index 92c1faf..e7cd33f 100644
--- a/tempest/services/network/json/network_client.py
+++ b/tempest/services/network/json/network_client.py
@@ -123,8 +123,12 @@
resp, body = self.delete(uri, self.headers)
return resp, body
- def list_ports(self):
+ def list_ports(self, **filters):
uri = '%s/ports' % (self.uri_prefix)
+ filter_items = ["%s=%s" % (k, v) for (k, v) in filters.iteritems()]
+ querystring = "&".join(filter_items)
+ if querystring:
+ uri = "%s?%s" % (uri, querystring)
resp, body = self.get(uri, self.headers)
body = json.loads(body)
return resp, body
@@ -223,7 +227,7 @@
body = json.loads(body)
return resp, body
- def update_router(self, router_id, **kwargs):
+ def _update_router(self, router_id, set_enable_snat, **kwargs):
uri = '%s/routers/%s' % (self.uri_prefix, router_id)
resp, body = self.get(uri, self.headers)
body = json.loads(body)
@@ -231,15 +235,34 @@
update_body['name'] = kwargs.get('name', body['router']['name'])
update_body['admin_state_up'] = kwargs.get(
'admin_state_up', body['router']['admin_state_up'])
- # Must uncomment/modify these lines once LP question#233187 is solved
- # update_body['external_gateway_info'] = kwargs.get(
- # 'external_gateway_info', body['router']['external_gateway_info'])
+ cur_gw_info = body['router']['external_gateway_info']
+ if cur_gw_info and 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'])
update_body = dict(router=update_body)
update_body = json.dumps(update_body)
resp, body = self.put(uri, update_body, self.headers)
body = json.loads(body)
return resp, 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 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 = '%s/routers/%s/add_router_interface' % (self.uri_prefix,
router_id)
diff --git a/tempest/services/volume/json/volumes_client.py b/tempest/services/volume/json/volumes_client.py
index 62a6e24..eb87cbe 100644
--- a/tempest/services/volume/json/volumes_client.py
+++ b/tempest/services/volume/json/volumes_client.py
@@ -154,3 +154,34 @@
except exceptions.NotFound:
return True
return False
+
+ 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.headers)
+ return 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.headers)
+ return resp, body
+
+ def volume_begin_detaching(self, volume_id):
+ """Volume Begin Detaching."""
+ post_body = json.dumps({'os-begin_detaching': {}})
+ resp, body = self.post('volumes/%s/action' % volume_id, post_body,
+ self.headers)
+ return resp, body
+
+ def volume_roll_detaching(self, volume_id):
+ """Volume Roll Detaching."""
+ post_body = json.dumps({'os-roll_detaching': {}})
+ resp, body = self.post('volumes/%s/action' % volume_id, post_body,
+ self.headers)
+ return resp, body
diff --git a/tempest/services/volume/xml/volumes_client.py b/tempest/services/volume/xml/volumes_client.py
index b59ec03..be292a2 100644
--- a/tempest/services/volume/xml/volumes_client.py
+++ b/tempest/services/volume/xml/volumes_client.py
@@ -227,3 +227,42 @@
resp, body = self.post(url, str(Document(post_body)), self.headers)
volume = xml_to_json(etree.fromstring(body))
return resp, volume
+
+ def extend_volume(self, volume_id, extend_size):
+ """Extend a volume."""
+ post_body = Element("os-extend",
+ new_size=extend_size)
+ url = 'volumes/%s/action' % str(volume_id)
+ resp, body = self.post(url, str(Document(post_body)), self.headers)
+ if body:
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body
+
+ def reset_volume_status(self, volume_id, status):
+ """Reset the Specified Volume's Status."""
+ post_body = Element("os-reset_status",
+ status=status
+ )
+ url = 'volumes/%s/action' % str(volume_id)
+ resp, body = self.post(url, str(Document(post_body)), self.headers)
+ if body:
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body
+
+ def volume_begin_detaching(self, volume_id):
+ """Volume Begin Detaching."""
+ post_body = Element("os-begin_detaching")
+ url = 'volumes/%s/action' % str(volume_id)
+ resp, body = self.post(url, str(Document(post_body)), self.headers)
+ if body:
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body
+
+ def volume_roll_detaching(self, volume_id):
+ """Volume Roll Detaching."""
+ post_body = Element("os-roll_detaching")
+ url = 'volumes/%s/action' % str(volume_id)
+ resp, body = self.post(url, str(Document(post_body)), self.headers)
+ if body:
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body
diff --git a/tempest/test.py b/tempest/test.py
index 8ce7af8..6ae7925 100644
--- a/tempest/test.py
+++ b/tempest/test.py
@@ -26,6 +26,7 @@
import testtools
from tempest import clients
+from tempest.common import isolated_creds
from tempest import config
from tempest import exceptions
from tempest.openstack.common import log as logging
@@ -164,7 +165,11 @@
if at_exit_set:
raise RuntimeError("tearDownClass does not calls the super's "
"tearDownClass in these classes: "
- + str(at_exit_set))
+ + str(at_exit_set) + "\n"
+ "If you see the exception, with another "
+ "exception please do not report this one!"
+ "If you are changing tempest code, make sure you",
+ "are calling the super class's tearDownClass!")
atexit.register(validate_tearDownClass)
@@ -216,7 +221,36 @@
os.environ.get('OS_LOG_CAPTURE') != '0'):
log_format = '%(asctime)-15s %(message)s'
self.useFixture(fixtures.LoggerFixture(nuke_handlers=False,
- format=log_format))
+ format=log_format,
+ level=None))
+
+ @classmethod
+ def get_client_manager(cls):
+ """
+ Returns an Openstack client manager
+ """
+ cls.isolated_creds = isolated_creds.IsolatedCreds(cls.__name__)
+
+ force_tenant_isolation = getattr(cls, 'force_tenant_isolation', None)
+ if (cls.config.compute.allow_tenant_isolation or
+ force_tenant_isolation):
+ creds = cls.isolated_creds.get_primary_creds()
+ username, tenant_name, password = creds
+ os = clients.Manager(username=username,
+ password=password,
+ tenant_name=tenant_name,
+ interface=cls._interface)
+ else:
+ os = clients.Manager(interface=cls._interface)
+ return os
+
+ @classmethod
+ def clear_isolated_creds(cls):
+ """
+ Clears isolated creds if set
+ """
+ if getattr(cls, 'isolated_creds'):
+ cls.isolated_creds.clear_isolated_creds()
@classmethod
def _get_identity_admin_client(cls):
diff --git a/tempest/tests/base.py b/tempest/tests/base.py
new file mode 100644
index 0000000..12c1c25
--- /dev/null
+++ b/tempest/tests/base.py
@@ -0,0 +1,55 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# 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 fixtures
+import mox
+import stubout
+import testtools
+
+
+class MoxStubout(fixtures.Fixture):
+ """Deal with code around mox and stubout as a fixture."""
+
+ def setUp(self):
+ super(MoxStubout, self).setUp()
+ # emulate some of the mox stuff, we can't use the metaclass
+ # because it screws with our generators
+ self.mox = mox.Mox()
+ self.stubs = stubout.StubOutForTesting()
+ self.addCleanup(self.stubs.UnsetAll)
+ self.addCleanup(self.stubs.SmartUnsetAll)
+ self.addCleanup(self.mox.UnsetStubs)
+ self.addCleanup(self.mox.VerifyAll)
+
+
+class TestCase(testtools.TestCase):
+
+ def setUp(self):
+ super(TestCase, self).setUp()
+ 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))
+
+ mox_fixture = self.useFixture(MoxStubout())
+ self.mox = mox_fixture.mox
+ self.stubs = mox_fixture.stubs
diff --git a/tempest/tests/test_list_tests.py b/tempest/tests/test_list_tests.py
new file mode 100644
index 0000000..ab0d114
--- /dev/null
+++ b/tempest/tests/test_list_tests.py
@@ -0,0 +1,38 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# 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 re
+import subprocess
+
+from tempest.tests import base
+
+
+class TestTestList(base.TestCase):
+
+ def test_no_import_errors(self):
+ import_failures = []
+ p = subprocess.Popen(['testr', 'list-tests'], stdout=subprocess.PIPE)
+ ids = p.stdout.read()
+ ids = ids.split('\n')
+ for test_id in ids:
+ if re.match('(\w+\.){3}\w+', test_id):
+ if not test_id.startswith('tempest.'):
+ fail_id = test_id.split('unittest.loader.ModuleImport'
+ 'Failure.')[1]
+ import_failures.append(fail_id)
+ error_message = ("The following tests have import failures and aren't"
+ " being run with test filters %s" % import_failures)
+ self.assertFalse(import_failures, error_message)
diff --git a/tempest/tests/test_wrappers.py b/tempest/tests/test_wrappers.py
index dbe3420..dbf1809 100644
--- a/tempest/tests/test_wrappers.py
+++ b/tempest/tests/test_wrappers.py
@@ -18,12 +18,13 @@
import shutil
import subprocess
import tempfile
-import testtools
+
+from tempest.tests import base
DEVNULL = open(os.devnull, 'wb')
-class TestWrappers(testtools.TestCase):
+class TestWrappers(base.TestCase):
def setUp(self):
super(TestWrappers, self).setUp()
# Setup test dirs
diff --git a/tempest/thirdparty/boto/test.py b/tempest/thirdparty/boto/test.py
index 5295e44..5ae21c8 100644
--- a/tempest/thirdparty/boto/test.py
+++ b/tempest/thirdparty/boto/test.py
@@ -193,11 +193,10 @@
class BotoTestCase(tempest.test.BaseTestCase):
"""Recommended to use as base class for boto related test."""
- conclusion = decision_maker()
-
@classmethod
def setUpClass(cls):
super(BotoTestCase, cls).setUpClass()
+ cls.conclusion = decision_maker()
# The trash contains cleanup functions and paramaters in tuples
# (function, *args, **kwargs)
cls._resource_trash_bin = {}
diff --git a/test-requirements.txt b/test-requirements.txt
index 8aa6ed9..fbe7e43 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -4,3 +4,4 @@
sphinx>=1.1.2
python-subunit
oslo.sphinx
+mox>=0.5.3
diff --git a/tools/skip_tracker.py b/tools/skip_tracker.py
index ffaf134..0ae3323 100755
--- a/tools/skip_tracker.py
+++ b/tools/skip_tracker.py
@@ -46,14 +46,24 @@
test methods that have been decorated to skip because of
a particular bug.
"""
- results = []
+ 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)
- results += find_skips_in_file(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
@@ -83,11 +93,19 @@
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
+
+
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 results]))
+ unique_bugs = sorted(set([bug for (method, bug) in get_results(results)]))
unskips = []
duplicates = []
info("Total bug skips found: %d", len(results))
@@ -122,4 +140,7 @@
print("should be removed from the test cases:")
print()
for bug in unskips:
- print(" %7s" % bug)
+ message = " %7s in " % bug
+ locations = ["%s" % x for x in results[bug].keys()]
+ message += " and ".join(locations)
+ print(message)
diff --git a/tools/verify_tempest_config.py b/tools/verify_tempest_config.py
new file mode 100755
index 0000000..1b5fe68
--- /dev/null
+++ b/tools/verify_tempest_config.py
@@ -0,0 +1,77 @@
+#!/usr/bin/env python
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# 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 sys
+
+from tempest import clients
+from tempest import config
+
+
+CONF = config.TempestConfig()
+
+#Dicts matching extension names to config options
+NOVA_EXTENSIONS = {
+ 'disk_config': 'DiskConfig',
+ 'change_password': 'ServerPassword',
+ 'flavor_extra': 'FlavorExtraSpecs'
+}
+
+
+def verify_glance_api_versions(os):
+ # Check glance api versions
+ __, versions = os.image_client.get_versions()
+ if CONF.image_feature_enabled.api_v1 != ('v1.1' in versions or 'v1.0' in
+ versions):
+ print 'Config option image api_v1 should be change to: %s' % (
+ not CONF.image_feature_enabled.api_v1)
+ if CONF.image_feature_enabled.api_v2 != ('v2.0' in versions):
+ print 'Config option image api_v2 should be change to: %s' % (
+ not CONF.image_feature_enabled.api_v2)
+
+
+def verify_extensions(os):
+ results = {}
+ extensions_client = os.extensions_client
+ __, resp = extensions_client.list_extensions()
+ resp = resp['extensions']
+ extensions = map(lambda x: x['name'], resp)
+ results['nova_features'] = {}
+ for extension in NOVA_EXTENSIONS.keys():
+ if NOVA_EXTENSIONS[extension] in extensions:
+ results['nova_features'][extension] = True
+ else:
+ results['nova_features'][extension] = False
+ return results
+
+
+def display_results(results):
+ for option in NOVA_EXTENSIONS.keys():
+ config_value = getattr(CONF.compute_feature_enabled, option)
+ if config_value != results['nova_features'][option]:
+ print "Config option: %s should be changed to: %s" % (
+ option, not config_value)
+
+
+def main(argv):
+ os = clients.ComputeAdminManager(interface='json')
+ results = verify_extensions(os)
+ verify_glance_api_versions(os)
+ display_results(results)
+
+
+if __name__ == "__main__":
+ main(sys.argv)
diff --git a/tox.ini b/tox.ini
index d93112c..a3c781b 100644
--- a/tox.ini
+++ b/tox.ini
@@ -81,7 +81,7 @@
# https://bugs.launchpad.net/tempest/+bug/1216076 so the neutron smoke
# job would fail if we moved it to parallel.
commands =
- sh tools/pretty_tox_serial.sh 'smoke {posargs}'
+ sh tools/pretty_tox_serial.sh '(?!.*\[.*\bslow\b.*\])((smoke)|(^tempest\.scenario)) {posargs}'
[testenv:coverage]
sitepackages = True