Merge "Adding negative test to check limits of Security Groups and rules"
diff --git a/.gitignore b/.gitignore
index c154603..0f4880f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
ChangeLog
*.pyc
etc/tempest.conf
+etc/logging.conf
include/swift_objects/swift_small
include/swift_objects/swift_medium
include/swift_objects/swift_large
@@ -13,3 +14,4 @@
.venv
dist
build
+.testrepository
diff --git a/README.rst b/README.rst
index 04a4b3e..1f44832 100644
--- a/README.rst
+++ b/README.rst
@@ -33,11 +33,6 @@
devstack uploaded and set the image_ref value in the [environment]
section in the tempest.conf to that image UUID.
- In addition, the ``<devstack-repo>/tools/configure_tempest.sh`` script can
- also be used to generate a tempest.conf based on your devstack's rc files.
- TEMPEST_DIR variable points to location /opt/stack/temptest. Update this
- variable if the location is different..
-
Tempest is not tied to any single test runner, but Nose been the most commonly
used tool. After setting up your configuration file, you can execute
the set of Tempest tests by using ``nosetests`` ::
diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample
index ac18490..9e93759 100644
--- a/etc/tempest.conf.sample
+++ b/etc/tempest.conf.sample
@@ -79,6 +79,9 @@
# Name of a user used to authenticated to an instance
ssh_user = cirros
+# Visible fixed network name
+fixed_network_name = private
+
# Network id used for SSH (public, private, etc)
network_for_ssh = private
@@ -164,6 +167,9 @@
# 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
+
[network]
# This section contains configuration options used when executing tests
# against the OpenStack Network API.
@@ -210,6 +216,12 @@
# 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 backend declared in cinder.conf)
+# They must have different volume_backend_name (backend1_name and backend2_name
+# have to be different)
+multi_backend_enabled = false
+backend1_name = LVM_iSCSI
+backend2_name = LVM_iSCSI_1
[object-storage]
# This section contains configuration options used when executing tests
@@ -273,3 +285,26 @@
# Status change wait interval
build_interval = 1
+
+[orchestration]
+# Status change wait interval
+build_interval = 1
+
+# Status change wait timout. This may vary across environments as some some
+# tests spawn full VMs, which could be slow if the test is already in a VM.
+build_timeout = 300
+
+# Whether or not Heat is expected to be available
+heat_available = false
+
+# Instance type for tests. Needs to be big enough for a
+# full OS plus the test workload
+instance_type = m1.tiny
+
+# Name of heat-cfntools enabled image to use when launching test instances
+# If not specified, tests that spawn instances will not run
+#image_ref = ubuntu-vm-heat-cfntools
+
+# Name of existing keypair to launch servers with. The default is not to specify
+# any key, which will generate a keypair for each test class
+#keypair_name = heat_key
diff --git a/openstack-common.conf b/openstack-common.conf
index 501328c..24af119 100644
--- a/openstack-common.conf
+++ b/openstack-common.conf
@@ -1,7 +1,7 @@
[DEFAULT]
# The list of modules to copy from openstack-common
-modules=setup,install_venv_common
+modules=install_venv_common
# The base module to hold the copy of openstack.common
base=tempest
diff --git a/run_tests.sh b/run_tests.sh
index 6fcdd90..56a6e6e 100755
--- a/run_tests.sh
+++ b/run_tests.sh
@@ -109,7 +109,7 @@
function run_pep8 {
echo "Running pep8 ..."
- ${wrapper} tools/check_source.sh
+ ${wrapper} flake8
}
function run_coverage_start {
diff --git a/setup.cfg b/setup.cfg
index b522f3a..59050cd 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,3 +1,30 @@
+[metadata]
+name = tempest
+version = 2013.2
+summary = OpenStack Integration Testing
+description-file =
+ README.rst
+author = OpenStack QA
+author-email = openstack-qa@lists.openstack.org
+home-page = http://www.openstack.org/
+classifier =
+ Intended Audience :: Information Technology
+ Intended Audience :: System Administrators
+ Intended Audience :: Developers
+ License :: OSI Approved :: Apache Software License
+ Operating System :: POSIX :: Linux
+ Programming Language :: Python
+ Programming Language :: Python :: 2
+ Programming Language :: Python :: 2.7
+
+[global]
+setup-hooks =
+ pbr.hooks.setup_hook
+
+[files]
+scripts =
+ bin/tempest
+
[nosetests]
# NOTE(jkoelker) To run the test suite under nose install the following
# coverage http://pypi.python.org/pypi/coverage
diff --git a/setup.py b/setup.py
index 1507797..59a0090 100755
--- a/setup.py
+++ b/setup.py
@@ -1,51 +1,21 @@
#!/usr/bin/env python
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright 2010 United States Government as represented by the
-# Administrator of the National Aeronautics and Space Administration.
-# All Rights Reserved.
+# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
#
-# http://www.apache.org/licenses/LICENSE-2.0
+# http://www.apache.org/licenses/LICENSE-2.0
#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 setuptools
-from tempest.openstack.common import setup as common_setup
-
-requires = common_setup.parse_requirements()
-depend_links = common_setup.parse_dependency_links()
-
-setuptools.setup(name='tempest',
- version=common_setup.get_version('tempest', "2013.2"),
- description='Integration test tools',
- author='OpenStack',
- author_email='openstack-qa@lists.launchpad.net',
- url='http://www.openstack.org/',
- classifiers=['Environment :: OpenStack',
- 'Intended Audience :: Information Technology',
- 'Intended Audience :: System Administrators',
- 'Intended Audience :: Developers',
- 'License :: OSI Approved :'
- ': Apache Software License',
- 'Operating System :: POSIX :: Linux',
- 'Programming Language :: Python',
- 'Programming Language :: Python :: 2',
- 'Programming Language :: Python :: 2.7', ],
- cmdclass=common_setup.get_cmdclass(),
- packages=setuptools.find_packages(exclude=['bin']),
- install_requires=requires,
- dependency_links=depend_links,
- include_package_data=True,
- test_suite='nose.collector',
- setup_requires=['setuptools_git>=0.4'],
- scripts=['bin/tempest'],
- py_modules=[])
+setuptools.setup(
+ setup_requires=['d2to1', 'pbr'],
+ d2to1=True)
diff --git a/stress/README.rst b/stress/README.rst
deleted file mode 100644
index d935289..0000000
--- a/stress/README.rst
+++ /dev/null
@@ -1,68 +0,0 @@
-Quanta Research Cambridge OpenStack Stress Test System
-======================================================
-
-Nova is a distributed, asynchronous system that is prone to race condition
-bugs. These bugs will not be easily found during
-functional testing but will be encountered by users in large deployments in a
-way that is hard to debug. The stress test tries to cause these bugs to happen
-in a more controlled environment.
-
-The basic idea of the test is that there are a number of actions, roughly
-corresponding to the Compute API, that are fired pseudo-randomly at a nova
-cluster as fast as possible. These actions consist of what to do, how to
-verify success, and a state filter to make sure that the operation makes sense.
-For example, if the action is to reboot a server and none are active, nothing
-should be done. A test case is a set of actions to be performed and the
-probability that each action should be selected. There are also parameters
-controlling rate of fire and stuff like that.
-
-This test framework is designed to stress test a Nova cluster. Hence,
-you must have a working Nova cluster with rate limiting turned off.
-
-Environment
-------------
-This particular framework assumes your working Nova cluster understands Nova
-API 2.0. The stress tests can read the logs from the cluster. To enable this
-you have to provide the hostname to call 'nova-manage' and
-the private key and user name for ssh to the cluster in the
-[stress] section of tempest.conf. You also need to provide the
-value of --logdir in nova.conf:
-
- host_private_key_path=<path to private ssh key>
- host_admin_user=<name of user for ssh command>
- nova_logdir=<value of --logdir in nova.conf>
- controller=<hostname for calling nova-manage>
- max_instances=<limit on instances that will be created>
-
-Also, make sure to set
-
-log_level=CRITICAL
-
-so that the API client does not log failed calls which are expected while
-running stress tests.
-
-The stress test needs the top-level tempest directory to be on PYTHONPATH
-if you are not using nosetests to run.
-
-
-Running the sample test
------------------------
-
-To test your installation, do the following (from the tempest directory):
-
- PYTHONPATH=. python stress/tests/user_script_sample.py
-
-This sample test tries to create a few VMs and kill a few VMs.
-
-
-Additional Tools
-----------------
-
-Sometimes the tests don't finish, or there are failures. In these
-cases, you may want to clean out the nova cluster. We have provided
-some scripts to do this in the ``tools`` subdirectory. To use these
-tools, you will need to install python-novaclient.
-You can then use the following script to destroy any keypairs,
-floating ips, and servers::
-
-stress/tools/nova_destroy_all.py
diff --git a/stress/basher.py b/stress/basher.py
deleted file mode 100644
index e34738f..0000000
--- a/stress/basher.py
+++ /dev/null
@@ -1,41 +0,0 @@
-# Copyright 2011 Quanta Research Cambridge, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""Class to describe actions to be included in a stress test."""
-
-
-class BasherAction(object):
- """
- Used to describe each action that you would like to include in a test run.
- """
-
- def __init__(self, test_case, probability, pargs=[], kargs={}):
- """
- `test_case` : the name of the class that implements the action
- `pargs` : positional arguments to the constructor of `test_case`
- `kargs` : keyword arguments to the constructor of `test_case`
- `probability`: frequency that each action
- """
- self.test_case = test_case
- self.pargs = pargs
- self.kargs = kargs
- self.probability = probability
-
- def invoke(self, manager, state):
- """
- Calls the `run` method of the `test_case`.
- """
- return self.test_case.run(manager, state, *self.pargs, **self.kargs)
-
- def __str__(self):
- return self.test_case.__class__.__name__
diff --git a/stress/config.py b/stress/config.py
deleted file mode 100755
index 25cb910..0000000
--- a/stress/config.py
+++ /dev/null
@@ -1,45 +0,0 @@
-# Copyright 2011 Quanta Research Cambridge, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-
-class StressConfig(object):
- """Provides configuration information for whitebox stress tests."""
-
- def __init__(self, conf):
- self.conf = conf
-
- @property
- def host_private_key_path(self):
- """Path to ssh key for logging into compute nodes."""
- return self.conf.compute.path_to_private_key
-
- @property
- def host_admin_user(self):
- """Username for logging into compute nodes."""
- return self.conf.compute.ssh_user
-
- @property
- def nova_logdir(self):
- """Directory containing log files on the compute nodes."""
- return self.conf.stress.nova_logdir
-
- @property
- def controller(self):
- """Controller host."""
- return self.conf.stress.controller
-
- @property
- def max_instances(self):
- """Maximum number of instances to create during test."""
- return self.conf.stress.max_instances
diff --git a/stress/driver.py b/stress/driver.py
deleted file mode 100644
index f80e765..0000000
--- a/stress/driver.py
+++ /dev/null
@@ -1,274 +0,0 @@
-# Copyright 2011 Quanta Research Cambridge, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""The entry point for the execution of a workloadTo execute a workload.
-Users pass in a description of the workload and a nova manager object
-to the bash_openstack function call"""
-
-import datetime
-import random
-import time
-from urlparse import urlparse
-
-from config import StressConfig
-from state import ClusterState
-from state import FloatingIpState
-from state import KeyPairState
-from state import VolumeState
-import stress.utils
-from test_case import logging
-
-from tempest.common.utils.data_utils import rand_name
-
-# setup logging to file
-logging.basicConfig(
- format='%(asctime)s %(name)-20s %(levelname)-8s %(message)s',
- datefmt='%m-%d %H:%M:%S',
- filename="stress.debug.log",
- filemode="w",
- level=logging.DEBUG,
-)
-
-# define a Handler which writes INFO messages or higher to the sys.stdout
-_console = logging.StreamHandler()
-_console.setLevel(logging.INFO)
-# set a format which is simpler for console use
-_formatter = logging.Formatter('%(name)-20s: %(levelname)-8s %(message)s')
-# tell the handler to use this format
-_console.setFormatter(_formatter)
-# add the handler to the root logger
-logging.getLogger('').addHandler(_console)
-
-
-def _create_cases(choice_spec):
- """
- Generate a workload of tests from workload description
- """
- cases = []
- count = 0
- for choice in choice_spec:
- p = choice.probability
- for i in range(p):
- cases.append(choice)
- i = i + p
- count = count + p
- assert(count == 100)
- return cases
-
-
-def _get_compute_nodes(keypath, user, controller):
- """
- Returns a list of active compute nodes. List is generated by running
- nova-manage on the controller.
- """
- nodes = []
- if keypath is None or user is None:
- return nodes
- cmd = "nova-manage service list | grep ^nova-compute"
- lines = stress.utils.ssh(keypath, user, controller, cmd).split('\n')
- # For example: nova-compute xg11eth0 nova enabled :-) 2011-10-31 18:57:46
- # This is fragile but there is, at present, no other way to get this info.
- for line in lines:
- words = line.split()
- if len(words) > 0 and words[4] == ":-)":
- nodes.append(words[1])
- return nodes
-
-
-def _error_in_logs(keypath, logdir, user, nodes):
- """
- Detect errors in the nova log files on the controller and compute nodes.
- """
- grep = 'egrep "ERROR\|TRACE" %s/*.log' % logdir
- for node in nodes:
- errors = stress.utils.ssh(keypath, user, node, grep, check=False)
- if len(errors) > 0:
- logging.error('%s: %s' % (node, errors))
- return True
- return False
-
-
-def create_initial_vms(manager, state, count):
- image = manager.config.compute.image_ref
- flavor = manager.config.compute.flavor_ref
- servers = []
- logging.info('Creating %d vms' % count)
- for _ in xrange(count):
- name = rand_name('initial_vm-')
- _, server = manager.servers_client.create_server(name, image, flavor)
- servers.append(server)
- for server in servers:
- manager.servers_client.wait_for_server_status(server['id'], 'ACTIVE')
- logging.info('Server Name: %s Id: %s' % (name, server['id']))
- state.set_instance_state(server['id'], (server, 'ACTIVE'))
-
-
-def create_initial_floating_ips(manager, state, count):
- logging.info('Creating %d floating ips' % count)
- for _ in xrange(count):
- _, ip = manager.floating_ips_client.create_floating_ip()
- logging.info('Ip: %s' % ip['ip'])
- state.add_floating_ip(FloatingIpState(ip))
-
-
-def create_initial_keypairs(manager, state, count):
- logging.info('Creating %d keypairs' % count)
- for _ in xrange(count):
- name = rand_name('keypair-')
- _, keypair = manager.keypairs_client.create_keypair(name)
- logging.info('Keypair: %s' % name)
- state.add_keypair(KeyPairState(keypair))
-
-
-def create_initial_volumes(manager, state, count):
- volumes = []
- logging.info('Creating %d volumes' % count)
- for _ in xrange(count):
- name = rand_name('volume-')
- _, volume = manager.volumes_client.create_volume(size=1,
- display_name=name)
- volumes.append(volume)
- for volume in volumes:
- manager.volumes_client.wait_for_volume_status(volume['id'],
- 'available')
- logging.info('Volume Name: %s Id: %s' % (name, volume['id']))
- state.add_volume(VolumeState(volume))
-
-
-def bash_openstack(manager,
- choice_spec,
- **kwargs):
- """
- Workload driver. Executes a workload as specified by the `choice_spec`
- parameter against a nova-cluster.
-
- `manager` : Manager object
- `choice_spec` : list of BasherChoice actions to run on the cluster
- `kargs` : keyword arguments to the constructor of `test_case`
- `duration` = how long this test should last (3 sec)
- `sleep_time` = time to sleep between actions (in msec)
- `test_name` = human readable workload description
- (default: unnamed test)
- `max_vms` = maximum number of instances to launch
- (default: 32)
- `seed` = random seed (default: None)
- """
- stress_config = StressConfig(manager.config)
- # get keyword arguments
- duration = kwargs.get('duration', datetime.timedelta(seconds=10))
- seed = kwargs.get('seed', None)
- sleep_time = float(kwargs.get('sleep_time', 3000)) / 1000
- max_vms = int(kwargs.get('max_vms', stress_config.max_instances))
- test_name = kwargs.get('test_name', 'unamed test')
-
- keypath = stress_config.host_private_key_path
- user = stress_config.host_admin_user
- logdir = stress_config.nova_logdir
- host = urlparse(manager.config.identity.uri).hostname
- computes = _get_compute_nodes(keypath, user, host)
- stress.utils.execute_on_all(keypath, user, computes,
- "rm -f %s/*.log" % logdir)
- random.seed(seed)
- cases = _create_cases(choice_spec)
- state = ClusterState(max_vms=max_vms)
- create_initial_keypairs(manager, state,
- int(kwargs.get('initial_keypairs', 0)))
- create_initial_vms(manager, state,
- int(kwargs.get('initial_vms', 0)))
- create_initial_floating_ips(manager, state,
- int(kwargs.get('initial_floating_ips', 0)))
- create_initial_volumes(manager, state,
- int(kwargs.get('initial_volumes', 0)))
- test_end_time = time.time() + duration.seconds
-
- retry_list = []
- last_retry = time.time()
- cooldown = False
- logcheck_count = 0
- test_succeeded = True
- logging.debug('=== Test \"%s\" on %s ===' %
- (test_name, time.asctime(time.localtime())))
- for kw in kwargs:
- logging.debug('\t%s = %s', kw, kwargs[kw])
-
- while True:
- if not cooldown:
- if time.time() < test_end_time:
- case = random.choice(cases)
- logging.debug('Chose %s' % case)
- retry = case.invoke(manager, state)
- if retry is not None:
- retry_list.append(retry)
- else:
- logging.info('Cooling down...')
- cooldown = True
- if cooldown and len(retry_list) == 0:
- if _error_in_logs(keypath, logdir, user, computes):
- test_succeeded = False
- break
- # Retry verifications every 5 seconds.
- if time.time() - last_retry > 5:
- logging.debug('retry verifications for %d tasks', len(retry_list))
- new_retry_list = []
- for v in retry_list:
- v.check_timeout()
- if not v.retry():
- new_retry_list.append(v)
- retry_list = new_retry_list
- last_retry = time.time()
- time.sleep(sleep_time)
- # Check error logs after 100 actions
- if logcheck_count > 100:
- if _error_in_logs(keypath, logdir, user, computes):
- test_succeeded = False
- break
- else:
- logcheck_count = 0
- else:
- logcheck_count = logcheck_count + 1
- # Cleanup
- logging.info('Cleaning up: terminating virtual machines...')
- vms = state.get_instances()
- active_vms = [v for _k, v in vms.iteritems()
- if v and v[1] != 'TERMINATING']
- for target in active_vms:
- manager.servers_client.delete_server(target[0]['id'])
- # check to see that the server was actually killed
- for target in active_vms:
- kill_id = target[0]['id']
- i = 0
- while True:
- try:
- manager.servers_client.get_server(kill_id)
- except Exception:
- break
- i += 1
- if i > 60:
- _error_in_logs(keypath, logdir, user, computes)
- raise Exception("Cleanup timed out")
- time.sleep(1)
- logging.info('killed %s' % kill_id)
- state.delete_instance_state(kill_id)
- for floating_ip_state in state.get_floating_ips():
- manager.floating_ips_client.delete_floating_ip(
- floating_ip_state.resource_id)
- for keypair_state in state.get_keypairs():
- manager.keypairs_client.delete_keypair(keypair_state.name)
- for volume_state in state.get_volumes():
- manager.volumes_client.delete_volume(volume_state.resource_id)
-
- if test_succeeded:
- logging.info('*** Test succeeded ***')
- else:
- logging.info('*** Test had errors ***')
- return test_succeeded
diff --git a/stress/pending_action.py b/stress/pending_action.py
deleted file mode 100644
index abfa74d..0000000
--- a/stress/pending_action.py
+++ /dev/null
@@ -1,91 +0,0 @@
-# Copyright 2011 Quanta Research Cambridge, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""Describe follow-up actions using `PendingAction` class to verify
-that nova API calls such as create/delete are completed"""
-
-import logging
-import time
-
-from tempest.exceptions import TimeoutException
-
-
-class PendingAction(object):
- """
- Initialize and describe actions to verify that a Nova API call
- is successful.
- """
-
- def __init__(self, nova_manager, timeout=None):
- """
- `nova_manager` : Manager object.
- `timeout` : time before we declare a TimeoutException
- """
- if timeout is None:
- timeout = nova_manager.config.compute.build_timeout
- self._manager = nova_manager
- self._logger = logging.getLogger(self.__class__.__name__)
- self._start_time = time.time()
- self._timeout = timeout
-
- def retry(self):
- """
- Invoked by user of this class to verify completion of
- previous TestCase actions
- """
- return False
-
- def check_timeout(self):
- """Check for timeouts of TestCase actions."""
- time_diff = time.time() - self._start_time
- if time_diff > self._timeout:
- self._logger.error('%s exceeded timeout of %d' %
- (self.__class__.__name__, self._timeout))
- raise TimeoutException
-
- def elapsed(self):
- return time.time() - self._start_time
-
-
-class PendingServerAction(PendingAction):
- """
- Initialize and describe actions to verify that a Nova API call that
- changes server state is successful.
- """
-
- def __init__(self, nova_manager, state, target_server, timeout=None):
- """
- `state` : externally maintained data structure about
- state of VMs or other persistent objects in
- the nova cluster
- `target_server` : server that actions were performed on
- """
- super(PendingServerAction, self).__init__(nova_manager,
- timeout=timeout)
- self._state = state
- self._target = target_server
-
- def _check_for_status(self, state_string):
- """Check to see if the machine has transitioned states."""
- t = time.time() # for debugging
- target = self._target
- _resp, body = self._manager.servers_client.get_server(target['id'])
- if body['status'] != state_string:
- # grab the actual state as we think it is
- temp_obj = self._state.get_instances()[target['id']]
- self._logger.debug("machine %s in state %s" %
- (target['id'], temp_obj[1]))
- self._logger.debug('%s, time: %d' % (temp_obj[1], time.time() - t))
- return temp_obj[1]
- self._logger.debug('%s, time: %d' % (state_string, time.time() - t))
- return state_string
diff --git a/stress/state.py b/stress/state.py
deleted file mode 100644
index 9c31b76..0000000
--- a/stress/state.py
+++ /dev/null
@@ -1,117 +0,0 @@
-# Copyright 2011 Quanta Research Cambridge, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-
-class ClusterState(object):
- """A class to store the state of various persistent objects in the Nova
- cluster, e.g. instances, volumes. Use methods to query to state which than
- can be compared to the current state of the objects in Nova.
- """
-
- def __init__(self, **kwargs):
- self._max_vms = kwargs.get('max_vms', 32)
- self._instances = {}
- self._floating_ips = []
- self._keypairs = []
- self._volumes = []
-
- # instance state methods
- def get_instances(self):
- """return the instances dictionary that we believe are in cluster."""
- return self._instances
-
- def get_max_instances(self):
- """return the maximum number of instances we can create."""
- return self._max_vms
-
- def set_instance_state(self, key, val):
- """Store `val` in the dictionary indexed at `key`."""
- self._instances[key] = val
-
- def delete_instance_state(self, key):
- """Delete state indexed at `key`."""
- del self._instances[key]
-
- #floating_ip state methods
- def get_floating_ips(self):
- """return the floating ips list for the cluster."""
- return self._floating_ips
-
- def add_floating_ip(self, floating_ip_state):
- """Add floating ip."""
- self._floating_ips.append(floating_ip_state)
-
- def remove_floating_ip(self, floating_ip_state):
- """Remove floating ip."""
- self._floating_ips.remove(floating_ip_state)
-
- # keypair methods
- def get_keypairs(self):
- """return the keypairs list for the cluster."""
- return self._keypairs
-
- def add_keypair(self, keypair_state):
- """Add keypair."""
- self._keypairs.append(keypair_state)
-
- def remove_keypair(self, keypair_state):
- """Remove keypair."""
- self._keypairs.remove(keypair_state)
-
- # volume methods
- def get_volumes(self):
- """return the volumes list for the cluster."""
- return self._volumes
-
- def add_volume(self, volume_state):
- """Add volume."""
- self._volumes.append(volume_state)
-
- def remove_volume(self, volume_state):
- """Remove volume."""
- self._volumes.remove(volume_state)
-
-
-class ServerAssociatedState(object):
- """Class that tracks resources that are associated with a particular server
- such as a volume or floating ip.
- """
-
- def __init__(self, resource_id):
- # The id of the server.
- self.server_id = None
- # The id of the resource that is attached to the server.
- self.resource_id = resource_id
- # True if in the process of attaching/detaching the resource.
- self.change_pending = False
-
-
-class FloatingIpState(ServerAssociatedState):
-
- def __init__(self, ip_desc):
- super(FloatingIpState, self).__init__(ip_desc['id'])
- self.address = ip_desc['ip']
-
-
-class VolumeState(ServerAssociatedState):
-
- def __init__(self, volume_desc):
- super(VolumeState, self).__init__(volume_desc['id'])
-
-
-class KeyPairState(object):
-
- def __init__(self, keypair_spec):
- self.name = keypair_spec['name']
- self.private_key = keypair_spec['private_key']
diff --git a/stress/test_case.py b/stress/test_case.py
deleted file mode 100644
index d04ace0..0000000
--- a/stress/test_case.py
+++ /dev/null
@@ -1,29 +0,0 @@
-# Copyright 2011 Quanta Research Cambridge, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""Abstract class for implementing an action. You only need to override
-the `run` method which specifies all the actual nova API class you wish
-to make."""
-
-
-import logging
-
-
-class StressTestCase(object):
-
- def __init__(self):
- self._logger = logging.getLogger(self.__class__.__name__)
-
- def run(self, nova_manager, state_obj, *pargs, **kargs):
- """Nova API methods to call that would modify state of the cluster."""
- return
diff --git a/stress/test_floating_ips.py b/stress/test_floating_ips.py
deleted file mode 100755
index 6774e81..0000000
--- a/stress/test_floating_ips.py
+++ /dev/null
@@ -1,88 +0,0 @@
-# Copyright 2011 Quanta Research Cambridge, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import random
-import telnetlib
-
-import pending_action
-import test_case
-
-
-class TestChangeFloatingIp(test_case.StressTestCase):
- """Add or remove a floating ip from a vm."""
-
- def __init__(self):
- super(TestChangeFloatingIp, self).__init__()
- self.server_ids = None
-
- def run(self, manager, state, *pargs, **kwargs):
- if self.server_ids is None:
- vms = state.get_instances()
- self.server_ids = [k for k, v in vms.iteritems()]
- floating_ip = random.choice(state.get_floating_ips())
- if floating_ip.change_pending:
- return None
- floating_ip.change_pending = True
- timeout = int(kwargs.get('timeout', 60))
- cli = manager.floating_ips_client
- if floating_ip.server_id is None:
- server = random.choice(self.server_ids)
- address = floating_ip.address
- self._logger.info('Adding %s to server %s' % (address, server))
- resp, body = cli.associate_floating_ip_to_server(address,
- server)
- if resp.status != 202:
- raise Exception("response: %s body: %s" % (resp, body))
- floating_ip.server_id = server
- return VerifyChangeFloatingIp(manager, floating_ip,
- timeout, add=True)
- else:
- server = floating_ip.server_id
- address = floating_ip.address
- self._logger.info('Removing %s from server %s' % (address, server))
- resp, body = cli.disassociate_floating_ip_from_server(address,
- server)
- if resp.status != 202:
- raise Exception("response: %s body: %s" % (resp, body))
- return VerifyChangeFloatingIp(manager, floating_ip,
- timeout, add=False)
-
-
-class VerifyChangeFloatingIp(pending_action.PendingAction):
- """Verify that floating ip was changed."""
- def __init__(self, manager, floating_ip, timeout, add=None):
- super(VerifyChangeFloatingIp, self).__init__(manager, timeout=timeout)
- self.floating_ip = floating_ip
- self.add = add
-
- def retry(self):
- """
- Check to see that we can contact the server at its new address.
- """
- try:
- conn = telnetlib.Telnet(self.floating_ip.address, 22, timeout=0.5)
- conn.close()
- if self.add:
- self._logger.info('%s added [%.1f secs elapsed]' %
- (self.floating_ip.address, self.elapsed()))
- self.floating_ip.change_pending = False
- return True
- except Exception:
- if not self.add:
- self._logger.info('%s removed [%.1f secs elapsed]' %
- (self.floating_ip.address, self.elapsed()))
- self.floating_ip.change_pending = False
- self.floating_ip.server_id = None
- return True
- return False
diff --git a/stress/test_server_actions.py b/stress/test_server_actions.py
deleted file mode 100644
index f4ddf23..0000000
--- a/stress/test_server_actions.py
+++ /dev/null
@@ -1,275 +0,0 @@
-# Copyright 2011 Quanta Research Cambridge, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""Defines various sub-classes of the `StressTestCase` and
-`PendingServerAction` class. Sub-classes of StressTestCase implement various
-API calls on the Nova cluster having to do with Server Actions. Each
-sub-class will have a corresponding PendingServerAction. These pending
-actions veriy that the API call was successful or not."""
-
-import random
-
-import pending_action
-import stress.utils
-from tempest.exceptions import Duplicate
-import test_case
-
-
-class TestRebootVM(test_case.StressTestCase):
- """Reboot a server."""
-
- def run(self, manager, state, *pargs, **kwargs):
- """
- Send an HTTP POST request to the nova cluster to reboot a random
- server. Update state of object in `state` variable to indicate that
- it is rebooting.
- `manager` : Manager object
- `state` : `State` object describing our view of state of cluster
- `pargs` : positional arguments
- `kwargs` : keyword arguments, which include:
- `timeout` : how long to wait before issuing Exception
- `type` : reboot type [SOFT or HARD] (default is SOFT)
- """
-
- vms = state.get_instances()
- active_vms = [v for k, v in vms.iteritems() if v and v[1] == 'ACTIVE']
- # no active vms, so return null
- if not active_vms:
- self._logger.info('no ACTIVE instances to reboot')
- return
-
- _reboot_arg = kwargs.get('type', 'SOFT')
-
- # select active vm to reboot and then send request to nova controller
- target = random.choice(active_vms)
- reboot_target = target[0]
- # It seems that doing a reboot when in reboot is an error.
- try:
- response, body = manager.servers_client.reboot(reboot_target['id'],
- _reboot_arg)
- except Duplicate:
- return
-
- if (response.status != 202):
- self._logger.error("response: %s" % response)
- raise Exception
-
- if _reboot_arg == 'SOFT':
- reboot_state = 'REBOOT'
- else:
- reboot_state = 'HARD_REBOOT'
-
- self._logger.info('waiting for machine %s to change to %s' %
- (reboot_target['id'], reboot_state))
-
- return VerifyRebootVM(manager,
- state,
- reboot_target,
- reboot_state=reboot_state)
-
-
-class VerifyRebootVM(pending_action.PendingServerAction):
- """Class to verify that the reboot completed."""
- States = stress.utils.enum('REBOOT_CHECK', 'ACTIVE_CHECK')
-
- def __init__(self, manager, state, target_server,
- reboot_state=None,
- ip_addr=None):
- super(VerifyRebootVM, self).__init__(manager,
- state,
- target_server)
- self._reboot_state = reboot_state
- self._retry_state = self.States.REBOOT_CHECK
-
- def retry(self):
- """
- Check to see that the server of interest has actually rebooted. Update
- state to indicate that server is running again.
- """
- # don't run reboot verification if target machine has been
- # deleted or is going to be deleted
- target_id = self._target['id']
- if (self._target['id'] not in self._state.get_instances().keys() or
- self._state.get_instances()[target_id][1] == 'TERMINATING'):
- self._logger.debug('machine %s is deleted or TERMINATING' %
- self._target['id'])
- return True
-
- reboot_state = self._reboot_state
- if self._retry_state == self.States.REBOOT_CHECK:
- server_state = self._check_for_status(reboot_state)
- if server_state == reboot_state:
- self._logger.info('machine %s ACTIVE -> %s' %
- (self._target['id'], reboot_state))
- self._state.set_instance_state(self._target['id'],
- (self._target, reboot_state))
- self._retry_state = self.States.ACTIVE_CHECK
- elif server_state == 'ACTIVE':
- # machine must have gone ACTIVE -> REBOOT ->ACTIVE
- self._retry_state = self.States.ACTIVE_CHECK
-
- elif self._retry_state == self.States.ACTIVE_CHECK:
- if not self._check_for_status('ACTIVE'):
- return False
- target = self._target
- self._logger.info('machine %s %s -> ACTIVE [%.1f secs elapsed]' %
- (target['id'], reboot_state, self.elapsed()))
- self._state.set_instance_state(target['id'],
- (target, 'ACTIVE'))
-
- return True
-
-# This code needs to be tested against a cluster that supports resize.
-#class TestResizeVM(test_case.StressTestCase):
-# """Resize a server (change flavors)."""
-#
-# def run(self, manager, state, *pargs, **kwargs):
-# """
-# Send an HTTP POST request to the nova cluster to resize a random
-# server. Update `state` to indicate server is rebooting.
-#
-# `manager` : Manager object.
-# `state` : `State` object describing our view of state of cluster
-# `pargs` : positional arguments
-# `kwargs` : keyword arguments, which include:
-# `timeout` : how long to wait before issuing Exception
-# """
-#
-# vms = state.get_instances()
-# active_vms = [v for k, v in vms.iteritems() if v and v[1] == 'ACTIVE']
-# # no active vms, so return null
-# if not active_vms:
-# self._logger.debug('no ACTIVE instances to resize')
-# return
-#
-# target = random.choice(active_vms)
-# resize_target = target[0]
-# print resize_target
-#
-# _timeout = kwargs.get('timeout', 600)
-#
-# # determine current flavor type, and resize to a different type
-# # m1.tiny -> m1.small, m1.small -> m1.tiny
-# curr_size = int(resize_target['flavor']['id'])
-# if curr_size == 1:
-# new_size = 2
-# else:
-# new_size = 1
-# flavor_type = { 'flavorRef': new_size } # resize to m1.small
-#
-# post_body = json.dumps({'resize' : flavor_type})
-# url = '/servers/%s/action' % resize_target['id']
-# (response, body) = manager.request('POST',
-# url,
-# body=post_body)
-#
-# if (response.status != 202):
-# self._logger.error("response: %s" % response)
-# raise Exception
-#
-# state_name = check_for_status(manager, resize_target, 'RESIZE')
-#
-# if state_name == 'RESIZE':
-# self._logger.info('machine %s: ACTIVE -> RESIZE' %
-# resize_target['id'])
-# state.set_instance_state(resize_target['id'],
-# (resize_target, 'RESIZE'))
-#
-# return VerifyResizeVM(manager,
-# state,
-# resize_target,
-# state_name=state_name,
-# timeout=_timeout)
-#
-#class VerifyResizeVM(pending_action.PendingServerAction):
-# """Verify that resizing of a VM was successful."""
-# States = enum('VERIFY_RESIZE_CHECK', 'ACTIVE_CHECK')
-#
-# def __init__(self, manager, state, created_server,
-# state_name=None,
-# timeout=300):
-# super(VerifyResizeVM, self).__init__(manager,
-# state,
-# created_server,
-# timeout=timeout)
-# self._retry_state = self.States.VERIFY_RESIZE_CHECK
-# self._state_name = state_name
-#
-# def retry(self):
-# """
-# Check to see that the server was actually resized. And change `state`
-# of server to running again.
-# """
-# # don't run resize if target machine has been deleted
-# # or is going to be deleted
-# if (self._target['id'] not in self._state.get_instances().keys() or
-# self._state.get_instances()[self._target['id']][1] ==
-# 'TERMINATING'):
-# self._logger.debug('machine %s is deleted or TERMINATING' %
-# self._target['id'])
-# return True
-#
-# if self._retry_state == self.States.VERIFY_RESIZE_CHECK:
-# if self._check_for_status('VERIFY_RESIZE') == 'VERIFY_RESIZE':
-# # now issue command to CONFIRM RESIZE
-# post_body = json.dumps({'confirmResize' : null})
-# url = '/servers/%s/action' % self._target['id']
-# (response, body) = manager.request('POST',
-# url,
-# body=post_body)
-# if (response.status != 204):
-# self._logger.error("response: %s" % response)
-# raise Exception
-#
-# self._logger.info(
-# 'CONFIRMING RESIZE of machine %s [%.1f secs elapsed]' %
-# (self._target['id'], self.elapsed())
-# )
-# state.set_instance_state(self._target['id'],
-# (self._target, 'CONFIRM_RESIZE'))
-#
-# # change states
-# self._retry_state = self.States.ACTIVE_CHECK
-#
-# return False
-#
-# elif self._retry_state == self.States.ACTIVE_CHECK:
-# if not self._check_manager("ACTIVE"):
-# return False
-# else:
-# server = self._manager.get_server(self._target['id'])
-#
-# # Find private IP of server?
-# try:
-# (_, network) = server['addresses'].popitem()
-# ip = network[0]['addr']
-# except KeyError:
-# self._logger.error(
-# 'could not get ip address for machine %s' %
-# self._target['id']
-# )
-# raise Exception
-#
-# self._logger.info(
-# 'machine %s: VERIFY_RESIZE -> ACTIVE [%.1f sec elapsed]' %
-# (self._target['id'], self.elapsed())
-# )
-# self._state.set_instance_state(self._target['id'],
-# (self._target, 'ACTIVE'))
-#
-# return True
-#
-# else:
-# # should never get here
-# self._logger.error('Unexpected state')
-# raise Exception
diff --git a/stress/test_servers.py b/stress/test_servers.py
deleted file mode 100644
index 25cbbb0..0000000
--- a/stress/test_servers.py
+++ /dev/null
@@ -1,318 +0,0 @@
-# Copyright 2011 Quanta Research Cambridge, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""Defines various sub-classes of the `StressTestCase` and
-`PendingServerAction` class. Sub-classes of StressTestCase implement various
-API calls on the Nova cluster having to do with creating and deleting VMs.
-Each sub-class will have a corresponding PendingServerAction. These pending
-actions veriy that the API call was successful or not."""
-
-import random
-
-import pending_action
-import test_case
-
-
-class TestCreateVM(test_case.StressTestCase):
- """Create a virtual machine in the Nova cluster."""
- _vm_id = 0
-
- def run(self, manager, state, *pargs, **kwargs):
- """
- Send an HTTP POST request to the nova cluster to build a
- server. Update the state variable to track state of new server
- and set to PENDING state.
-
- `manager` : Manager object.
- `state` : `State` object describing our view of state of cluster
- `pargs` : positional arguments
- `kwargs` : keyword arguments, which include:
- `key_name` : name of keypair
- `image_ref` : index to image types availablexs
- `flavor_ref`: index to flavor types available
- (default = 1, which is tiny)
- """
-
- # restrict number of instances we can launch
- if len(state.get_instances()) >= state.get_max_instances():
- self._logger.debug("maximum number of instances created: %d" %
- state.get_max_instances())
- return None
-
- _key_name = kwargs.get('key_name', '')
- _image_ref = kwargs.get('image_ref', manager.config.compute.image_ref)
- _flavor_ref = kwargs.get('flavor_ref',
- manager.config.compute.flavor_ref)
-
- expected_server = {
- 'name': 'server' + str(TestCreateVM._vm_id),
- 'metadata': {
- 'key1': 'value1',
- 'key2': 'value2',
- },
- 'imageRef': _image_ref,
- 'flavorRef': _flavor_ref,
- 'adminPass': 'testpwd',
- 'key_name': _key_name,
- }
- TestCreateVM._vm_id = TestCreateVM._vm_id + 1
- create_server = manager.servers_client.create_server
- response, body = create_server(expected_server['name'],
- _image_ref,
- _flavor_ref,
- meta=expected_server['metadata'],
- adminPass=expected_server['adminPass'])
-
- if (response.status != 202):
- self._logger.error("response: %s" % response)
- self._logger.error("body: %s" % body)
- raise Exception
-
- created_server = body
-
- self._logger.info('setting machine %s to BUILD' %
- created_server['id'])
- state.set_instance_state(created_server['id'],
- (created_server, 'BUILD'))
-
- return VerifyCreateVM(manager,
- state,
- created_server,
- expected_server)
-
-
-class VerifyCreateVM(pending_action.PendingServerAction):
- """Verify that VM was built and is running."""
- def __init__(self, manager,
- state,
- created_server,
- expected_server):
- super(VerifyCreateVM, self).__init__(manager,
- state,
- created_server,
- )
- self._expected = expected_server
-
- def retry(self):
- """
- Check to see that the server was created and is running.
- Update local view of state to indicate that it is running.
- """
- # don't run create verification
- # if target machine has been deleted or is going to be deleted
- target_id = self._target['id']
- if (self._target['id'] not in self._state.get_instances().keys() or
- self._state.get_instances()[target_id][1] == 'TERMINATING'):
- self._logger.info('machine %s is deleted or TERMINATING' %
- self._target['id'])
- return True
-
- admin_pass = self._target['adminPass']
- # Could check more things here.
- if (self._expected['adminPass'] != admin_pass):
- self._logger.error('expected: %s' %
- (self._expected['adminPass']))
- self._logger.error('returned: %s' %
- (admin_pass))
- raise Exception
-
- if self._check_for_status('ACTIVE') != 'ACTIVE':
- return False
-
- self._logger.info('machine %s: BUILD -> ACTIVE [%.1f secs elapsed]' %
- (self._target['id'], self.elapsed()))
- self._state.set_instance_state(self._target['id'],
- (self._target, 'ACTIVE'))
- return True
-
-
-class TestKillActiveVM(test_case.StressTestCase):
- """Class to destroy a random ACTIVE server."""
- def run(self, manager, state, *pargs, **kwargs):
- """
- Send an HTTP POST request to the nova cluster to destroy
- a random ACTIVE server. Update `state` to indicate TERMINATING.
-
- `manager` : Manager object.
- `state` : `State` object describing our view of state of cluster
- `pargs` : positional arguments
- `kwargs` : keyword arguments, which include:
- `timeout` : how long to wait before issuing Exception
- """
- # check for active instances
- vms = state.get_instances()
- active_vms = [v for k, v in vms.iteritems() if v and v[1] == 'ACTIVE']
- # no active vms, so return null
- if not active_vms:
- self._logger.info('no ACTIVE instances to delete')
- return
-
- _timeout = kwargs.get('timeout', manager.config.compute.build_timeout)
-
- target = random.choice(active_vms)
- killtarget = target[0]
- manager.servers_client.delete_server(killtarget['id'])
- self._logger.info('machine %s: ACTIVE -> TERMINATING' %
- killtarget['id'])
- state.set_instance_state(killtarget['id'],
- (killtarget, 'TERMINATING'))
- return VerifyKillActiveVM(manager, state,
- killtarget, timeout=_timeout)
-
-
-class VerifyKillActiveVM(pending_action.PendingServerAction):
- """Verify that server was destroyed."""
-
- def retry(self):
- """
- Check to see that the server of interest is destroyed. Update
- state to indicate that server is destroyed by deleting it from local
- view of state.
- """
- tid = self._target['id']
- # if target machine has been deleted from the state, then it was
- # already verified to be deleted
- if (not tid in self._state.get_instances().keys()):
- return False
-
- try:
- self._manager.servers_client.get_server(tid)
- except Exception:
- # if we get a 404 response, is the machine really gone?
- target = self._target
- self._logger.info('machine %s: DELETED [%.1f secs elapsed]' %
- (target['id'], self.elapsed()))
- self._state.delete_instance_state(target['id'])
- return True
-
- return False
-
-
-class TestKillAnyVM(test_case.StressTestCase):
- """Class to destroy a random server regardless of state."""
-
- def run(self, manager, state, *pargs, **kwargs):
- """
- Send an HTTP POST request to the nova cluster to destroy
- a random server. Update state to TERMINATING.
-
- `manager` : Manager object.
- `state` : `State` object describing our view of state of cluster
- `pargs` : positional arguments
- `kwargs` : keyword arguments, which include:
- `timeout` : how long to wait before issuing Exception
- """
-
- vms = state.get_instances()
- # no vms, so return null
- if not vms:
- self._logger.info('no active instances to delete')
- return
-
- _timeout = kwargs.get('timeout', manager.config.compute.build_timeout)
-
- target = random.choice(vms)
- killtarget = target[0]
-
- manager.servers_client.delete_server(killtarget['id'])
- self._state.set_instance_state(killtarget['id'],
- (killtarget, 'TERMINATING'))
- # verify object will do the same thing as the active VM
- return VerifyKillAnyVM(manager, state, killtarget, timeout=_timeout)
-
-VerifyKillAnyVM = VerifyKillActiveVM
-
-
-class TestUpdateVMName(test_case.StressTestCase):
- """Class to change the name of the active server."""
- def run(self, manager, state, *pargs, **kwargs):
- """
- Issue HTTP POST request to change the name of active server.
- Update state of server to reflect name changing.
-
- `manager` : Manager object.
- `state` : `State` object describing our view of state of cluster
- `pargs` : positional arguments
- `kwargs` : keyword arguments, which include:
- `timeout` : how long to wait before issuing Exception
- """
-
- # select one machine from active ones
- vms = state.get_instances()
- active_vms = [v for k, v in vms.iteritems() if v and v[1] == 'ACTIVE']
- # no active vms, so return null
- if not active_vms:
- self._logger.info('no active instances to update')
- return
-
- _timeout = kwargs.get('timeout', manager.config.compute.build_timeout)
-
- target = random.choice(active_vms)
- update_target = target[0]
-
- # Update name by appending '_updated' to the name
- new_name = update_target['name'] + '_updated'
- (response, body) = \
- manager.servers_client.update_server(update_target['id'],
- name=new_name)
- if (response.status != 200):
- self._logger.error("response: %s " % response)
- self._logger.error("body: %s " % body)
- raise Exception
-
- assert(new_name == body['name'])
-
- self._logger.info('machine %s: ACTIVE -> UPDATING_NAME' %
- body['id'])
- state.set_instance_state(body['id'],
- (body, 'UPDATING_NAME'))
-
- return VerifyUpdateVMName(manager,
- state,
- body,
- timeout=_timeout)
-
-
-class VerifyUpdateVMName(pending_action.PendingServerAction):
- """Check that VM has new name."""
- def retry(self):
- """
- Check that VM has new name. Update local view of `state` to RUNNING.
- """
- # don't run update verification
- # if target machine has been deleted or is going to be deleted
- target_id = self._target['id']
- if (not self._target['id'] in self._state.get_instances().keys() or
- self._state.get_instances()[target_id][1] == 'TERMINATING'):
- return False
-
- response, body = \
- self._manager.serverse_client.get_server(self._target['id'])
- if (response.status != 200):
- self._logger.error("response: %s " % response)
- self._logger.error("body: %s " % body)
- raise Exception
-
- if self._target['name'] != body['name']:
- self._logger.error(self._target['name'] +
- ' vs. ' +
- body['name'])
- raise Exception
-
- # log the update
- self._logger.info('machine %s: UPDATING_NAME -> ACTIVE' %
- self._target['id'])
- self._state.set_instance_state(self._target['id'],
- (body,
- 'ACTIVE'))
- return True
diff --git a/stress/tests/create_kill.py b/stress/tests/create_kill.py
deleted file mode 100644
index 30ddfd7..0000000
--- a/stress/tests/create_kill.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# Copyright 2011 Quanta Research Cambridge, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""More aggressive test that creates and destroys VMs with shorter
-sleep times"""
-
-import datetime
-import time
-
-from stress.basher import BasherAction
-from stress.driver import bash_openstack
-from stress.test_servers import TestCreateVM
-from stress.test_servers import TestKillActiveVM
-from tempest import clients
-
-choice_spec = [
- BasherAction(TestCreateVM(), 50),
- BasherAction(TestKillActiveVM(), 50)
-]
-
-nova = clients.Manager()
-
-bash_openstack(nova,
- choice_spec,
- duration=datetime.timedelta(seconds=180),
- sleep_time=100, # in milliseconds
- seed=int(time.time()),
- test_name="create and delete",
- )
diff --git a/stress/tests/floating_ips.py b/stress/tests/floating_ips.py
deleted file mode 100755
index b1b3778..0000000
--- a/stress/tests/floating_ips.py
+++ /dev/null
@@ -1,35 +0,0 @@
-# Copyright 2011 Quanta Research Cambridge, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""Stress test that associates/disasssociates floating ips."""
-
-import datetime
-
-from stress.basher import BasherAction
-from stress.driver import bash_openstack
-from stress.test_floating_ips import TestChangeFloatingIp
-from tempest import clients
-
-
-choice_spec = [
- BasherAction(TestChangeFloatingIp(), 100)
-]
-
-nova = clients.Manager()
-
-bash_openstack(nova,
- choice_spec,
- duration=datetime.timedelta(seconds=300),
- test_name="floating_ips",
- initial_floating_ips=8,
- initial_vms=8)
diff --git a/stress/tests/hard_reboots.py b/stress/tests/hard_reboots.py
deleted file mode 100644
index 50a2e91..0000000
--- a/stress/tests/hard_reboots.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# Copyright 2011 Quanta Research Cambridge, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""Test that reboots random instances in a Nova cluster."""
-
-import datetime
-import time
-
-from stress.basher import BasherAction
-from stress.driver import bash_openstack
-from stress.test_server_actions import TestRebootVM
-from stress.test_servers import TestCreateVM
-from tempest import clients
-
-choice_spec = [
- BasherAction(TestCreateVM(), 50),
- BasherAction(TestRebootVM(), 50,
- kargs={'type': 'HARD'})
-]
-
-nova = clients.Manager()
-
-bash_openstack(nova,
- choice_spec,
- duration=datetime.timedelta(seconds=180),
- sleep_time=500, # in milliseconds
- seed=int(time.time()),
- test_name="hard reboots",
- )
diff --git a/stress/tests/user_script_sample.py b/stress/tests/user_script_sample.py
deleted file mode 100644
index d941ea0..0000000
--- a/stress/tests/user_script_sample.py
+++ /dev/null
@@ -1,40 +0,0 @@
-# Copyright 2011 Quanta Research Cambridge, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""Sample stress test that creates a few virtual machines and then
-destroys them"""
-
-import datetime
-
-from stress.basher import BasherAction
-from stress.driver import bash_openstack
-from stress.test_servers import TestCreateVM
-from stress.test_servers import TestKillActiveVM
-from tempest import clients
-
-choice_spec = [
- BasherAction(TestCreateVM(), 50,
- kargs={'timeout': '60'}),
- BasherAction(TestKillActiveVM(), 50)
-]
-
-
-nova = clients.Manager()
-
-bash_openstack(nova,
- choice_spec,
- duration=datetime.timedelta(seconds=10),
- sleep_time=1000, # in milliseconds
- seed=None,
- test_name="simple create and delete",
- max_vms=4)
diff --git a/stress/tools/nova_destroy_all.py b/stress/tools/nova_destroy_all.py
deleted file mode 100755
index 00d8883..0000000
--- a/stress/tools/nova_destroy_all.py
+++ /dev/null
@@ -1,58 +0,0 @@
-#!/usr/bin/env python
-
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright 2011 Quanta Research Cambridge, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-from novaclient.v1_1 import client
-import tempest.config
-
-# get the environment variables for credentials
-identity = tempest.config.TempestConfig().identity
-
-nt = client.Client(identity.username, identity.password,
- identity.tenant_name, identity.uri)
-
-flavor_list = nt.flavors.list()
-server_list = nt.servers.list()
-images_list = nt.images.list()
-keypairs_list = nt.keypairs.list()
-floating_ips_list = nt.floating_ips.list()
-volumes_list = nt.volumes.list()
-
-print "total servers: %3d, total flavors: %3d, total images: %3d," % \
- (len(server_list),
- len(flavor_list),
- len(images_list)),
-
-print "total keypairs: %3d, total floating ips: %3d" % \
- (len(keypairs_list),
- len(floating_ips_list))
-
-print "deleting all servers"
-for s in server_list:
- s.delete()
-
-print "deleting all keypairs"
-for s in keypairs_list:
- s.delete()
-
-print "deleting all floating_ips"
-for s in floating_ips_list:
- s.delete()
-
-print "deleting all volumes"
-for s in volumes_list:
- s.delete()
diff --git a/stress/tools/nova_status.py b/stress/tools/nova_status.py
deleted file mode 100755
index ee20282..0000000
--- a/stress/tools/nova_status.py
+++ /dev/null
@@ -1,49 +0,0 @@
-#!/usr/bin/env python
-
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright 2011 Quanta Research Cambridge, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-from novaclient.v1_1 import client
-import tempest.config
-
-# get the environment variables for credentials
-identity = tempest.config.TempestConfig().identity
-print identity.username, identity.password,\
- identity.tenant_name, identity.uri
-
-nt = client.Client(identity.username, identity.password,
- identity.tenant_name, identity.uri)
-
-flavor_list = nt.flavors.list()
-server_list = nt.servers.list()
-images_list = nt.images.list()
-keypairs_list = nt.keypairs.list()
-floating_ips_list = nt.floating_ips.list()
-
-print "total servers: %3d, total flavors: %3d, total images: %3d" % \
- (len(server_list),
- len(flavor_list),
- len(images_list))
-
-print "total keypairs: %3d, total floating ips: %3d" % \
- (len(keypairs_list),
- len(floating_ips_list))
-
-print "flavors:\t", flavor_list
-print "servers:\t", server_list
-print "images: \t", images_list
-print "keypairs:\t", keypairs_list
-print "floating ips:\t", floating_ips_list
diff --git a/stress/utils.py b/stress/utils.py
deleted file mode 100644
index ec63b99..0000000
--- a/stress/utils.py
+++ /dev/null
@@ -1,55 +0,0 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright 2011 Quanta Research Cambridge, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import shlex
-import subprocess
-
-SSH_OPTIONS = (" -q" +
- " -o UserKnownHostsFile=/dev/null" +
- " -o StrictHostKeyChecking=no -i ")
-
-
-def get_ssh_options(keypath):
- return SSH_OPTIONS + keypath
-
-
-def scp(keypath, args):
- options = get_ssh_options(keypath)
- return subprocess.check_call(shlex.split("scp" + options + args))
-
-
-def ssh(keypath, user, node, command, check=True):
- command = 'sudo ' + command
- command = "ssh %s %s@%s %s" % (get_ssh_options(keypath), user,
- node, command)
- popenargs = shlex.split(command)
- process = subprocess.Popen(popenargs, stdout=subprocess.PIPE)
- output, unused_err = process.communicate()
- retcode = process.poll()
- if retcode and check:
- raise Exception("%s: ssh failed with retcode: %s" % (node, retcode))
- return output
-
-
-def execute_on_all(keypath, user, nodes, command):
- for node in nodes:
- ssh(keypath, user, node, command)
-
-
-def enum(*sequential, **named):
- """Create auto-incremented enumerated types."""
- enums = dict(zip(sequential, range(len(sequential))), **named)
- return type('Enum', (), enums)
diff --git a/tempest/README.rst b/tempest/README.rst
new file mode 100644
index 0000000..c41ef96
--- /dev/null
+++ b/tempest/README.rst
@@ -0,0 +1,98 @@
+Tempest Field Guide
+-----------
+
+Tempest is designed to be useful for a large number of different
+environments. This includes being useful for gating commits to
+OpenStack core projects, being used to validate OpenStack cloud
+implementations for both correctness, as well as a burn in tool for
+OpenStack clouds.
+
+As such Tempest tests come in many flavors, each with their own rules
+and guidelines. Below is the proposed Havana restructuring for Tempest
+to make this clear.
+
+tempest/
+ 3rdparty/ - 3rd party api tests
+ api/ - API tests
+ cli/ - CLI tests
+ scenario/ - complex scenario tests
+ stress/ - stress tests
+ whitebox/ - white box testing
+
+Each of these directories contains different types of tests. What
+belongs in each directory, the rules and examples for good tests, are
+documented in a README.rst file in the directory.
+
+
+3rdparty
+------------
+
+Many openstack components include 3rdparty API support. It is
+completely legitmate for Tempest to include tests of 3rdparty APIs,
+but those should be kept seperate from the normal OpenStack
+validation.
+
+TODO: tempest/tests/boto should become tempest/3rdparty/boto
+
+
+api
+------------
+
+API tests are validation tests for the OpenStack API. They should not
+use the existing python clients for OpenStack, but should instead use
+the tempest implementations of clients. This allows us to test both
+XML and JSON. Having raw clients also lets us pass invalid JSON and
+XML to the APIs and see the results, something we could not get with
+the native clients.
+
+When it makes sense, API testing should be moved closer to the
+projects themselves, possibly as functional tests in their unit test
+frameworks.
+
+TODO: The bulk of tempest/tests should move to tempest/api
+
+
+cli
+------------
+
+CLI tests use the openstack CLI to interact with the OpenStack
+cloud. CLI testing in unit tests is somewhat difficult because unlike
+server testing, there is no access to server code to
+instantiate. Tempest seems like a logical place for this, as it
+prereqs having a running OpenStack cloud.
+
+TODO: the top level cli directory moves to tempest/cli
+
+
+scenario
+------------
+
+Scenario tests are complex "through path" tests for OpenStack
+functionality. They are typically a series of steps where complicated
+state requiring multiple services is set up exercised, and torn down.
+
+Scenario tests can and should use the OpenStack python clients.
+
+TODO: tests/network/test_network_basic_ops.py,
+tests/compute/servers/*_ops.py should move to tempest/scenario (others)
+
+
+stress
+-----------
+
+Stress tests are designed to stress an OpenStack environment by
+running a high workload against it and seeing what breaks. Tools may
+be provided to help detect breaks (stack traces in the logs).
+
+TODO: old stress tests deleted, new_stress that david is working on
+moves into here.
+
+
+whitebox
+----------
+
+Whitebox tests are tests which require access to the database of the
+target OpenStack machine to verify internal state after opperations
+are made. White box tests are allowed to use the python clients.
+
+TODO: collect out whitebox tests to this location.
diff --git a/tempest/cli/README.rst b/tempest/cli/README.rst
new file mode 100644
index 0000000..4742d4a
--- /dev/null
+++ b/tempest/cli/README.rst
@@ -0,0 +1,48 @@
+Tempest Guide to CLI tests
+========
+
+
+What are these tests?
+---------
+The cli tests test the various OpenStack command line interface tools
+to ensure that they minimally function. The current scope is read only
+operations on a cloud that are hard to test via unit tests.
+
+
+Why are these tests in tempest?
+---------
+These tests exist here because it is extremely difficult to build a
+functional enough environment in the python-*client unit tests to
+provide this kind of testing. Because we already put up a cloud in the
+gate with devstack + tempest it was decided it was better to have
+these as a side tree in tempest instead of another QA effort which
+would split review time.
+
+
+Scope of these tests
+---------
+This should stay limited to the scope of testing the cli. Functional
+testing of the cloud should be elsewhere, this is about exercising the
+cli code.
+
+
+Example of a good test
+---------
+Tests should be isolated to a single command in one of the python
+clients.
+
+Tests should not modify the cloud.
+
+If a test is validating the cli for bad data, it should do it with
+assertRaises.
+
+A reasonable example of an existing test is as follows:
+
+ def test_admin_list(self):
+ self.nova('list')
+ self.nova('list', params='--all-tenants 1')
+ self.nova('list', params='--all-tenants 0')
+ self.assertRaises(subprocess.CalledProcessError,
+ self.nova,
+ 'list',
+ params='--all-tenants bad')
diff --git a/cli/__init__.py b/tempest/cli/__init__.py
similarity index 81%
rename from cli/__init__.py
rename to tempest/cli/__init__.py
index 7a92260..413990d 100644
--- a/cli/__init__.py
+++ b/tempest/cli/__init__.py
@@ -21,7 +21,7 @@
from oslo.config import cfg
-import cli.output_parser
+import tempest.cli.output_parser
import tempest.test
@@ -52,7 +52,7 @@
super(ClientTestBase, cls).setUpClass()
def __init__(self, *args, **kwargs):
- self.parser = cli.output_parser
+ self.parser = tempest.cli.output_parser
super(ClientTestBase, self).__init__(*args, **kwargs)
def nova(self, action, flags='', params='', admin=True, fail_ok=False):
@@ -87,6 +87,15 @@
flags = creds + ' ' + flags
return self.cmd(cmd, action, flags, params, fail_ok)
+ def check_output(self, cmd, **kwargs):
+ # substitutes subprocess.check_output which is not in python2.6
+ kwargs['stdout'] = subprocess.PIPE
+ proc = subprocess.Popen(cmd, **kwargs)
+ output = proc.communicate()[0]
+ if proc.returncode != 0:
+ raise CommandFailed(proc.returncode, cmd, output)
+ return output
+
def cmd(self, cmd, action, flags='', params='', fail_ok=False,
merge_stderr=False):
"""Executes specified command for the given action."""
@@ -96,10 +105,10 @@
cmd = shlex.split(cmd)
try:
if merge_stderr:
- result = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
+ result = self.check_output(cmd, stderr=subprocess.STDOUT)
else:
- devnull = open('/dev/null', 'w')
- result = subprocess.check_output(cmd, stderr=devnull)
+ with open('/dev/null', 'w') as devnull:
+ result = self.check_output(cmd, stderr=devnull)
except subprocess.CalledProcessError, e:
LOG.error("command output:\n%s" % e.output)
raise
@@ -110,3 +119,10 @@
for item in items:
for field in field_names:
self.assertIn(field, item)
+
+
+class CommandFailed(subprocess.CalledProcessError):
+ # adds output attribute for python2.6
+ def __init__(self, returncode, cmd, output):
+ super(CommandFailed, self).__init__(returncode, cmd)
+ self.output = output
diff --git a/cli/output_parser.py b/tempest/cli/output_parser.py
similarity index 100%
rename from cli/output_parser.py
rename to tempest/cli/output_parser.py
diff --git a/cli/simple_read_only/README.txt b/tempest/cli/simple_read_only/README.txt
similarity index 100%
rename from cli/simple_read_only/README.txt
rename to tempest/cli/simple_read_only/README.txt
diff --git a/cli/simple_read_only/__init__.py b/tempest/cli/simple_read_only/__init__.py
similarity index 100%
rename from cli/simple_read_only/__init__.py
rename to tempest/cli/simple_read_only/__init__.py
diff --git a/cli/simple_read_only/test_compute.py b/tempest/cli/simple_read_only/test_compute.py
similarity index 98%
rename from cli/simple_read_only/test_compute.py
rename to tempest/cli/simple_read_only/test_compute.py
index d301d38..fa64561 100644
--- a/cli/simple_read_only/test_compute.py
+++ b/tempest/cli/simple_read_only/test_compute.py
@@ -21,7 +21,7 @@
from oslo.config import cfg
import testtools
-import cli
+import tempest.cli
CONF = cfg.CONF
@@ -30,7 +30,7 @@
LOG = logging.getLogger(__name__)
-class SimpleReadOnlyNovaClientTest(cli.ClientTestBase):
+class SimpleReadOnlyNovaClientTest(tempest.cli.ClientTestBase):
"""
This is a first pass at a simple read only python-novaclient test. This
diff --git a/cli/simple_read_only/test_compute_manage.py b/tempest/cli/simple_read_only/test_compute_manage.py
similarity index 96%
rename from cli/simple_read_only/test_compute_manage.py
rename to tempest/cli/simple_read_only/test_compute_manage.py
index bbcc5b1..a788c8b 100644
--- a/cli/simple_read_only/test_compute_manage.py
+++ b/tempest/cli/simple_read_only/test_compute_manage.py
@@ -18,13 +18,13 @@
import logging
import subprocess
-import cli
+import tempest.cli
LOG = logging.getLogger(__name__)
-class SimpleReadOnlyNovaManageTest(cli.ClientTestBase):
+class SimpleReadOnlyNovaManageTest(tempest.cli.ClientTestBase):
"""
This is a first pass at a simple read only nova-manage test. This
diff --git a/cli/simple_read_only/test_glance.py b/tempest/cli/simple_read_only/test_glance.py
similarity index 96%
rename from cli/simple_read_only/test_glance.py
rename to tempest/cli/simple_read_only/test_glance.py
index f9822cc..b3b3eb7 100644
--- a/cli/simple_read_only/test_glance.py
+++ b/tempest/cli/simple_read_only/test_glance.py
@@ -19,13 +19,13 @@
import re
import subprocess
-import cli
+import tempest.cli
LOG = logging.getLogger(__name__)
-class SimpleReadOnlyGlanceClientTest(cli.ClientTestBase):
+class SimpleReadOnlyGlanceClientTest(tempest.cli.ClientTestBase):
"""Basic, read-only tests for Glance CLI client.
Checks return values and output of read-only commands.
diff --git a/cli/simple_read_only/test_keystone.py b/tempest/cli/simple_read_only/test_keystone.py
similarity index 97%
rename from cli/simple_read_only/test_keystone.py
rename to tempest/cli/simple_read_only/test_keystone.py
index 4b14c3c..067f58c 100644
--- a/cli/simple_read_only/test_keystone.py
+++ b/tempest/cli/simple_read_only/test_keystone.py
@@ -19,13 +19,13 @@
import re
import subprocess
-import cli
+import tempest.cli
LOG = logging.getLogger(__name__)
-class SimpleReadOnlyKeystoneClientTest(cli.ClientTestBase):
+class SimpleReadOnlyKeystoneClientTest(tempest.cli.ClientTestBase):
"""Basic, read-only tests for Keystone CLI client.
Checks return values and output of read-only commands.
diff --git a/tempest/clients.py b/tempest/clients.py
index 642f009..037a1c4 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -22,39 +22,60 @@
from tempest.services import botoclients
from tempest.services.compute.json.aggregates_client import \
AggregatesClientJSON
+from tempest.services.compute.json.availability_zone_client import \
+ AvailabilityZoneClientJSON
from tempest.services.compute.json.extensions_client import \
ExtensionsClientJSON
+from tempest.services.compute.json.fixed_ips_client import FixedIPsClientJSON
from tempest.services.compute.json.flavors_client import FlavorsClientJSON
from tempest.services.compute.json.floating_ips_client import \
FloatingIPsClientJSON
from tempest.services.compute.json.hosts_client import HostsClientJSON
from tempest.services.compute.json.images_client import ImagesClientJSON
+from tempest.services.compute.json.interfaces_client import \
+ InterfacesClientJSON
from tempest.services.compute.json.keypairs_client import KeyPairsClientJSON
from tempest.services.compute.json.limits_client import LimitsClientJSON
from tempest.services.compute.json.quotas_client import QuotasClientJSON
from tempest.services.compute.json.security_groups_client import \
SecurityGroupsClientJSON
from tempest.services.compute.json.servers_client import ServersClientJSON
+from tempest.services.compute.json.services_client import ServicesClientJSON
from tempest.services.compute.json.volumes_extensions_client import \
VolumesExtensionsClientJSON
+from tempest.services.compute.xml.aggregates_client import AggregatesClientXML
+from tempest.services.compute.xml.availability_zone_client import \
+ AvailabilityZoneClientXML
from tempest.services.compute.xml.extensions_client import ExtensionsClientXML
+from tempest.services.compute.xml.fixed_ips_client import FixedIPsClientXML
from tempest.services.compute.xml.flavors_client import FlavorsClientXML
from tempest.services.compute.xml.floating_ips_client import \
FloatingIPsClientXML
from tempest.services.compute.xml.images_client import ImagesClientXML
+from tempest.services.compute.xml.interfaces_client import \
+ InterfacesClientXML
from tempest.services.compute.xml.keypairs_client import KeyPairsClientXML
from tempest.services.compute.xml.limits_client import LimitsClientXML
from tempest.services.compute.xml.quotas_client import QuotasClientXML
from tempest.services.compute.xml.security_groups_client \
import SecurityGroupsClientXML
from tempest.services.compute.xml.servers_client import ServersClientXML
+from tempest.services.compute.xml.services_client import ServicesClientXML
from tempest.services.compute.xml.volumes_extensions_client import \
VolumesExtensionsClientXML
-from tempest.services.identity.v3.json.endpoints_client import \
- EndPointClientJSON
from tempest.services.identity.json.identity_client import IdentityClientJSON
from tempest.services.identity.json.identity_client import TokenClientJSON
+from tempest.services.identity.v3.json.endpoints_client import \
+ EndPointClientJSON
+from tempest.services.identity.v3.json.identity_client import \
+ IdentityV3ClientJSON
+from tempest.services.identity.v3.json.service_client import \
+ ServiceClientJSON
from tempest.services.identity.v3.xml.endpoints_client import EndPointClientXML
+from tempest.services.identity.v3.xml.identity_client import \
+ IdentityV3ClientXML
+from tempest.services.identity.v3.xml.service_client import \
+ ServiceClientXML
from tempest.services.identity.xml.identity_client import IdentityClientXML
from tempest.services.identity.xml.identity_client import TokenClientXML
from tempest.services.image.v1.json.image_client import ImageClientJSON
@@ -67,6 +88,8 @@
from tempest.services.object_storage.object_client import ObjectClient
from tempest.services.object_storage.object_client import \
ObjectClientCustomizedHeader
+from tempest.services.orchestration.json.orchestration_client import \
+ OrchestrationClient
from tempest.services.volume.json.admin.volume_types_client import \
VolumeTypesClientJSON
from tempest.services.volume.json.snapshots_client import SnapshotsClientJSON
@@ -75,12 +98,6 @@
VolumeTypesClientXML
from tempest.services.volume.xml.snapshots_client import SnapshotsClientXML
from tempest.services.volume.xml.volumes_client import VolumesClientXML
-from tempest.services.compute.json.interfaces_client import \
- InterfacesClientJSON
-from tempest.services.compute.xml.interfaces_client import \
- InterfacesClientXML
-from tempest.services.compute.json.fixed_ips_client import FixedIPsClientJSON
-from tempest.services.compute.xml.fixed_ips_client import FixedIPsClientXML
LOG = logging.getLogger(__name__)
@@ -149,6 +166,11 @@
"xml": IdentityClientXML,
}
+IDENTITY_V3_CLIENT = {
+ "json": IdentityV3ClientJSON,
+ "xml": IdentityV3ClientXML,
+}
+
TOKEN_CLIENT = {
"json": TokenClientJSON,
"xml": TokenClientXML,
@@ -174,6 +196,26 @@
"xml": FixedIPsClientXML
}
+AVAILABILITY_ZONE_CLIENT = {
+ "json": AvailabilityZoneClientJSON,
+ "xml": AvailabilityZoneClientXML,
+}
+
+SERVICE_CLIENT = {
+ "json": ServiceClientJSON,
+ "xml": ServiceClientXML,
+}
+
+AGGREGATES_CLIENT = {
+ "json": AggregatesClientJSON,
+ "xml": AggregatesClientXML,
+}
+
+SERVICES_CLIENT = {
+ "json": ServicesClientJSON,
+ "xml": ServicesClientXML,
+}
+
class Manager(object):
@@ -232,12 +274,19 @@
self.volume_types_client = \
VOLUME_TYPES_CLIENTS[interface](*client_args)
self.identity_client = IDENTITY_CLIENT[interface](*client_args)
+ self.identity_v3_client = \
+ IDENTITY_V3_CLIENT[interface](*client_args)
self.token_client = TOKEN_CLIENT[interface](self.config)
self.security_groups_client = \
SECURITY_GROUPS_CLIENT[interface](*client_args)
self.interfaces_client = INTERFACES_CLIENT[interface](*client_args)
self.endpoints_client = ENDPOINT_CLIENT[interface](*client_args)
self.fixed_ips_client = FIXED_IPS_CLIENT[interface](*client_args)
+ self.availability_zone_client = \
+ AVAILABILITY_ZONE_CLIENT[interface](*client_args)
+ self.service_client = SERVICE_CLIENT[interface](*client_args)
+ self.aggregates_client = AGGREGATES_CLIENT[interface](*client_args)
+ self.services_client = SERVICES_CLIENT[interface](*client_args)
except KeyError:
msg = "Unsupported interface type `%s'" % interface
raise exceptions.InvalidConfiguration(msg)
@@ -248,12 +297,12 @@
self.image_client_v2 = ImageClientV2JSON(*client_args)
self.container_client = ContainerClient(*client_args)
self.object_client = ObjectClient(*client_args)
+ self.orchestration_client = OrchestrationClient(*client_args)
self.ec2api_client = botoclients.APIClientEC2(*client_args)
self.s3_client = botoclients.ObjectClientS3(*client_args)
self.custom_object_client = ObjectClientCustomizedHeader(*client_args)
self.custom_account_client = \
AccountClientCustomizedHeader(*client_args)
- self.aggregates_client = AggregatesClientJSON(*client_args)
class AltManager(Manager):
@@ -299,3 +348,17 @@
conf.compute_admin.password,
conf.compute_admin.tenant_name,
interface=interface)
+
+
+class OrchestrationManager(Manager):
+ """
+ Manager object that uses the admin credentials for its
+ so that heat templates can create users
+ """
+ def __init__(self, interface='json'):
+ conf = config.TempestConfig()
+ base = super(OrchestrationManager, self)
+ base.__init__(conf.identity.admin_username,
+ conf.identity.admin_password,
+ conf.identity.admin_tenant_name,
+ interface=interface)
diff --git a/tempest/common/glance_http.py b/tempest/common/glance_http.py
index 0902239..4ddaf17 100644
--- a/tempest/common/glance_http.py
+++ b/tempest/common/glance_http.py
@@ -136,7 +136,7 @@
# Read body into string if it isn't obviously image data
if resp.getheader('content-type', None) != 'application/octet-stream':
- body_str = ''.join([chunk for chunk in body_iter])
+ body_str = ''.join([body_chunk for body_chunk in body_iter])
body_iter = StringIO.StringIO(body_str)
self._log_response(resp, None)
else:
diff --git a/tempest/common/ssh.py b/tempest/common/ssh.py
index be6fe27..448708e 100644
--- a/tempest/common/ssh.py
+++ b/tempest/common/ssh.py
@@ -16,7 +16,7 @@
# under the License.
-from cStringIO import StringIO
+import cStringIO
import select
import socket
import time
@@ -28,7 +28,6 @@
with warnings.catch_warnings():
warnings.simplefilter("ignore")
import paramiko
- from paramiko import RSAKey
class Client(object):
@@ -39,7 +38,8 @@
self.username = username
self.password = password
if isinstance(pkey, basestring):
- pkey = RSAKey.from_private_key(StringIO(str(pkey)))
+ pkey = paramiko.RSAKey.from_private_key(
+ cStringIO.StringIO(str(pkey)))
self.pkey = pkey
self.look_for_keys = look_for_keys
self.key_filename = key_filename
@@ -117,8 +117,8 @@
ready = select.select(*select_params)
if not any(ready):
raise exceptions.TimeoutException(
- "Command: '{0}' executed on host '{1}'.".format(
- cmd, self.host))
+ "Command: '{0}' executed on host '{1}'.".format(
+ cmd, self.host))
if not ready[0]: # If there is nothing to read.
continue
out_chunk = err_chunk = None
@@ -133,8 +133,8 @@
exit_status = channel.recv_exit_status()
if 0 != exit_status:
raise exceptions.SSHExecCommandFailed(
- command=cmd, exit_status=exit_status,
- strerror=''.join(err_data))
+ command=cmd, exit_status=exit_status,
+ strerror=''.join(err_data))
return ''.join(out_data)
def test_connection_auth(self):
diff --git a/tempest/config.py b/tempest/config.py
index 9c41660..f5f56a8 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -156,6 +156,9 @@
default=60,
help="Timeout in seconds to wait for output from ssh "
"channel."),
+ cfg.StrOpt('fixed_network_name',
+ default='private',
+ help="Visible fixed network name "),
cfg.StrOpt('network_for_ssh',
default='public',
help="Network used for SSH connections."),
@@ -243,7 +246,11 @@
help="Version of the API"),
cfg.StrOpt('catalog_type',
default='image',
- help='Catalog type of the Image service.')
+ help='Catalog type of the Image service.'),
+ cfg.StrOpt('http_image',
+ default='http://download.cirros-cloud.net/0.3.1/'
+ 'cirros-0.3.1-x86_64-uec.tar.gz',
+ help='http accessable image')
]
@@ -303,6 +310,15 @@
cfg.StrOpt('catalog_type',
default='Volume',
help="Catalog type of the Volume Service"),
+ cfg.BoolOpt('multi_backend_enabled',
+ default=False,
+ help="Runs Cinder multi-backend test (requires 2 backend)"),
+ cfg.StrOpt('backend1_name',
+ default='LVM_iSCSI',
+ help="Name of the backend1 (must be declared in cinder.conf)"),
+ cfg.StrOpt('backend2_name',
+ default='LVM_iSCSI_1',
+ help="Name of the backend2 (must be declared in cinder.conf)"),
]
@@ -335,6 +351,48 @@
for opt in ObjectStoreConfig:
conf.register_opt(opt, group='object-storage')
+
+orchestration_group = cfg.OptGroup(name='orchestration',
+ title='Orchestration Service Options')
+
+OrchestrationGroup = [
+ cfg.StrOpt('catalog_type',
+ default='orchestration',
+ help="Catalog type of the Orchestration service."),
+ cfg.BoolOpt('allow_tenant_isolation',
+ default=False,
+ help="Allows test cases to create/destroy tenants and "
+ "users. This option enables isolated test cases and "
+ "better parallel execution, but also requires that "
+ "OpenStack Identity API admin credentials are known."),
+ cfg.IntOpt('build_interval',
+ default=1,
+ help="Time in seconds between build status checks."),
+ cfg.IntOpt('build_timeout',
+ default=300,
+ help="Timeout in seconds to wait for a stack to build."),
+ cfg.BoolOpt('heat_available',
+ default=False,
+ help="Whether or not Heat is expected to be available"),
+ cfg.StrOpt('instance_type',
+ default='m1.tiny',
+ help="Instance type for tests. Needs to be big enough for a "
+ "full OS plus the test workload"),
+ cfg.StrOpt('image_ref',
+ default=None,
+ help="Name of heat-cfntools enabled image to use when "
+ "launching test instances."),
+ cfg.StrOpt('keypair_name',
+ default=None,
+ help="Name of existing keypair to launch servers with."),
+]
+
+
+def register_orchestration_opts(conf):
+ conf.register_group(orchestration_group)
+ for opt in OrchestrationGroup:
+ conf.register_opt(opt, group='orchestration')
+
boto_group = cfg.OptGroup(name='boto',
title='EC2/S3 options')
BotoConfig = [
@@ -398,7 +456,23 @@
help='Maximum number of instances to create during test.'),
cfg.StrOpt('controller',
default=None,
- help='Controller host.')
+ help='Controller host.'),
+ # new stress options
+ cfg.StrOpt('target_controller',
+ default=None,
+ help='Controller host.'),
+ cfg.StrOpt('target_ssh_user',
+ default=None,
+ help='ssh user.'),
+ cfg.StrOpt('target_private_key_path',
+ default=None,
+ help='Path to private key.'),
+ cfg.StrOpt('target_logfiles',
+ default=None,
+ help='regexp for list of log files.'),
+ cfg.StrOpt('log_check_interval',
+ default=60,
+ help='time between log file error checks.')
]
@@ -420,6 +494,9 @@
def __init__(self):
"""Initialize a configuration from a conf directory and conf file."""
+ config_files = []
+
+ failsafe_path = "/etc/tempest/" + self.DEFAULT_CONFIG_FILE
# Environment variables override defaults...
conf_dir = os.environ.get('TEMPEST_CONFIG_DIR',
@@ -431,16 +508,17 @@
if not (os.path.isfile(path) or
'TEMPEST_CONFIG_DIR' in os.environ or
'TEMPEST_CONFIG' in os.environ):
- path = "/etc/tempest/" + self.DEFAULT_CONFIG_FILE
+ path = failsafe_path
LOG.info("Using tempest config file %s" % path)
if not os.path.exists(path):
msg = "Config file %(path)s not found" % locals()
print >> sys.stderr, RuntimeError(msg)
- sys.exit(os.EX_NOINPUT)
+ else:
+ config_files.append(path)
- cfg.CONF([], project='tempest', default_config_files=[path])
+ cfg.CONF([], project='tempest', default_config_files=config_files)
register_compute_opts(cfg.CONF)
register_identity_opts(cfg.CONF)
@@ -449,6 +527,7 @@
register_network_opts(cfg.CONF)
register_volume_opts(cfg.CONF)
register_object_storage_opts(cfg.CONF)
+ register_orchestration_opts(cfg.CONF)
register_boto_opts(cfg.CONF)
register_compute_admin_opts(cfg.CONF)
register_stress_opts(cfg.CONF)
@@ -459,6 +538,7 @@
self.network = cfg.CONF.network
self.volume = cfg.CONF.volume
self.object_storage = cfg.CONF['object-storage']
+ self.orchestration = cfg.CONF.orchestration
self.boto = cfg.CONF.boto
self.compute_admin = cfg.CONF['compute-admin']
self.stress = cfg.CONF.stress
diff --git a/tempest/exceptions.py b/tempest/exceptions.py
index 235a2e7..448fbdf 100644
--- a/tempest/exceptions.py
+++ b/tempest/exceptions.py
@@ -90,6 +90,11 @@
message = "Snapshot %(snapshot_id)s failed to build and is in ERROR status"
+class StackBuildErrorException(TempestException):
+ message = ("Stack %(stack_identifier)s is in %(stack_status)s status "
+ "due to '%(stack_status_reason)s'")
+
+
class BadRequest(RestClientException):
message = "Bad request"
diff --git a/tempest/openstack/common/setup.py b/tempest/openstack/common/setup.py
deleted file mode 100644
index 80a0ece..0000000
--- a/tempest/openstack/common/setup.py
+++ /dev/null
@@ -1,359 +0,0 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright 2011 OpenStack LLC.
-# Copyright 2012-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.
-
-"""
-Utilities with minimum-depends for use in setup.py
-"""
-
-import email
-import os
-import re
-import subprocess
-import sys
-
-from setuptools.command import sdist
-
-
-def parse_mailmap(mailmap='.mailmap'):
- mapping = {}
- if os.path.exists(mailmap):
- with open(mailmap, 'r') as fp:
- for l in fp:
- try:
- canonical_email, alias = re.match(
- r'[^#]*?(<.+>).*(<.+>).*', l).groups()
- except AttributeError:
- continue
- mapping[alias] = canonical_email
- return mapping
-
-
-def _parse_git_mailmap(git_dir, mailmap='.mailmap'):
- mailmap = os.path.join(os.path.dirname(git_dir), mailmap)
- return parse_mailmap(mailmap)
-
-
-def canonicalize_emails(changelog, mapping):
- """Takes in a string and an email alias mapping and replaces all
- instances of the aliases in the string with their real email.
- """
- for alias, email_address in mapping.iteritems():
- changelog = changelog.replace(alias, email_address)
- return changelog
-
-
-# Get requirements from the first file that exists
-def get_reqs_from_files(requirements_files):
- for requirements_file in requirements_files:
- if os.path.exists(requirements_file):
- with open(requirements_file, 'r') as fil:
- return fil.read().split('\n')
- return []
-
-
-def parse_requirements(requirements_files=['requirements.txt',
- 'tools/pip-requires']):
- requirements = []
- for line in get_reqs_from_files(requirements_files):
- # For the requirements list, we need to inject only the portion
- # after egg= so that distutils knows the package it's looking for
- # such as:
- # -e git://github.com/openstack/nova/master#egg=nova
- if re.match(r'\s*-e\s+', line):
- requirements.append(re.sub(r'\s*-e\s+.*#egg=(.*)$', r'\1',
- line))
- # such as:
- # http://github.com/openstack/nova/zipball/master#egg=nova
- elif re.match(r'\s*https?:', line):
- requirements.append(re.sub(r'\s*https?:.*#egg=(.*)$', r'\1',
- line))
- # -f lines are for index locations, and don't get used here
- elif re.match(r'\s*-f\s+', line):
- pass
- # argparse is part of the standard library starting with 2.7
- # adding it to the requirements list screws distro installs
- elif line == 'argparse' and sys.version_info >= (2, 7):
- pass
- else:
- requirements.append(line)
-
- return requirements
-
-
-def parse_dependency_links(requirements_files=['requirements.txt',
- 'tools/pip-requires']):
- dependency_links = []
- # dependency_links inject alternate locations to find packages listed
- # in requirements
- for line in get_reqs_from_files(requirements_files):
- # skip comments and blank lines
- if re.match(r'(\s*#)|(\s*$)', line):
- continue
- # lines with -e or -f need the whole line, minus the flag
- if re.match(r'\s*-[ef]\s+', line):
- dependency_links.append(re.sub(r'\s*-[ef]\s+', '', line))
- # lines that are only urls can go in unmolested
- elif re.match(r'\s*https?:', line):
- dependency_links.append(line)
- return dependency_links
-
-
-def _run_shell_command(cmd, throw_on_error=False):
- if os.name == 'nt':
- output = subprocess.Popen(["cmd.exe", "/C", cmd],
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE)
- else:
- output = subprocess.Popen(["/bin/sh", "-c", cmd],
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE)
- out = output.communicate()
- if output.returncode and throw_on_error:
- raise Exception("%s returned %d" % cmd, output.returncode)
- if len(out) == 0:
- return None
- if len(out[0].strip()) == 0:
- return None
- return out[0].strip()
-
-
-def _get_git_directory():
- parent_dir = os.path.dirname(__file__)
- while True:
- git_dir = os.path.join(parent_dir, '.git')
- if os.path.exists(git_dir):
- return git_dir
- parent_dir, child = os.path.split(parent_dir)
- if not child: # reached to root dir
- return None
-
-
-def write_git_changelog():
- """Write a changelog based on the git changelog."""
- new_changelog = 'ChangeLog'
- git_dir = _get_git_directory()
- if not os.getenv('SKIP_WRITE_GIT_CHANGELOG'):
- if git_dir:
- git_log_cmd = 'git --git-dir=%s log --stat' % git_dir
- changelog = _run_shell_command(git_log_cmd)
- mailmap = _parse_git_mailmap(git_dir)
- with open(new_changelog, "w") as changelog_file:
- changelog_file.write(canonicalize_emails(changelog, mailmap))
- else:
- open(new_changelog, 'w').close()
-
-
-def generate_authors():
- """Create AUTHORS file using git commits."""
- jenkins_email = 'jenkins@review.(openstack|stackforge).org'
- old_authors = 'AUTHORS.in'
- new_authors = 'AUTHORS'
- git_dir = _get_git_directory()
- if not os.getenv('SKIP_GENERATE_AUTHORS'):
- if git_dir:
- # don't include jenkins email address in AUTHORS file
- git_log_cmd = ("git --git-dir=" + git_dir +
- " log --format='%aN <%aE>' | sort -u | "
- "egrep -v '" + jenkins_email + "'")
- changelog = _run_shell_command(git_log_cmd)
- mailmap = _parse_git_mailmap(git_dir)
- with open(new_authors, 'w') as new_authors_fh:
- new_authors_fh.write(canonicalize_emails(changelog, mailmap))
- if os.path.exists(old_authors):
- with open(old_authors, "r") as old_authors_fh:
- new_authors_fh.write('\n' + old_authors_fh.read())
- else:
- open(new_authors, 'w').close()
-
-
-_rst_template = """%(heading)s
-%(underline)s
-
-.. automodule:: %(module)s
- :members:
- :undoc-members:
- :show-inheritance:
-"""
-
-
-def get_cmdclass():
- """Return dict of commands to run from setup.py."""
-
- cmdclass = dict()
-
- def _find_modules(arg, dirname, files):
- for filename in files:
- if filename.endswith('.py') and filename != '__init__.py':
- arg["%s.%s" % (dirname.replace('/', '.'),
- filename[:-3])] = True
-
- class LocalSDist(sdist.sdist):
- """Builds the ChangeLog and Authors files from VC first."""
-
- def run(self):
- write_git_changelog()
- generate_authors()
- # sdist.sdist is an old style class, can't use super()
- sdist.sdist.run(self)
-
- cmdclass['sdist'] = LocalSDist
-
- # If Sphinx is installed on the box running setup.py,
- # enable setup.py to build the documentation, otherwise,
- # just ignore it
- try:
- from sphinx.setup_command import BuildDoc
-
- class LocalBuildDoc(BuildDoc):
-
- builders = ['html', 'man']
-
- def generate_autoindex(self):
- print "**Autodocumenting from %s" % os.path.abspath(os.curdir)
- modules = {}
- option_dict = self.distribution.get_option_dict('build_sphinx')
- source_dir = os.path.join(option_dict['source_dir'][1], 'api')
- if not os.path.exists(source_dir):
- os.makedirs(source_dir)
- for pkg in self.distribution.packages:
- if '.' not in pkg:
- os.path.walk(pkg, _find_modules, modules)
- module_list = modules.keys()
- module_list.sort()
- autoindex_filename = os.path.join(source_dir, 'autoindex.rst')
- with open(autoindex_filename, 'w') as autoindex:
- autoindex.write(""".. toctree::
- :maxdepth: 1
-
-""")
- for module in module_list:
- output_filename = os.path.join(source_dir,
- "%s.rst" % module)
- heading = "The :mod:`%s` Module" % module
- underline = "=" * len(heading)
- values = dict(module=module, heading=heading,
- underline=underline)
-
- print "Generating %s" % output_filename
- with open(output_filename, 'w') as output_file:
- output_file.write(_rst_template % values)
- autoindex.write(" %s.rst\n" % module)
-
- def run(self):
- if not os.getenv('SPHINX_DEBUG'):
- self.generate_autoindex()
-
- for builder in self.builders:
- self.builder = builder
- self.finalize_options()
- self.project = self.distribution.get_name()
- self.version = self.distribution.get_version()
- self.release = self.distribution.get_version()
- BuildDoc.run(self)
-
- class LocalBuildLatex(LocalBuildDoc):
- builders = ['latex']
-
- cmdclass['build_sphinx'] = LocalBuildDoc
- cmdclass['build_sphinx_latex'] = LocalBuildLatex
- except ImportError:
- pass
-
- return cmdclass
-
-
-def _get_revno(git_dir):
- """Return the number of commits since the most recent tag.
-
- We use git-describe to find this out, but if there are no
- tags then we fall back to counting commits since the beginning
- of time.
- """
- describe = _run_shell_command(
- "git --git-dir=%s describe --always" % git_dir)
- if "-" in describe:
- return describe.rsplit("-", 2)[-2]
-
- # no tags found
- revlist = _run_shell_command(
- "git --git-dir=%s rev-list --abbrev-commit HEAD" % git_dir)
- return len(revlist.splitlines())
-
-
-def _get_version_from_git(pre_version):
- """Return a version which is equal to the tag that's on the current
- revision if there is one, or tag plus number of additional revisions
- if the current revision has no tag."""
-
- git_dir = _get_git_directory()
- if git_dir:
- if pre_version:
- try:
- return _run_shell_command(
- "git --git-dir=" + git_dir + " describe --exact-match",
- throw_on_error=True).replace('-', '.')
- except Exception:
- sha = _run_shell_command(
- "git --git-dir=" + git_dir + " log -n1 --pretty=format:%h")
- return "%s.a%s.g%s" % (pre_version, _get_revno(git_dir), sha)
- else:
- return _run_shell_command(
- "git --git-dir=" + git_dir + " describe --always").replace(
- '-', '.')
- return None
-
-
-def _get_version_from_pkg_info(package_name):
- """Get the version from PKG-INFO file if we can."""
- try:
- pkg_info_file = open('PKG-INFO', 'r')
- except (IOError, OSError):
- return None
- try:
- pkg_info = email.message_from_file(pkg_info_file)
- except email.MessageError:
- return None
- # Check to make sure we're in our own dir
- if pkg_info.get('Name', None) != package_name:
- return None
- return pkg_info.get('Version', None)
-
-
-def get_version(package_name, pre_version=None):
- """Get the version of the project. First, try getting it from PKG-INFO, if
- it exists. If it does, that means we're in a distribution tarball or that
- install has happened. Otherwise, if there is no PKG-INFO file, pull the
- version from git.
-
- We do not support setup.py version sanity in git archive tarballs, nor do
- we support packagers directly sucking our git repo into theirs. We expect
- that a source tarball be made from our git repo - or that if someone wants
- to make a source tarball from a fork of our repo with additional tags in it
- that they understand and desire the results of doing that.
- """
- version = os.environ.get("OSLO_PACKAGE_VERSION", None)
- if version:
- return version
- version = _get_version_from_pkg_info(package_name)
- if version:
- return version
- version = _get_version_from_git(pre_version)
- if version:
- return version
- raise Exception("Versioning for this project requires either an sdist"
- " tarball, or access to an upstream git repository.")
diff --git a/tempest/services/botoclients.py b/tempest/services/botoclients.py
index 0870c96..628151a 100644
--- a/tempest/services/botoclients.py
+++ b/tempest/services/botoclients.py
@@ -96,7 +96,7 @@
ec2_cred.secret
else:
raise exceptions.InvalidConfiguration(
- "Unable to get access and secret keys")
+ "Unable to get access and secret keys")
return self.connect_method(**self.connection_data)
diff --git a/tempest/services/compute/json/availability_zone_client.py b/tempest/services/compute/json/availability_zone_client.py
new file mode 100644
index 0000000..b11871b
--- /dev/null
+++ b/tempest/services/compute/json/availability_zone_client.py
@@ -0,0 +1,39 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 NEC Corporation.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import json
+
+from tempest.common.rest_client import RestClient
+
+
+class AvailabilityZoneClientJSON(RestClient):
+
+ def __init__(self, config, username, password, auth_url, tenant_name=None):
+ super(AvailabilityZoneClientJSON, self).__init__(config, username,
+ password, auth_url,
+ tenant_name)
+ self.service = self.config.compute.catalog_type
+
+ def get_availability_zone_list(self):
+ resp, body = self.get('os-availability-zone')
+ body = json.loads(body)
+ return resp, body['availabilityZoneInfo']
+
+ def get_availability_zone_list_detail(self):
+ resp, body = self.get('os-availability-zone/detail')
+ body = json.loads(body)
+ return resp, body['availabilityZoneInfo']
diff --git a/tempest/services/compute/json/servers_client.py b/tempest/services/compute/json/servers_client.py
index 9e71f3d..3569b50 100644
--- a/tempest/services/compute/json/servers_client.py
+++ b/tempest/services/compute/json/servers_client.py
@@ -390,3 +390,17 @@
def unrescue_server(self, server_id):
"""Unrescue the provided server."""
return self.action(server_id, 'unrescue', None)
+
+ def list_instance_actions(self, server_id):
+ """List the provided server action."""
+ resp, body = self.get("servers/%s/os-instance-actions" %
+ str(server_id))
+ body = json.loads(body)
+ return resp, body['instanceActions']
+
+ def get_instance_action(self, server_id, request_id):
+ """Returns the action details of the provided server."""
+ resp, body = self.get("servers/%s/os-instance-actions/%s" %
+ (str(server_id), str(request_id)))
+ body = json.loads(body)
+ return resp, body['instanceAction']
diff --git a/tempest/services/compute/json/services_client.py b/tempest/services/compute/json/services_client.py
new file mode 100644
index 0000000..d054f72
--- /dev/null
+++ b/tempest/services/compute/json/services_client.py
@@ -0,0 +1,33 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 NEC Corporation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import json
+
+from tempest.common.rest_client import RestClient
+
+
+class ServicesClientJSON(RestClient):
+
+ def __init__(self, config, username, password, auth_url, tenant_name=None):
+ super(ServicesClientJSON, self).__init__(config, username, password,
+ auth_url, tenant_name)
+ self.service = self.config.compute.catalog_type
+
+ def list_services(self):
+ resp, body = self.get("os-services")
+ body = json.loads(body)
+ return resp, body['services']
diff --git a/tempest/services/compute/xml/aggregates_client.py b/tempest/services/compute/xml/aggregates_client.py
new file mode 100644
index 0000000..0ef8e22
--- /dev/null
+++ b/tempest/services/compute/xml/aggregates_client.py
@@ -0,0 +1,103 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 NEC Corporation.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from lxml import etree
+
+from tempest.common.rest_client import RestClientXML
+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 xml_to_json
+
+
+class AggregatesClientXML(RestClientXML):
+
+ def __init__(self, config, username, password, auth_url, tenant_name=None):
+ super(AggregatesClientXML, self).__init__(config, username, password,
+ auth_url, tenant_name)
+ self.service = self.config.compute.catalog_type
+
+ def _format_aggregate(self, g):
+ agg = xml_to_json(g)
+ aggregate = {}
+ for key, value in agg.items():
+ if key == 'hosts':
+ aggregate['hosts'] = []
+ for k, v in value.items():
+ aggregate['hosts'].append(v)
+ elif key == 'availability_zone':
+ aggregate[key] = None if value == 'None' else value
+ else:
+ aggregate[key] = value
+ return aggregate
+
+ def _parse_array(self, node):
+ return [self._format_aggregate(x) for x in node]
+
+ def list_aggregates(self):
+ """Get aggregate list."""
+ resp, body = self.get("os-aggregates", self.headers)
+ aggregates = self._parse_array(etree.fromstring(body))
+ return resp, aggregates
+
+ def get_aggregate(self, aggregate_id):
+ """Get details of the given aggregate."""
+ resp, body = self.get("os-aggregates/%s" % str(aggregate_id),
+ self.headers)
+ aggregate = self._format_aggregate(etree.fromstring(body))
+ return resp, aggregate
+
+ def create_aggregate(self, name, availability_zone=None):
+ """Creates a new aggregate."""
+ post_body = Element("aggregate",
+ name=name,
+ availability_zone=availability_zone)
+ resp, body = self.post('os-aggregates',
+ str(Document(post_body)),
+ self.headers)
+ aggregate = self._format_aggregate(etree.fromstring(body))
+ return resp, aggregate
+
+ def delete_aggregate(self, aggregate_id):
+ """Deletes the given aggregate."""
+ return self.delete("os-aggregates/%s" % str(aggregate_id),
+ self.headers)
+
+ def is_resource_deleted(self, id):
+ try:
+ self.get_aggregate(id)
+ except exceptions.NotFound:
+ return True
+ return False
+
+ def add_host(self, aggregate_id, host):
+ """Adds a host to the given aggregate."""
+ post_body = Element("add_host", host=host)
+ 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
+
+ def remove_host(self, aggregate_id, host):
+ """Removes a host from the given aggregate."""
+ post_body = Element("remove_host", host=host)
+ 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/availability_zone_client.py b/tempest/services/compute/xml/availability_zone_client.py
new file mode 100644
index 0000000..ae93774
--- /dev/null
+++ b/tempest/services/compute/xml/availability_zone_client.py
@@ -0,0 +1,43 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 NEC Corporation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from lxml import etree
+
+from tempest.common.rest_client import RestClientXML
+from tempest.services.compute.xml.common import xml_to_json
+
+
+class AvailabilityZoneClientXML(RestClientXML):
+
+ def __init__(self, config, username, password, auth_url, tenant_name=None):
+ super(AvailabilityZoneClientXML, self).__init__(config, username,
+ password, auth_url,
+ tenant_name)
+ self.service = self.config.compute.catalog_type
+
+ def _parse_array(self, node):
+ return [xml_to_json(x) for x in node]
+
+ def get_availability_zone_list(self):
+ resp, body = self.get('os-availability-zone', self.headers)
+ availability_zone = self._parse_array(etree.fromstring(body))
+ return resp, availability_zone
+
+ def get_availability_zone_list_detail(self):
+ resp, body = self.get('os-availability-zone/detail', self.headers)
+ availability_zone = self._parse_array(etree.fromstring(body))
+ return resp, availability_zone
diff --git a/tempest/services/compute/xml/security_groups_client.py b/tempest/services/compute/xml/security_groups_client.py
index 4fccc29..08b381c 100644
--- a/tempest/services/compute/xml/security_groups_client.py
+++ b/tempest/services/compute/xml/security_groups_client.py
@@ -31,8 +31,8 @@
def __init__(self, config, username, password, auth_url, tenant_name=None):
super(SecurityGroupsClientXML, self).__init__(
- config, username, password,
- auth_url, tenant_name)
+ config, username, password,
+ auth_url, tenant_name)
self.service = self.config.compute.catalog_type
def _parse_array(self, node):
diff --git a/tempest/services/compute/xml/servers_client.py b/tempest/services/compute/xml/servers_client.py
index 331d560..f7e8915 100644
--- a/tempest/services/compute/xml/servers_client.py
+++ b/tempest/services/compute/xml/servers_client.py
@@ -527,3 +527,17 @@
resp, body = self.delete('servers/%s/os-volume_attachments/%s' %
(server_id, volume_id), headers)
return resp, body
+
+ def list_instance_actions(self, server_id):
+ """List the provided server action."""
+ resp, body = self.get("servers/%s/os-instance-actions" % server_id,
+ self.headers)
+ body = self._parse_array(etree.fromstring(body))
+ return resp, body
+
+ def get_instance_action(self, server_id, request_id):
+ """Returns the action details of the provided server."""
+ resp, body = self.get("servers/%s/os-instance-actions/%s" %
+ (server_id, request_id), self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body
diff --git a/tempest/services/compute/xml/services_client.py b/tempest/services/compute/xml/services_client.py
new file mode 100644
index 0000000..ce23403
--- /dev/null
+++ b/tempest/services/compute/xml/services_client.py
@@ -0,0 +1,34 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 NEC Corporation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from lxml import etree
+from tempest.common.rest_client import RestClientXML
+from tempest.services.compute.xml.common import xml_to_json
+
+
+class ServicesClientXML(RestClientXML):
+
+ def __init__(self, config, username, password, auth_url, tenant_name=None):
+ super(ServicesClientXML, self).__init__(config, username, password,
+ auth_url, tenant_name)
+ self.service = self.config.compute.catalog_type
+
+ def list_services(self):
+ resp, body = self.get("os-services", self.headers)
+ node = etree.fromstring(body)
+ body = [xml_to_json(x) for x in node.getchildren()]
+ return resp, body
diff --git a/tempest/services/identity/json/identity_client.py b/tempest/services/identity/json/identity_client.py
index 5b6eaa0..a216b55 100644
--- a/tempest/services/identity/json/identity_client.py
+++ b/tempest/services/identity/json/identity_client.py
@@ -138,6 +138,12 @@
body = json.loads(body)
return resp, body['user']
+ def get_user(self, user_id):
+ """GET a user."""
+ resp, body = self.get("users/%s" % user_id)
+ body = json.loads(body)
+ return resp, body['user']
+
def delete_user(self, user_id):
"""Delete a user."""
resp, body = self.delete("users/%s" % user_id)
@@ -152,7 +158,7 @@
def enable_disable_user(self, user_id, enabled):
"""Enables or disables a user."""
put_body = {
- 'enabled': enabled
+ 'enabled': enabled
}
put_body = json.dumps({'user': put_body})
resp, body = self.put('users/%s/enabled' % user_id,
diff --git a/tempest/services/identity/v3/json/endpoints_client.py b/tempest/services/identity/v3/json/endpoints_client.py
index 3cb8f90..cf26d0a 100755
--- a/tempest/services/identity/v3/json/endpoints_client.py
+++ b/tempest/services/identity/v3/json/endpoints_client.py
@@ -16,7 +16,7 @@
# under the License.
import json
-from urlparse import urlparse
+import urlparse
from tempest.common.rest_client import RestClient
@@ -33,8 +33,8 @@
def request(self, method, url, headers=None, body=None, wait=None):
"""Overriding the existing HTTP request in super class rest_client."""
self._set_auth()
- self.base_url = self.base_url.replace(urlparse(self.base_url).path,
- "/v3")
+ self.base_url = self.base_url.replace(
+ urlparse.urlparse(self.base_url).path, "/v3")
return super(EndPointClientJSON, self).request(method, url,
headers=headers,
body=body)
diff --git a/tempest/services/identity/v3/json/identity_client.py b/tempest/services/identity/v3/json/identity_client.py
new file mode 100644
index 0000000..014df1e
--- /dev/null
+++ b/tempest/services/identity/v3/json/identity_client.py
@@ -0,0 +1,162 @@
+# 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.
+
+import json
+from urlparse import urlparse
+
+from tempest.common.rest_client import RestClient
+
+
+class IdentityV3ClientJSON(RestClient):
+
+ def __init__(self, config, username, password, auth_url, tenant_name=None):
+ super(IdentityV3ClientJSON, self).__init__(config, username, password,
+ auth_url, tenant_name)
+ self.service = self.config.identity.catalog_type
+ self.endpoint_url = 'adminURL'
+
+ def request(self, method, url, headers=None, body=None, wait=None):
+ """Overriding the existing HTTP request in super class rest_client."""
+ self._set_auth()
+ self.base_url = self.base_url.replace(urlparse(self.base_url).path,
+ "/v3")
+ return super(IdentityV3ClientJSON, self).request(method, url,
+ headers=headers,
+ body=body)
+
+ def create_user(self, user_name, **kwargs):
+ """Creates a user."""
+ password = kwargs.get('password', None)
+ email = kwargs.get('email', None)
+ en = kwargs.get('enabled', True)
+ project_id = kwargs.get('project_id', None)
+ description = kwargs.get('description', None)
+ domain_id = kwargs.get('domain_id', 'default')
+ post_body = {
+ 'project_id': project_id,
+ 'description': description,
+ 'domain_id': domain_id,
+ 'email': email,
+ 'enabled': en,
+ 'name': user_name,
+ 'password': password
+ }
+ post_body = json.dumps({'user': post_body})
+ resp, body = self.post('users', post_body,
+ self.headers)
+ body = json.loads(body)
+ return resp, body['user']
+
+ def update_user(self, user_id, name, **kwargs):
+ """Updates a user."""
+ email = kwargs.get('email', None)
+ en = kwargs.get('enabled', True)
+ project_id = kwargs.get('project_id', None)
+ description = kwargs.get('description', None)
+ domain_id = kwargs.get('domain_id', 'default')
+ post_body = {
+ 'name': name,
+ 'email': email,
+ 'enabled': en,
+ 'project_id': project_id,
+ 'id': user_id,
+ 'domain_id': domain_id,
+ 'description': description
+ }
+ post_body = json.dumps({'user': post_body})
+ resp, body = self.patch('users/%s' % user_id, post_body,
+ self.headers)
+ body = json.loads(body)
+ return resp, body['user']
+
+ def list_user_projects(self, user_id):
+ """Lists the projects on which a user has roles assigned."""
+ resp, body = self.get('users/%s/projects' % user_id, self.headers)
+ body = json.loads(body)
+ return resp, body['projects']
+
+ def get_users(self):
+ """Get the list of users."""
+ resp, body = self.get("users")
+ body = json.loads(body)
+ return resp, body['users']
+
+ def get_user(self, user_id):
+ """GET a user."""
+ resp, body = self.get("users/%s" % user_id)
+ body = json.loads(body)
+ return resp, body['user']
+
+ def delete_user(self, user_id):
+ """Deletes a User."""
+ resp, body = self.delete("users/%s" % user_id)
+ return resp, body
+
+ def create_project(self, name, **kwargs):
+ """Creates a project."""
+ description = kwargs.get('description', None)
+ en = kwargs.get('enabled', True)
+ domain_id = kwargs.get('domain_id', 'default')
+ post_body = {
+ 'description': description,
+ 'domain_id': domain_id,
+ 'enabled': en,
+ 'name': name
+ }
+ post_body = json.dumps({'project': post_body})
+ resp, body = self.post('projects', post_body, self.headers)
+ body = json.loads(body)
+ return resp, body['project']
+
+ def get_project(self, project_id):
+ """GET a Project."""
+ resp, body = self.get("projects/%s" % project_id)
+ body = json.loads(body)
+ return resp, body['project']
+
+ def delete_project(self, project_id):
+ """Delete a project."""
+ resp, body = self.delete('projects/%s' % str(project_id))
+ return resp, body
+
+ def create_role(self, name):
+ """Create a Role."""
+ post_body = {
+ 'name': name
+ }
+ post_body = json.dumps({'role': post_body})
+ resp, body = self.post('roles', post_body, self.headers)
+ body = json.loads(body)
+ return resp, body['role']
+
+ def get_role(self, role_id):
+ """GET a Role."""
+ resp, body = self.get('roles/%s' % str(role_id))
+ body = json.loads(body)
+ return resp, body['role']
+
+ def delete_role(self, role_id):
+ """Delete a role."""
+ resp, body = self.delete('roles/%s' % str(role_id))
+ return resp, body
+
+ def assign_user_role(self, project_id, user_id, role_id):
+ """Add roles to a user on a project."""
+ resp, body = self.put('projects/%s/users/%s/roles/%s' %
+ (project_id, user_id, role_id), None,
+ self.headers)
+ return resp, body
diff --git a/tempest/services/identity/v3/json/service_client.py b/tempest/services/identity/v3/json/service_client.py
new file mode 100644
index 0000000..dde572e
--- /dev/null
+++ b/tempest/services/identity/v3/json/service_client.py
@@ -0,0 +1,63 @@
+# 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.
+
+import json
+from urlparse import urlparse
+
+from tempest.common.rest_client import RestClient
+
+
+class ServiceClientJSON(RestClient):
+
+ def __init__(self, config, username, password, auth_url, tenant_name=None):
+ super(ServiceClientJSON, self).__init__(config, username, password,
+ auth_url, tenant_name)
+ self.service = self.config.identity.catalog_type
+ self.endpoint_url = 'adminURL'
+
+ def request(self, method, url, headers=None, body=None, wait=None):
+ """Overriding the existing HTTP request in super class rest_client."""
+ self._set_auth()
+ self.base_url = self.base_url.replace(urlparse(self.base_url).path,
+ "/v3")
+ return super(ServiceClientJSON, self).request(method, url,
+ headers=headers,
+ body=body)
+
+ def update_service(self, service_id, **kwargs):
+ """Updates a service."""
+ resp, body = self.get_service(service_id)
+ name = kwargs.get('name', body['name'])
+ type = kwargs.get('type', body['type'])
+ desc = kwargs.get('description', body['description'])
+ patch_body = {
+ 'description': desc,
+ 'type': type,
+ 'name': name
+ }
+ patch_body = json.dumps({'service': patch_body})
+ resp, body = self.patch('services/%s' % service_id,
+ patch_body, self.headers)
+ body = json.loads(body)
+ return resp, body['service']
+
+ def get_service(self, service_id):
+ """Get Service."""
+ url = 'services/%s' % service_id
+ resp, body = self.get(url)
+ body = json.loads(body)
+ return resp, body['service']
diff --git a/tempest/services/identity/v3/xml/endpoints_client.py b/tempest/services/identity/v3/xml/endpoints_client.py
index 8400976..f81fccf 100755
--- a/tempest/services/identity/v3/xml/endpoints_client.py
+++ b/tempest/services/identity/v3/xml/endpoints_client.py
@@ -14,7 +14,7 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
-from urlparse import urlparse
+import urlparse
import httplib2
from lxml import etree
@@ -52,8 +52,8 @@
dscv = self.config.identity.disable_ssl_certificate_validation
self.http_obj = httplib2.Http(disable_ssl_certificate_validation=dscv)
self._set_auth()
- self.base_url = self.base_url.replace(urlparse(self.base_url).path,
- "/v3")
+ self.base_url = self.base_url.replace(
+ urlparse.urlparse(self.base_url).path, "/v3")
return super(EndPointClientXML, self).request(method, url,
headers=headers,
body=body)
diff --git a/tempest/services/identity/v3/xml/identity_client.py b/tempest/services/identity/v3/xml/identity_client.py
new file mode 100644
index 0000000..92151dd
--- /dev/null
+++ b/tempest/services/identity/v3/xml/identity_client.py
@@ -0,0 +1,187 @@
+# 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 urlparse import urlparse
+
+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
+
+
+XMLNS = "http://docs.openstack.org/identity/api/v3"
+
+
+class IdentityV3ClientXML(RestClientXML):
+
+ def __init__(self, config, username, password, auth_url, tenant_name=None):
+ super(IdentityV3ClientXML, self).__init__(config, username, password,
+ auth_url, tenant_name)
+ self.service = self.config.identity.catalog_type
+ self.endpoint_url = 'adminURL'
+
+ def _parse_projects(self, node):
+ array = []
+ for child in node.getchildren():
+ tag_list = child.tag.split('}', 1)
+ if tag_list[1] == "project":
+ array.append(xml_to_json(child))
+ return array
+
+ def _parse_array(self, node):
+ array = []
+ for child in node.getchildren():
+ array.append(xml_to_json(child))
+ return array
+
+ def _parse_body(self, body):
+ json = xml_to_json(body)
+ return json
+
+ def request(self, method, url, headers=None, body=None, wait=None):
+ """Overriding the existing HTTP request in super class RestClient."""
+ self._set_auth()
+ self.base_url = self.base_url.replace(urlparse(self.base_url).path,
+ "/v3")
+ return super(IdentityV3ClientXML, self).request(method, url,
+ headers=headers,
+ body=body)
+
+ def create_user(self, user_name, **kwargs):
+ """Creates a user."""
+ password = kwargs.get('password', None)
+ email = kwargs.get('email', None)
+ en = kwargs.get('enabled', 'true')
+ project_id = kwargs.get('project_id', None)
+ description = kwargs.get('description', None)
+ domain_id = kwargs.get('domain_id', 'default')
+ post_body = Element("user",
+ xmlns=XMLNS,
+ name=user_name,
+ password=password,
+ description=description,
+ email=email,
+ enabled=str(en).lower(),
+ project_id=project_id,
+ domain_id=domain_id)
+ resp, body = self.post('users', str(Document(post_body)),
+ self.headers)
+ body = self._parse_body(etree.fromstring(body))
+ return resp, body
+
+ def update_user(self, user_id, name, **kwargs):
+ """Updates a user."""
+ email = kwargs.get('email', None)
+ en = kwargs.get('enabled', True)
+ project_id = kwargs.get('project_id', None)
+ domain_id = kwargs.get('domain_id', 'default')
+ description = kwargs.get('description', None)
+ update_user = Element("user",
+ xmlns=XMLNS,
+ name=name,
+ email=email,
+ project_id=project_id,
+ domain_id=domain_id,
+ description=description,
+ enabled=str(en).lower())
+ resp, body = self.patch('users/%s' % user_id,
+ str(Document(update_user)),
+ self.headers)
+ body = self._parse_body(etree.fromstring(body))
+ return resp, body
+
+ def list_user_projects(self, user_id):
+ """Lists the projects on which a user has roles assigned."""
+ resp, body = self.get('users/%s/projects' % user_id, self.headers)
+ body = self._parse_projects(etree.fromstring(body))
+ return resp, body
+
+ def get_users(self):
+ """Get the list of users."""
+ resp, body = self.get("users", self.headers)
+ body = self._parse_array(etree.fromstring(body))
+ return resp, body
+
+ def get_user(self, user_id):
+ """GET a user."""
+ resp, body = self.get("users/%s" % user_id, self.headers)
+ body = self._parse_body(etree.fromstring(body))
+ return resp, body
+
+ def delete_user(self, user_id):
+ """Deletes a User."""
+ resp, body = self.delete("users/%s" % user_id, self.headers)
+ return resp, body
+
+ def create_project(self, name, **kwargs):
+ """Creates a project."""
+ description = kwargs.get('description', None)
+ en = kwargs.get('enabled', 'true')
+ domain_id = kwargs.get('domain_id', 'default')
+ post_body = Element("project",
+ xmlns=XMLNS,
+ description=description,
+ domain_id=domain_id,
+ enabled=str(en).lower(),
+ name=name)
+ resp, body = self.post('projects',
+ str(Document(post_body)),
+ self.headers)
+ body = self._parse_body(etree.fromstring(body))
+ return resp, body
+
+ def get_project(self, project_id):
+ """GET a Project."""
+ resp, body = self.get("projects/%s" % project_id, self.headers)
+ body = self._parse_body(etree.fromstring(body))
+ return resp, body
+
+ def delete_project(self, project_id):
+ """Delete a project."""
+ resp, body = self.delete('projects/%s' % str(project_id))
+ return resp, body
+
+ def create_role(self, name):
+ """Create a Role."""
+ post_body = Element("role",
+ xmlns=XMLNS,
+ name=name)
+ resp, body = self.post('roles',
+ str(Document(post_body)),
+ self.headers)
+ body = self._parse_body(etree.fromstring(body))
+ return resp, body
+
+ def get_role(self, role_id):
+ """GET a Role."""
+ resp, body = self.get('roles/%s' % str(role_id), self.headers)
+ body = self._parse_body(etree.fromstring(body))
+ return resp, body
+
+ def delete_role(self, role_id):
+ """Delete a role."""
+ resp, body = self.delete('roles/%s' % str(role_id),
+ self.headers)
+ return resp, body
+
+ def assign_user_role(self, project_id, user_id, role_id):
+ """Add roles to a user on a tenant."""
+ resp, body = self.put('projects/%s/users/%s/roles/%s' %
+ (project_id, user_id, role_id), '', self.headers)
+ return resp, body
diff --git a/tempest/services/identity/v3/xml/service_client.py b/tempest/services/identity/v3/xml/service_client.py
new file mode 100644
index 0000000..306245b
--- /dev/null
+++ b/tempest/services/identity/v3/xml/service_client.py
@@ -0,0 +1,81 @@
+# 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 urlparse import urlparse
+
+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
+
+
+XMLNS = "http://docs.openstack.org/identity/api/v3"
+
+
+class ServiceClientXML(RestClientXML):
+
+ def __init__(self, config, username, password, auth_url, tenant_name=None):
+ super(ServiceClientXML, self).__init__(config, username, password,
+ auth_url, tenant_name)
+ self.service = self.config.identity.catalog_type
+ self.endpoint_url = 'adminURL'
+
+ def _parse_array(self, node):
+ array = []
+ for child in node.getchildren():
+ array.append(xml_to_json(child))
+ return array
+
+ def _parse_body(self, body):
+ data = xml_to_json(body)
+ return data
+
+ def request(self, method, url, headers=None, body=None, wait=None):
+ """Overriding the existing HTTP request in super class rest_client."""
+ self._set_auth()
+ self.base_url = self.base_url.replace(urlparse(self.base_url).path,
+ "/v3")
+ return super(ServiceClientXML, self).request(method, url,
+ headers=headers,
+ body=body)
+
+ def update_service(self, service_id, **kwargs):
+ """Updates a service_id."""
+ resp, body = self.get_service(service_id)
+ name = kwargs.get('name', body['name'])
+ description = kwargs.get('description', body['description'])
+ type = kwargs.get('type', body['type'])
+ update_service = Element("service",
+ xmlns=XMLNS,
+ id=service_id,
+ name=name,
+ description=description,
+ type=type)
+ resp, body = self.patch('services/%s' % service_id,
+ str(Document(update_service)),
+ self.headers)
+ body = self._parse_body(etree.fromstring(body))
+ return resp, body
+
+ def get_service(self, service_id):
+ """Get Service."""
+ url = 'services/%s' % service_id
+ resp, body = self.get(url, self.headers)
+ body = self._parse_body(etree.fromstring(body))
+ return resp, body
diff --git a/tempest/services/identity/xml/identity_client.py b/tempest/services/identity/xml/identity_client.py
index 6f1b1b3..99a155a 100644
--- a/tempest/services/identity/xml/identity_client.py
+++ b/tempest/services/identity/xml/identity_client.py
@@ -172,6 +172,12 @@
body = self._parse_body(etree.fromstring(body))
return resp, body
+ def get_user(self, user_id):
+ """GET a user."""
+ resp, body = self.get("users/%s" % user_id, self.headers)
+ body = self._parse_body(etree.fromstring(body))
+ return resp, body
+
def delete_user(self, user_id):
"""Delete a user."""
resp, body = self.delete("users/%s" % user_id, self.headers)
diff --git a/tempest/services/object_storage/object_client.py b/tempest/services/object_storage/object_client.py
index 9626b6b..69df472 100644
--- a/tempest/services/object_storage/object_client.py
+++ b/tempest/services/object_storage/object_client.py
@@ -15,10 +15,10 @@
# License for the specific language governing permissions and limitations
# under the License.
-from hashlib import sha1
+import hashlib
import hmac
import httplib2
-from urlparse import urlparse
+import urlparse
from tempest.common.rest_client import RestClient
from tempest import exceptions
@@ -127,10 +127,10 @@
self._set_auth()
method = 'GET'
- path = "%s/%s/%s" % (urlparse(self.base_url).path, container,
+ path = "%s/%s/%s" % (urlparse.urlparse(self.base_url).path, container,
object_name)
hmac_body = '%s\n%s\n%s' % (method, expires, path)
- sig = hmac.new(key, hmac_body, sha1).hexdigest()
+ sig = hmac.new(key, hmac_body, hashlib.sha1).hexdigest()
url = "%s/%s?temp_url_sig=%s&temp_url_expires=%s" % (container,
object_name,
diff --git a/tempest/openstack/common/__init__.py b/tempest/services/orchestration/__init__.py
similarity index 100%
rename from tempest/openstack/common/__init__.py
rename to tempest/services/orchestration/__init__.py
diff --git a/tempest/openstack/common/__init__.py b/tempest/services/orchestration/json/__init__.py
similarity index 100%
copy from tempest/openstack/common/__init__.py
copy to tempest/services/orchestration/json/__init__.py
diff --git a/tempest/services/orchestration/json/orchestration_client.py b/tempest/services/orchestration/json/orchestration_client.py
new file mode 100644
index 0000000..81162df
--- /dev/null
+++ b/tempest/services/orchestration/json/orchestration_client.py
@@ -0,0 +1,99 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013 IBM Corp.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import json
+import time
+import urllib
+
+from tempest.common import rest_client
+from tempest import exceptions
+
+
+class OrchestrationClient(rest_client.RestClient):
+
+ def __init__(self, config, username, password, auth_url, tenant_name=None):
+ super(OrchestrationClient, self).__init__(config, username, password,
+ auth_url, tenant_name)
+ self.service = self.config.orchestration.catalog_type
+ self.build_interval = self.config.orchestration.build_interval
+ self.build_timeout = self.config.orchestration.build_timeout
+
+ def list_stacks(self, params=None):
+ """Lists all stacks for a user."""
+
+ uri = 'stacks'
+ if params:
+ uri += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(uri)
+ body = json.loads(body)
+ return resp, body
+
+ def create_stack(self, name, disable_rollback=True, parameters={},
+ timeout_mins=60, template=None, template_url=None):
+ post_body = {
+ "stack_name": name,
+ "disable_rollback": disable_rollback,
+ "parameters": parameters,
+ "timeout_mins": timeout_mins,
+ "template": "HeatTemplateFormatVersion: '2012-12-12'\n"
+ }
+ if template:
+ post_body['template'] = template
+ if template_url:
+ post_body['template_url'] = template_url
+ body = json.dumps(post_body)
+ uri = 'stacks'
+ resp, body = self.post(uri, headers=self.headers, body=body)
+ return resp, body
+
+ def get_stack(self, stack_identifier):
+ """Returns the details of a single stack."""
+ url = "stacks/%s" % stack_identifier
+ resp, body = self.get(url)
+ body = json.loads(body)
+ return resp, body['stack']
+
+ def delete_stack(self, stack_identifier):
+ """Deletes the specified Stack."""
+ return self.delete("stacks/%s" % str(stack_identifier))
+
+ def wait_for_stack_status(self, stack_identifier, status, failure_status=(
+ 'CREATE_FAILED',
+ 'DELETE_FAILED',
+ 'UPDATE_FAILED',
+ 'ROLLBACK_FAILED')):
+ """Waits for a Volume to reach a given status."""
+ stack_status = None
+ start = int(time.time())
+
+ while stack_status != status:
+ resp, body = self.get_stack(stack_identifier)
+ stack_name = body['stack_name']
+ stack_status = body['stack_status']
+ if stack_status in failure_status:
+ raise exceptions.StackBuildErrorException(
+ stack_identifier=stack_identifier,
+ stack_status=stack_status,
+ stack_status_reason=body['stack_status_reason'])
+
+ if int(time.time()) - start >= self.build_timeout:
+ message = ('Stack %s failed to reach %s status within '
+ 'the required time (%s s).' %
+ (stack_name, status, self.build_timeout))
+ raise exceptions.TimeoutException(message)
+ time.sleep(self.build_interval)
diff --git a/tempest/services/volume/json/snapshots_client.py b/tempest/services/volume/json/snapshots_client.py
index 9545d0b..db614f1 100644
--- a/tempest/services/volume/json/snapshots_client.py
+++ b/tempest/services/volume/json/snapshots_client.py
@@ -84,7 +84,7 @@
# state in a "normal" lifecycle
if (status == 'error'):
raise exceptions.SnapshotBuildErrorException(
- snapshot_id=snapshot_id)
+ snapshot_id=snapshot_id)
return status
diff --git a/tempest/services/volume/xml/snapshots_client.py b/tempest/services/volume/xml/snapshots_client.py
index 89ea89f..2209fc7 100644
--- a/tempest/services/volume/xml/snapshots_client.py
+++ b/tempest/services/volume/xml/snapshots_client.py
@@ -93,7 +93,7 @@
# state in a "normal" lifecycle
if (status == 'error'):
raise exceptions.SnapshotBuildErrorException(
- snapshot_id=snapshot_id)
+ snapshot_id=snapshot_id)
return status
diff --git a/tempest/stress/README.rst b/tempest/stress/README.rst
new file mode 100644
index 0000000..2c431ed
--- /dev/null
+++ b/tempest/stress/README.rst
@@ -0,0 +1,47 @@
+Quanta Research Cambridge OpenStack Stress Test System
+======================================================
+
+Nova is a distributed, asynchronous system that is prone to race condition
+bugs. These bugs will not be easily found during
+functional testing but will be encountered by users in large deployments in a
+way that is hard to debug. The stress test tries to cause these bugs to happen
+in a more controlled environment.
+
+
+Environment
+------------
+This particular framework assumes your working Nova cluster understands Nova
+API 2.0. The stress tests can read the logs from the cluster. To enable this
+you have to provide the hostname to call 'nova-manage' and
+the private key and user name for ssh to the cluster in the
+[stress] section of tempest.conf. You also need to provide the
+location of the log files:
+
+ target_logfiles = "regexp to all log files to be checked for errors"
+ target_private_key_path = "private ssh key for controller and log file nodes"
+ target_ssh_user = "username for controller and log file nodes"
+ target_controller = "hostname or ip of controller node (for nova-manage)
+ log_check_interval = "time between checking logs for errors (default 60s)"
+
+
+
+Running the sample test
+-----------------------
+
+To test installation, do the following (from the tempest/stress directory):
+
+ ./run_stress.py etc/sample-test.json -d 30
+
+This sample test tries to create a few VMs and kill a few VMs.
+
+
+Additional Tools
+----------------
+
+Sometimes the tests don't finish, or there are failures. In these
+cases, you may want to clean out the nova cluster. We have provided
+some scripts to do this in the ``tools`` subdirectory.
+You can use the following script to destroy any keypairs,
+floating ips, and servers:
+
+tempest/stress/tools/cleanup.py
diff --git a/stress/__init__.py b/tempest/stress/__init__.py
similarity index 75%
copy from stress/__init__.py
copy to tempest/stress/__init__.py
index 0875e0b..1caf74a 100644
--- a/stress/__init__.py
+++ b/tempest/stress/__init__.py
@@ -1,4 +1,4 @@
-# Copyright 2011 Quanta Research Cambridge, Inc.
+# Copyright 2013 Quanta Research Cambridge, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -11,7 +11,3 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
-"""Basic framework for constructing various simulated workloads for a
-nova cluster."""
-
-__author__ = "David Kranz and Eugene Shih"
diff --git a/stress/__init__.py b/tempest/stress/actions/__init__.py
similarity index 75%
copy from stress/__init__.py
copy to tempest/stress/actions/__init__.py
index 0875e0b..1caf74a 100644
--- a/stress/__init__.py
+++ b/tempest/stress/actions/__init__.py
@@ -1,4 +1,4 @@
-# Copyright 2011 Quanta Research Cambridge, Inc.
+# Copyright 2013 Quanta Research Cambridge, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -11,7 +11,3 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
-"""Basic framework for constructing various simulated workloads for a
-nova cluster."""
-
-__author__ = "David Kranz and Eugene Shih"
diff --git a/tempest/stress/actions/create_destroy_server.py b/tempest/stress/actions/create_destroy_server.py
new file mode 100644
index 0000000..44b149f
--- /dev/null
+++ b/tempest/stress/actions/create_destroy_server.py
@@ -0,0 +1,34 @@
+# Copyright 2013 Quanta Research Cambridge, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from tempest.common.utils.data_utils import rand_name
+
+
+def create_destroy(manager, logger):
+ image = manager.config.compute.image_ref
+ flavor = manager.config.compute.flavor_ref
+ while True:
+ name = rand_name("instance")
+ logger.info("creating %s" % name)
+ resp, server = manager.servers_client.create_server(
+ name, image, flavor)
+ server_id = server['id']
+ assert(resp.status == 202)
+ manager.servers_client.wait_for_server_status(server_id, 'ACTIVE')
+ logger.info("created %s" % server_id)
+ logger.info("deleting %s" % name)
+ resp, _ = manager.servers_client.delete_server(server_id)
+ assert(resp.status == 204)
+ manager.servers_client.wait_for_server_termination(server_id)
+ logger.info("deleted %s" % server_id)
diff --git a/tempest/stress/cleanup.py b/tempest/stress/cleanup.py
new file mode 100644
index 0000000..b2cb70a
--- /dev/null
+++ b/tempest/stress/cleanup.py
@@ -0,0 +1,60 @@
+#!/usr/bin/env python
+
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Quanta Research Cambridge, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from tempest import clients
+
+
+def cleanup():
+ admin_manager = clients.AdminManager()
+
+ _, body = admin_manager.servers_client.list_servers({"all_tenants": True})
+ for s in body['servers']:
+ try:
+ admin_manager.servers_client.delete_server(s['id'])
+ except Exception:
+ pass
+
+ for s in body['servers']:
+ try:
+ admin_manager.servers_client.wait_for_server_termination(s['id'])
+ except Exception:
+ pass
+
+ _, keypairs = admin_manager.keypairs_client.list_keypairs()
+ for k in keypairs:
+ try:
+ admin_manager.keypairs_client.delete_keypair(k['name'])
+ except Exception:
+ pass
+
+ _, floating_ips = admin_manager.floating_ips_client.list_floating_ips()
+ for f in floating_ips:
+ try:
+ admin_manager.floating_ips_client.delete_floating_ip(f['id'])
+ except Exception:
+ pass
+
+ _, users = admin_manager.identity_client.get_users()
+ for user in users:
+ if user['name'].startswith("stress_user"):
+ admin_manager.identity_client.delete_user(user['id'])
+
+ _, tenants = admin_manager.identity_client.list_tenants()
+ for tenant in tenants:
+ if tenant['name'].startswith("stress_tenant"):
+ admin_manager.identity_client.delete_tenant(tenant['id'])
diff --git a/tempest/stress/driver.py b/tempest/stress/driver.py
new file mode 100644
index 0000000..51f159d
--- /dev/null
+++ b/tempest/stress/driver.py
@@ -0,0 +1,156 @@
+# Copyright 2013 Quanta Research Cambridge, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import importlib
+import logging
+import multiprocessing
+import time
+
+from tempest import clients
+from tempest.common import ssh
+from tempest.common.utils.data_utils import rand_name
+from tempest import exceptions
+from tempest.stress import cleanup
+
+admin_manager = clients.AdminManager()
+
+# setup logging to file
+logging.basicConfig(
+ format='%(asctime)s %(process)d %(name)-20s %(levelname)-8s %(message)s',
+ datefmt='%m-%d %H:%M:%S',
+ filename="stress.debug.log",
+ filemode="w",
+ level=logging.DEBUG,
+)
+
+# define a Handler which writes INFO messages or higher to the sys.stdout
+_console = logging.StreamHandler()
+_console.setLevel(logging.INFO)
+# set a format which is simpler for console use
+format_str = '%(asctime)s %(process)d %(name)-20s: %(levelname)-8s %(message)s'
+_formatter = logging.Formatter(format_str)
+# tell the handler to use this format
+_console.setFormatter(_formatter)
+# add the handler to the root logger
+logger = logging.getLogger('tempest.stress')
+logger.addHandler(_console)
+
+
+def do_ssh(command, host):
+ username = admin_manager.config.stress.target_ssh_user
+ key_filename = admin_manager.config.stress.target_private_key_path
+ if not (username and key_filename):
+ return None
+ ssh_client = ssh.Client(host, username, key_filename=key_filename)
+ try:
+ return ssh_client.exec_command(command)
+ except exceptions.SSHExecCommandFailed:
+ return None
+
+
+def _get_compute_nodes(controller):
+ """
+ Returns a list of active compute nodes. List is generated by running
+ nova-manage on the controller.
+ """
+ nodes = []
+ cmd = "nova-manage service list | grep ^nova-compute"
+ output = do_ssh(cmd, controller)
+ if not output:
+ return nodes
+ # For example: nova-compute xg11eth0 nova enabled :-) 2011-10-31 18:57:46
+ # This is fragile but there is, at present, no other way to get this info.
+ for line in output.split('\n'):
+ words = line.split()
+ if len(words) > 0 and words[4] == ":-)":
+ nodes.append(words[1])
+ return nodes
+
+
+def _error_in_logs(logfiles, nodes):
+ """
+ Detect errors in the nova log files on the controller and compute nodes.
+ """
+ grep = 'egrep "ERROR|TRACE" %s' % logfiles
+ for node in nodes:
+ errors = do_ssh(grep, node)
+ if not errors:
+ return None
+ if len(errors) > 0:
+ logger.error('%s: %s' % (node, errors))
+ return errors
+ return None
+
+
+def get_action_function(path):
+ (module_part, _, function) = path.rpartition('.')
+ return getattr(importlib.import_module(module_part), function)
+
+
+def stress_openstack(tests, duration):
+ """
+ Workload driver. Executes an action function against a nova-cluster.
+
+ """
+ logfiles = admin_manager.config.stress.target_logfiles
+ log_check_interval = int(admin_manager.config.stress.log_check_interval)
+ if logfiles:
+ controller = admin_manager.config.stress.target_controller
+ computes = _get_compute_nodes(controller)
+ for node in computes:
+ do_ssh("rm -f %s" % logfiles, node)
+ processes = []
+ for test in tests:
+ if test.get('use_admin', False):
+ manager = admin_manager
+ else:
+ manager = clients.Manager()
+ for _ in xrange(test.get('threads', 1)):
+ if test.get('use_isolated_tenants', False):
+ username = rand_name("stress_user")
+ tenant_name = rand_name("stress_tenant")
+ password = "pass"
+ identity_client = admin_manager.identity_client
+ _, tenant = identity_client.create_tenant(name=tenant_name)
+ identity_client.create_user(username,
+ password,
+ tenant['id'],
+ "email")
+ manager = clients.Manager(username=username,
+ password="pass",
+ tenant_name=tenant_name)
+ target = get_action_function(test['action'])
+ p = multiprocessing.Process(target=target,
+ args=(manager, logger),
+ kwargs=test.get('kwargs', {}))
+ processes.append(p)
+ p.start()
+ end_time = time.time() + duration
+ had_errors = False
+ while True:
+ remaining = end_time - time.time()
+ if remaining <= 0:
+ break
+ time.sleep(min(remaining, log_check_interval))
+ if not logfiles:
+ continue
+ errors = _error_in_logs(logfiles, computes)
+ if errors:
+ had_errors = True
+ break
+ for p in processes:
+ p.terminate()
+ if not had_errors:
+ logger.info("cleaning up")
+ cleanup.cleanup()
diff --git a/tempest/stress/etc/sample-test.json b/tempest/stress/etc/sample-test.json
new file mode 100644
index 0000000..5a0189c
--- /dev/null
+++ b/tempest/stress/etc/sample-test.json
@@ -0,0 +1,7 @@
+[{"action": "tempest.stress.actions.create_destroy_server.create_destroy",
+ "threads": 8,
+ "use_admin": false,
+ "use_isolated_tenants": false,
+ "kwargs": {}
+ }
+]
diff --git a/tempest/stress/run_stress.py b/tempest/stress/run_stress.py
new file mode 100755
index 0000000..ef0ec8e
--- /dev/null
+++ b/tempest/stress/run_stress.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python
+
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Quanta Research Cambridge, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import json
+
+from tempest.stress import driver
+
+
+def main(ns):
+ tests = json.load(open(ns.tests, 'r'))
+ driver.stress_openstack(tests, ns.duration)
+
+
+parser = argparse.ArgumentParser(description='Run stress tests. ')
+parser.add_argument('-d', '--duration', default=300, type=int,
+ help="Duration of test.")
+parser.add_argument('tests', help="Name of the file with test description.")
+main(parser.parse_args())
diff --git a/stress/__init__.py b/tempest/stress/tools/cleanup.py
old mode 100644
new mode 100755
similarity index 75%
rename from stress/__init__.py
rename to tempest/stress/tools/cleanup.py
index 0875e0b..7139d6c
--- a/stress/__init__.py
+++ b/tempest/stress/tools/cleanup.py
@@ -1,4 +1,6 @@
-# Copyright 2011 Quanta Research Cambridge, Inc.
+#!/usr/bin/env python
+
+# Copyright 2013 Quanta Research Cambridge, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -11,7 +13,8 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
-"""Basic framework for constructing various simulated workloads for a
-nova cluster."""
-__author__ = "David Kranz and Eugene Shih"
+from tempest.stress import cleanup
+
+
+cleanup.cleanup()
diff --git a/tempest/test.py b/tempest/test.py
index ccb2251..9d6c2d3 100644
--- a/tempest/test.py
+++ b/tempest/test.py
@@ -37,13 +37,12 @@
"""
def decorator(f):
- testtool_attributes = ('smoke')
-
- if 'type' in kwargs and kwargs['type'] in testtool_attributes:
- return nose.plugins.attrib.attr(*args, **kwargs)(
- testtools.testcase.attr(kwargs['type'])(f))
- else:
- return nose.plugins.attrib.attr(*args, **kwargs)(f)
+ if 'type' in kwargs and isinstance(kwargs['type'], str):
+ f = testtools.testcase.attr(kwargs['type'])(f)
+ elif 'type' in kwargs and isinstance(kwargs['type'], list):
+ for attr in kwargs['type']:
+ f = testtools.testcase.attr(attr)(f)
+ return nose.plugins.attrib.attr(*args, **kwargs)(f)
return decorator
@@ -51,10 +50,36 @@
class BaseTestCase(testtools.TestCase,
testtools.testcase.WithAttributes,
testresources.ResourcedTestCase):
- def __init__(self, *args, **kwargs):
- super(BaseTestCase, self).__init__(*args, **kwargs)
- #NOTE(afazekas): inspection workaround
- BaseTestCase.config = config.TempestConfig()
+
+ config = config.TempestConfig()
+
+ @classmethod
+ def setUpClass(cls):
+ if hasattr(super(BaseTestCase, cls), 'setUpClass'):
+ super(BaseTestCase, cls).setUpClass()
+
+
+def call_until_true(func, duration, sleep_for):
+ """
+ Call the given function until it returns True (and return True) or
+ until the specified duration (in seconds) elapses (and return
+ False).
+
+ :param func: A zero argument callable that returns True on success.
+ :param duration: The number of seconds for which to attempt a
+ successful call of the function.
+ :param sleep_for: The number of seconds to sleep after an unsuccessful
+ invocation of the function.
+ """
+ now = time.time()
+ timeout = now + duration
+ while now < timeout:
+ if func():
+ return True
+ LOG.debug("Sleeping for %d seconds", sleep_for)
+ time.sleep(sleep_for)
+ now = time.time()
+ return False
class TestCase(BaseTestCase):
@@ -91,58 +116,34 @@
self.os_resources.remove(thing)
del self.resource_keys[key]
-
-def call_until_true(func, duration, sleep_for):
- """
- Call the given function until it returns True (and return True) or
- until the specified duration (in seconds) elapses (and return
- False).
-
- :param func: A zero argument callable that returns True on success.
- :param duration: The number of seconds for which to attempt a successful
- call of the function.
- :param sleep_for: The number of seconds to sleep after an unsuccessful
- invocation of the function.
- """
- now = time.time()
- timeout = now + duration
- while now < timeout:
- if func():
- return True
- LOG.debug("Sleeping for %d seconds", sleep_for)
- time.sleep(sleep_for)
- now = time.time()
- return False
-
-
-def status_timeout(things, thing_id, expected_status):
- """
- Given a thing and an expected status, do a loop, sleeping
- for a configurable amount of time, checking for the
- expected status to show. At any time, if the returned
- status of the thing is ERROR, fail out.
- """
- def check_status():
- # python-novaclient has resources available to its client
- # that all implement a get() method taking an identifier
- # for the singular resource to retrieve.
- thing = things.get(thing_id)
- new_status = thing.status
- if new_status == 'ERROR':
- self.fail("%s failed to get to expected status."
- "In ERROR state."
- % thing)
- elif new_status == expected_status:
- return True # All good.
- LOG.debug("Waiting for %s to get to %s status. "
- "Currently in %s status",
- thing, expected_status, new_status)
- conf = config.TempestConfig()
- if not call_until_true(check_status,
- conf.compute.build_timeout,
- conf.compute.build_interval):
- self.fail("Timed out waiting for thing %s to become %s"
- % (thing_id, expected_status))
+ def status_timeout(self, things, thing_id, expected_status):
+ """
+ Given a thing and an expected status, do a loop, sleeping
+ for a configurable amount of time, checking for the
+ expected status to show. At any time, if the returned
+ status of the thing is ERROR, fail out.
+ """
+ def check_status():
+ # python-novaclient has resources available to its client
+ # that all implement a get() method taking an identifier
+ # for the singular resource to retrieve.
+ thing = things.get(thing_id)
+ new_status = thing.status
+ if new_status == 'ERROR':
+ self.fail("%s failed to get to expected status."
+ "In ERROR state."
+ % thing)
+ elif new_status == expected_status:
+ return True # All good.
+ LOG.debug("Waiting for %s to get to %s status. "
+ "Currently in %s status",
+ thing, expected_status, new_status)
+ conf = config.TempestConfig()
+ if not call_until_true(check_status,
+ conf.compute.build_timeout,
+ conf.compute.build_interval):
+ self.fail("Timed out waiting for thing %s to become %s"
+ % (thing_id, expected_status))
class DefaultClientSmokeTest(TestCase):
diff --git a/tempest/testboto.py b/tempest/testboto.py
index cee8843..9e652cb 100644
--- a/tempest/testboto.py
+++ b/tempest/testboto.py
@@ -15,7 +15,7 @@
# License for the specific language governing permissions and limitations
# under the License.
-from contextlib import closing
+import contextlib
import logging
import os
import re
@@ -282,9 +282,10 @@
@classmethod
def get_lfunction_gone(cls, obj):
- """ If the object is instance of a well know type returns back with
+ """If the object is instance of a well know type returns back with
with the correspoding function otherwise it assumes the obj itself
- is the function"""
+ is the function.
+ """
ec = cls.ec2_error_code
if isinstance(obj, ec2.instance.Instance):
colusure_matcher = ec.client.InvalidInstanceID.NotFound
@@ -412,7 +413,8 @@
"""Destroys the bucket and its content, just for teardown."""
exc_num = 0
try:
- with closing(boto.connect_s3(**connection_data)) as conn:
+ with contextlib.closing(
+ boto.connect_s3(**connection_data)) as conn:
if isinstance(bucket, basestring):
bucket = conn.lookup(bucket)
assert isinstance(bucket, s3.bucket.Bucket)
@@ -442,7 +444,7 @@
return "_GONE"
except exception.EC2ResponseError as exc:
if cls.ec2_error_code.\
- client.InvalidInstanceID.NotFound.match(exc):
+ client.InvalidInstanceID.NotFound.match(exc):
return "_GONE"
#NOTE(afazekas): incorrect code,
# but the resource must be destoreyd
diff --git a/tempest/tests/boto/test_ec2_instance_run.py b/tempest/tests/boto/test_ec2_instance_run.py
index 3293dea..b6b93d8 100644
--- a/tempest/tests/boto/test_ec2_instance_run.py
+++ b/tempest/tests/boto/test_ec2_instance_run.py
@@ -73,8 +73,8 @@
"location": cls.bucket_name + "/" + ari_manifest}}
for image in cls.images.itervalues():
image["image_id"] = cls.ec2_client.register_image(
- name=image["name"],
- image_location=image["location"])
+ name=image["name"],
+ image_location=image["location"])
cls.addResourceCleanUp(cls.ec2_client.deregister_image,
image["image_id"])
@@ -151,13 +151,15 @@
group_desc)
self.addResourceCleanUp(self.destroy_security_group_wait,
security_group)
- self.assertTrue(self.ec2_client.authorize_security_group(
+ self.assertTrue(
+ self.ec2_client.authorize_security_group(
sec_group_name,
ip_protocol="icmp",
cidr_ip="0.0.0.0/0",
from_port=-1,
to_port=-1))
- self.assertTrue(self.ec2_client.authorize_security_group(
+ self.assertTrue(
+ self.ec2_client.authorize_security_group(
sec_group_name,
ip_protocol="tcp",
cidr_ip="0.0.0.0/0",
@@ -200,14 +202,13 @@
re_search_wait(_output, text)
part_lines = ssh.get_partitions().split('\n')
- # "attaching" invalid EC2 state ! #1074901
volume.attach(instance.id, "/dev/vdh")
def _volume_state():
volume.update(validate=True)
return volume.status
- #self.assertVolumeStatusWait(_volume_state, "in-use") # #1074901
+ self.assertVolumeStatusWait(_volume_state, "in-use")
re_search_wait(_volume_state, "in-use")
#NOTE(afazekas): Different Hypervisor backends names
@@ -227,9 +228,9 @@
#TODO(afazekas): Resource compare to the flavor settings
- volume.detach() # "detaching" invalid EC2 status #1074901
+ volume.detach()
- #self.assertVolumeStatusWait(_volume_state, "available")
+ self.assertVolumeStatusWait(_volume_state, "available")
re_search_wait(_volume_state, "available")
LOG.info("Volume %s state: %s", volume.id, volume.status)
diff --git a/tempest/tests/boto/test_ec2_volumes.py b/tempest/tests/boto/test_ec2_volumes.py
index dc8ff31..37a913e 100644
--- a/tempest/tests/boto/test_ec2_volumes.py
+++ b/tempest/tests/boto/test_ec2_volumes.py
@@ -39,7 +39,6 @@
cls.client = cls.os.ec2api_client
cls.zone = cls.client.get_good_zone()
-#NOTE(afazekas): as admin it can trigger the Bug #1074901
@attr(type='smoke')
def test_create_get_delete(self):
# EC2 Create, get, delete Volume
diff --git a/tempest/tests/boto/test_s3_ec2_images.py b/tempest/tests/boto/test_s3_ec2_images.py
index 4068aba..f77743e 100644
--- a/tempest/tests/boto/test_s3_ec2_images.py
+++ b/tempest/tests/boto/test_s3_ec2_images.py
@@ -63,12 +63,12 @@
"location": self.bucket_name + "/" + self.ami_manifest,
"type": "ami"}
image["image_id"] = self.images_client.register_image(
- name=image["name"],
- image_location=image["location"])
+ name=image["name"],
+ image_location=image["location"])
#Note(afazekas): delete_snapshot=True might trigger boto lib? bug
image["cleanUp"] = self.addResourceCleanUp(
- self.images_client.deregister_image,
- image["image_id"])
+ self.images_client.deregister_image,
+ image["image_id"])
self.assertEqual(image["image_id"][0:3], image["type"])
retrieved_image = self.images_client.get_image(image["image_id"])
self.assertTrue(retrieved_image.name == image["name"])
@@ -90,11 +90,11 @@
"location": self.bucket_name + "/" + self.ari_manifest,
"type": "aki"}
image["image_id"] = self.images_client.register_image(
- name=image["name"],
- image_location=image["location"])
+ name=image["name"],
+ image_location=image["location"])
image["cleanUp"] = self.addResourceCleanUp(
- self.images_client.deregister_image,
- image["image_id"])
+ self.images_client.deregister_image,
+ image["image_id"])
self.assertEqual(image["image_id"][0:3], image["type"])
retrieved_image = self.images_client.get_image(image["image_id"])
self.assertTrue(retrieved_image.name == image["name"])
@@ -115,11 +115,11 @@
"location": "/" + self.bucket_name + "/" + self.ari_manifest,
"type": "ari"}
image["image_id"] = self.images_client.register_image(
- name=image["name"],
- image_location=image["location"])
+ name=image["name"],
+ image_location=image["location"])
image["cleanUp"] = self.addResourceCleanUp(
- self.images_client.deregister_image,
- image["image_id"])
+ self.images_client.deregister_image,
+ image["image_id"])
self.assertEqual(image["image_id"][0:3], image["type"])
retrieved_image = self.images_client.get_image(image["image_id"])
self.assertIn(retrieved_image.state, self.valid_image_state)
diff --git a/tempest/tests/boto/test_s3_objects.py b/tempest/tests/boto/test_s3_objects.py
index dcb7c86..9d4d79c 100644
--- a/tempest/tests/boto/test_s3_objects.py
+++ b/tempest/tests/boto/test_s3_objects.py
@@ -15,9 +15,9 @@
# License for the specific language governing permissions and limitations
# under the License.
-from contextlib import closing
+import contextlib
-from boto.s3.key import Key
+import boto.s3.key
from tempest import clients
from tempest.common.utils.data_utils import rand_name
@@ -46,7 +46,7 @@
bucket_name)
self.assertTrue(bucket.name == bucket_name)
- with closing(Key(bucket)) as key:
+ with contextlib.closing(boto.s3.key.Key(bucket)) as key:
key.key = object_name
key.set_contents_from_string(content)
readback = key.get_contents_as_string()
diff --git a/tempest/tests/boto/utils/s3.py b/tempest/tests/boto/utils/s3.py
index 4c3229b..ea9869b 100644
--- a/tempest/tests/boto/utils/s3.py
+++ b/tempest/tests/boto/utils/s3.py
@@ -15,24 +15,24 @@
# License for the specific language governing permissions and limitations
# under the License.
-from contextlib import closing
+import contextlib
import logging
import os
import re
import boto
-from boto.s3.key import Key
+import boto.s3.key
LOG = logging.getLogger(__name__)
def s3_upload_dir(bucket, path, prefix="", connection_data=None):
if isinstance(bucket, basestring):
- with closing(boto.connect_s3(**connection_data)) as conn:
+ with contextlib.closing(boto.connect_s3(**connection_data)) as conn:
bucket = conn.lookup(bucket)
for root, dirs, files in os.walk(path):
for fil in files:
- with closing(Key(bucket)) as key:
+ with contextlib.closing(boto.s3.key.Key(bucket)) as key:
source = root + os.sep + fil
target = re.sub("^" + re.escape(path) + "?/", prefix, source)
if os.sep != '/':
diff --git a/tempest/tests/boto/utils/wait.py b/tempest/tests/boto/utils/wait.py
index c2d4ea3..6cd17a9 100644
--- a/tempest/tests/boto/utils/wait.py
+++ b/tempest/tests/boto/utils/wait.py
@@ -19,7 +19,7 @@
import re
import time
-from boto.exception import BotoServerError
+import boto.exception
from testtools import TestCase
import tempest.config
@@ -87,7 +87,7 @@
"""Stops waiting on success."""
start_time = time.time()
if exc_matcher is not None:
- exc_class = BotoServerError
+ exc_class = boto.exception.BotoServerError
if exc_class is None:
exc_class = BaseException
diff --git a/tempest/tests/compute/admin/test_aggregates.py b/tempest/tests/compute/admin/test_aggregates.py
index 06acc41..07df77f 100644
--- a/tempest/tests/compute/admin/test_aggregates.py
+++ b/tempest/tests/compute/admin/test_aggregates.py
@@ -27,13 +27,14 @@
Tests Aggregates API that require admin privileges
"""
+ _host_key = 'OS-EXT-SRV-ATTR:host'
_interface = 'json'
@classmethod
def setUpClass(cls):
super(AggregatesAdminTestJSON, cls).setUpClass()
cls.client = cls.os_adm.aggregates_client
- cls.user_client = cls.os.aggregates_client
+ cls.user_client = cls.aggregates_client
cls.aggregate_name_prefix = 'test_aggregate_'
cls.az_name_prefix = 'test_az_'
@@ -212,7 +213,7 @@
availability_zone=az_name)
servers_client.wait_for_server_status(server['id'], 'ACTIVE')
resp, body = admin_servers_client.get_server(server['id'])
- self.assertEqual(self.host, body['OS-EXT-SRV-ATTR:host'])
+ self.assertEqual(self.host, body[self._host_key])
@attr(type='negative')
def test_aggregate_add_non_exist_host(self):
@@ -254,3 +255,9 @@
self.assertRaises(exceptions.Unauthorized,
self.user_client.remove_host,
aggregate['id'], self.host)
+
+
+class AggregatesAdminTestXML(AggregatesAdminTestJSON):
+ _host_key = (
+ '{http://docs.openstack.org/compute/ext/extended_status/api/v1.1}host')
+ _interface = 'xml'
diff --git a/tempest/tests/compute/admin/test_availability_zone.py b/tempest/tests/compute/admin/test_availability_zone.py
new file mode 100644
index 0000000..98ad49c
--- /dev/null
+++ b/tempest/tests/compute/admin/test_availability_zone.py
@@ -0,0 +1,69 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 NEC Corporation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from tempest import exceptions
+from tempest.test import attr
+from tempest.tests.compute import base
+
+
+class AvailabilityZoneAdminTestJSON(base.BaseComputeAdminTest):
+
+ """
+ Tests Availability Zone API List that require admin privileges
+ """
+
+ _interface = 'json'
+
+ @classmethod
+ def setUpClass(cls):
+ super(AvailabilityZoneAdminTestJSON, cls).setUpClass()
+ cls.client = cls.os_adm.availability_zone_client
+ cls.non_adm_client = cls.availability_zone_client
+
+ @attr('positive')
+ def test_get_availability_zone_list(self):
+ # List of availability zone
+ resp, availability_zone = self.client.get_availability_zone_list()
+ self.assertEqual(200, resp.status)
+ self.assertTrue(len(availability_zone) > 0)
+
+ @attr('positive')
+ def test_get_availability_zone_list_detail(self):
+ # List of availability zones and available services
+ resp, availability_zone = \
+ self.client.get_availability_zone_list_detail()
+ self.assertEqual(200, resp.status)
+ self.assertTrue(len(availability_zone) > 0)
+
+ @attr('positive')
+ def test_get_availability_zone_list_with_non_admin_user(self):
+ # List of availability zone with non admin user
+ resp, availability_zone = \
+ self.non_adm_client.get_availability_zone_list()
+ self.assertEqual(200, resp.status)
+ self.assertTrue(len(availability_zone) > 0)
+
+ @attr('negative')
+ def test_get_availability_zone_list_detail_with_non_admin_user(self):
+ # List of availability zones and available services with non admin user
+ self.assertRaises(
+ exceptions.Unauthorized,
+ self.non_adm_client.get_availability_zone_list_detail)
+
+
+class AvailabilityZoneAdminTestXML(AvailabilityZoneAdminTestJSON):
+ _interface = 'xml'
diff --git a/tempest/tests/compute/admin/test_flavors_extra_specs.py b/tempest/tests/compute/admin/test_flavors_extra_specs.py
index 01bff98..31a2511 100644
--- a/tempest/tests/compute/admin/test_flavors_extra_specs.py
+++ b/tempest/tests/compute/admin/test_flavors_extra_specs.py
@@ -92,9 +92,9 @@
def test_flavor_non_admin_get_keys(self):
specs = {"key1": "value1", "key2": "value2"}
set_resp, set_body = self.client.set_flavor_extra_spec(
- self.flavor['id'], specs)
+ self.flavor['id'], specs)
resp, body = self.flavors_client.get_flavor_extra_spec(
- self.flavor['id'])
+ self.flavor['id'])
self.assertEqual(resp.status, 200)
for key in specs:
self.assertEquals(body[key], specs[key])
@@ -103,7 +103,7 @@
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.flavor['id'], specs)
self.assertRaises(exceptions.Unauthorized,
self.flavors_client.unset_flavor_extra_spec,
diff --git a/tempest/tests/compute/admin/test_services.py b/tempest/tests/compute/admin/test_services.py
new file mode 100644
index 0000000..0577164
--- /dev/null
+++ b/tempest/tests/compute/admin/test_services.py
@@ -0,0 +1,52 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 NEC Corporation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from tempest import exceptions
+from tempest.test import attr
+from tempest.tests.compute import base
+
+
+class ServicesAdminTestJSON(base.BaseComputeAdminTest):
+
+ """
+ Tests Services API. List and Enable/Disable require admin privileges.
+ """
+
+ _interface = 'json'
+
+ @classmethod
+ def setUpClass(cls):
+ super(ServicesAdminTestJSON, cls).setUpClass()
+ cls.client = cls.os_adm.services_client
+ cls.non_admin_client = cls.services_client
+
+ @attr(type='positive')
+ def test_list_services(self):
+ # List Compute services
+ resp, services = self.client.list_services()
+ self.assertEqual(200, resp.status)
+ self.assertTrue(len(services) >= 2)
+
+ @attr(type='negative')
+ def test_list_services_with_non_admin_user(self):
+ # List Compute service with non admin user
+ self.assertRaises(exceptions.Unauthorized,
+ self.non_admin_client.list_services)
+
+
+class ServicesAdminTestXML(ServicesAdminTestJSON):
+ _interface = 'xml'
diff --git a/tempest/tests/compute/base.py b/tempest/tests/compute/base.py
index 7716922..fbefe35 100644
--- a/tempest/tests/compute/base.py
+++ b/tempest/tests/compute/base.py
@@ -61,6 +61,9 @@
cls.volumes_client = os.volumes_client
cls.interfaces_client = os.interfaces_client
cls.fixed_ips_client = os.fixed_ips_client
+ cls.availability_zone_client = os.availability_zone_client
+ cls.aggregates_client = os.aggregates_client
+ cls.services_client = os.services_client
cls.build_interval = cls.config.compute.build_interval
cls.build_timeout = cls.config.compute.build_timeout
cls.ssh_user = cls.config.compute.ssh_user
diff --git a/tempest/tests/compute/floating_ips/test_floating_ips_actions.py b/tempest/tests/compute/floating_ips/test_floating_ips_actions.py
index 2b21710..5fe911f 100644
--- a/tempest/tests/compute/floating_ips/test_floating_ips_actions.py
+++ b/tempest/tests/compute/floating_ips/test_floating_ips_actions.py
@@ -53,8 +53,8 @@
@classmethod
def tearDownClass(cls):
#Deleting the floating IP which is created in this method
- super(FloatingIPsTestJSON, cls).tearDownClass()
resp, body = cls.client.delete_floating_ip(cls.floating_ip_id)
+ super(FloatingIPsTestJSON, cls).tearDownClass()
@attr(type='positive')
def test_allocate_floating_ip(self):
@@ -102,14 +102,14 @@
# to a specific server should be successful
#Association of floating IP to fixed IP address
- resp, body =\
- self.client.associate_floating_ip_to_server(self.floating_ip,
- self.server_id)
+ resp, body = self.client.associate_floating_ip_to_server(
+ self.floating_ip,
+ self.server_id)
self.assertEqual(202, resp.status)
#Disassociation of floating IP that was associated in this method
- resp, body = \
- self.client.disassociate_floating_ip_from_server(self.floating_ip,
- self.server_id)
+ resp, body = self.client.disassociate_floating_ip_from_server(
+ self.floating_ip,
+ self.server_id)
self.assertEqual(202, resp.status)
@attr(type='negative')
@@ -150,13 +150,13 @@
self.new_server_id = body['id']
#Associating floating IP for the first time
- resp, _ = \
- self.client.associate_floating_ip_to_server(self.floating_ip,
- self.server_id)
+ resp, _ = self.client.associate_floating_ip_to_server(
+ self.floating_ip,
+ self.server_id)
#Associating floating IP for the second time
- resp, body = \
- self.client.associate_floating_ip_to_server(self.floating_ip,
- self.new_server_id)
+ resp, body = self.client.associate_floating_ip_to_server(
+ self.floating_ip,
+ self.new_server_id)
self.addCleanup(self.servers_client.delete_server, self.new_server_id)
if (resp['status'] is not None):
diff --git a/tempest/tests/compute/images/test_images_oneserver.py b/tempest/tests/compute/images/test_images_oneserver.py
index ca3dbb5..dfc16f4 100644
--- a/tempest/tests/compute/images/test_images_oneserver.py
+++ b/tempest/tests/compute/images/test_images_oneserver.py
@@ -72,7 +72,6 @@
snapshot_name)
@attr(type='negative')
- @testtools.skip("Until Bug #1005423 is fixed")
def test_create_image_specify_invalid_metadata(self):
# Return an error when creating image with invalid metadata
snapshot_name = rand_name('test-snap-')
@@ -81,12 +80,11 @@
self.server['id'], snapshot_name, meta)
@attr(type='negative')
- @testtools.skip("Until Bug #1005423 is fixed")
def test_create_image_specify_metadata_over_limits(self):
# Return an error when creating image with meta data over 256 chars
snapshot_name = rand_name('test-snap-')
meta = {'a' * 260: 'b' * 260}
- self.assertRaises(exceptions.OverLimit, self.client.create_image,
+ self.assertRaises(exceptions.BadRequest, self.client.create_image,
self.server['id'], snapshot_name, meta)
@attr(type='negative')
@@ -130,6 +128,11 @@
self.assertEqual(original_image['minRam'], image['minRam'])
self.assertEqual(original_image['minDisk'], image['minDisk'])
+ # Verify the image was deleted correctly
+ resp, body = self.client.delete_image(image_id)
+ self.assertEqual('204', resp['status'])
+ self.assertRaises(exceptions.NotFound, self.client.get_image, image_id)
+
@attr(type='negative')
@testtools.skipUnless(compute.MULTI_USER,
'Need multiple users for this test.')
diff --git a/tempest/tests/compute/images/test_images_whitebox.py b/tempest/tests/compute/images/test_images_whitebox.py
index 105a38a..9ec05dd 100644
--- a/tempest/tests/compute/images/test_images_whitebox.py
+++ b/tempest/tests/compute/images/test_images_whitebox.py
@@ -37,10 +37,10 @@
@classmethod
def tearDownClass(cls):
"""Delete images after a test is executed."""
- super(ImagesWhiteboxTest, cls).tearDownClass()
for image_id in cls.image_ids:
cls.client.delete_image(image_id)
cls.image_ids.remove(image_id)
+ super(ImagesWhiteboxTest, cls).tearDownClass()
@classmethod
def update_state(self, server_id, vm_state, task_state, deleted=0):
diff --git a/tempest/tests/compute/limits/test_absolute_limits.py b/tempest/tests/compute/limits/test_absolute_limits.py
index 2b31680..6933fd7 100644
--- a/tempest/tests/compute/limits/test_absolute_limits.py
+++ b/tempest/tests/compute/limits/test_absolute_limits.py
@@ -15,6 +15,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+from tempest import exceptions
+from tempest.test import attr
from tempest.tests.compute import base
@@ -25,6 +27,7 @@
def setUpClass(cls):
super(AbsoluteLimitsTestJSON, cls).setUpClass()
cls.client = cls.limits_client
+ cls.server_client = cls.servers_client
def test_absLimits_get(self):
# To check if all limits are present in the response
@@ -45,6 +48,24 @@
"Failed to find element %s in absolute limits list"
% ', '.join(ele for ele in missing_elements))
+ @attr(type='negative')
+ def test_max_image_meta_exceed_limit(self):
+ #We should not create vm with image meta over maxImageMeta limit
+ # Get max limit value
+ max_meta = self.client.get_specific_absolute_limit('maxImageMeta')
+
+ #Create server should fail, since we are passing > metadata Limit!
+ max_meta_data = int(max_meta) + 1
+
+ meta_data = {}
+ for xx in range(max_meta_data):
+ meta_data[str(xx)] = str(xx)
+
+ self.assertRaises(exceptions.OverLimit,
+ self.server_client.create_server,
+ name='test', meta=meta_data, flavor_ref='84',
+ image_ref='9e6a2e3b-1601-42a5-985f-c3a2f93a5ec3')
+
class AbsoluteLimitsTestXML(AbsoluteLimitsTestJSON):
_interface = 'xml'
diff --git a/tempest/tests/compute/servers/test_attach_interfaces.py b/tempest/tests/compute/servers/test_attach_interfaces.py
index 5e447c4..c7d4fa0 100644
--- a/tempest/tests/compute/servers/test_attach_interfaces.py
+++ b/tempest/tests/compute/servers/test_attach_interfaces.py
@@ -13,7 +13,6 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest import clients
from tempest.tests.compute import base
import time
@@ -24,11 +23,10 @@
@classmethod
def setUpClass(cls):
- super(AttachInterfacesTestJSON, cls).setUpClass()
- os = clients.Manager(interface=cls._interface)
- if not os.config.network.quantum_available:
+ if not cls.config.network.quantum_available:
raise cls.skipException("Quantum is required")
- cls.client = os.interfaces_client
+ super(AttachInterfacesTestJSON, cls).setUpClass()
+ cls.client = cls.os.interfaces_client
def _check_interface(self, iface, port_id=None, network_id=None,
fixed_ip=None):
diff --git a/tempest/tests/compute/servers/test_instance_actions.py b/tempest/tests/compute/servers/test_instance_actions.py
new file mode 100644
index 0000000..e7e31e8
--- /dev/null
+++ b/tempest/tests/compute/servers/test_instance_actions.py
@@ -0,0 +1,69 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 NEC Corporation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from tempest import exceptions
+from tempest.test import attr
+from tempest.tests.compute import base
+
+
+class InstanceActionsTestJSON(base.BaseComputeTest):
+ _interface = 'json'
+
+ @classmethod
+ def setUpClass(cls):
+ super(InstanceActionsTestJSON, cls).setUpClass()
+ cls.client = cls.servers_client
+ resp, server = cls.create_server(wait_until='ACTIVE')
+ cls.request_id = resp['x-compute-request-id']
+ cls.server_id = server['id']
+
+ @attr(type='positive')
+ def test_list_instance_actions(self):
+ # List actions of the provided server
+ resp, body = self.client.reboot(self.server_id, 'HARD')
+ self.client.wait_for_server_status(self.server_id, 'ACTIVE')
+
+ resp, body = self.client.list_instance_actions(self.server_id)
+ self.assertEqual(200, resp.status)
+ self.assertTrue(len(body) == 2)
+ self.assertTrue(any([i for i in body if i['action'] == 'create']))
+ self.assertTrue(any([i for i in body if i['action'] == 'reboot']))
+
+ @attr(type='positive')
+ def test_get_instance_action(self):
+ # Get the action details of the provided server
+ resp, body = self.client.get_instance_action(self.server_id,
+ self.request_id)
+ self.assertEqual(200, resp.status)
+ self.assertEqual(self.server_id, body['instance_uuid'])
+ self.assertEqual('create', body['action'])
+
+ @attr(type='negative')
+ def test_list_instance_actions_invalid_server(self):
+ # List actions of the invalid server id
+ self.assertRaises(exceptions.NotFound,
+ self.client.list_instance_actions, 'server-999')
+
+ @attr(type='negative')
+ def test_get_instance_action_invalid_request(self):
+ # Get the action details of the provided server with invalid request
+ self.assertRaises(exceptions.NotFound, self.client.get_instance_action,
+ self.server_id, '999')
+
+
+class InstanceActionsTestXML(InstanceActionsTestJSON):
+ _interface = 'xml'
diff --git a/tempest/tests/compute/servers/test_list_server_filters.py b/tempest/tests/compute/servers/test_list_server_filters.py
index 4d2b99f..ca5e112 100644
--- a/tempest/tests/compute/servers/test_list_server_filters.py
+++ b/tempest/tests/compute/servers/test_list_server_filters.py
@@ -22,6 +22,8 @@
from tempest.tests.compute import base
from tempest.tests import utils
+import testtools
+
class ListServerFiltersTestJSON(base.BaseComputeTest):
_interface = 'json'
@@ -73,6 +75,8 @@
cls.client.wait_for_server_status(cls.s3['id'], 'ACTIVE')
resp, cls.s3 = cls.client.get_server(cls.s3['id'])
+ cls.fixed_network_name = cls.config.compute.fixed_network_name
+
@classmethod
def tearDownClass(cls):
cls.client.delete_server(cls.s1['id'])
@@ -180,6 +184,56 @@
self.assertEqual(['ACTIVE'] * 3, [x['status'] for x in servers])
@attr(type='positive')
+ def test_list_servers_filtered_by_name_wildcard(self):
+ # List all servers that contains 'server' in name
+ params = {'name': 'server'}
+ resp, body = self.client.list_servers(params)
+ servers = body['servers']
+
+ self.assertIn(self.s1_name, map(lambda x: x['name'], servers))
+ self.assertIn(self.s2_name, map(lambda x: x['name'], servers))
+ self.assertIn(self.s3_name, map(lambda x: x['name'], servers))
+
+ # Let's take random part of name and try to search it
+ part_name = self.s1_name[6:-1]
+
+ params = {'name': part_name}
+ resp, body = self.client.list_servers(params)
+ servers = body['servers']
+
+ self.assertIn(self.s1_name, map(lambda x: x['name'], servers))
+ self.assertNotIn(self.s2_name, map(lambda x: x['name'], servers))
+ self.assertNotIn(self.s3_name, map(lambda x: x['name'], servers))
+
+ @testtools.skip('Until Bug #1170718 is resolved.')
+ @attr(type='positive', bug='lp1170718')
+ def test_list_servers_filtered_by_ip(self):
+ # Filter servers by ip
+ # Here should be listed 1 server
+ ip = self.s1['addresses'][self.fixed_network_name][0]['addr']
+ params = {'ip': ip}
+ resp, body = self.client.list_servers(params)
+ servers = body['servers']
+
+ self.assertIn(self.s1_name, map(lambda x: x['name'], servers))
+ self.assertNotIn(self.s2_name, map(lambda x: x['name'], servers))
+ self.assertNotIn(self.s3_name, map(lambda x: x['name'], servers))
+
+ @attr(type='positive')
+ def test_list_servers_filtered_by_ip_regex(self):
+ # Filter servers by regex ip
+ # List all servers filtered by part of ip address.
+ # Here should be listed all servers
+ ip = self.s1['addresses'][self.fixed_network_name][0]['addr'][0:-3]
+ params = {'ip': ip}
+ resp, body = self.client.list_servers(params)
+ servers = body['servers']
+
+ self.assertIn(self.s1_name, map(lambda x: x['name'], servers))
+ self.assertIn(self.s2_name, map(lambda x: x['name'], servers))
+ self.assertIn(self.s3_name, map(lambda x: x['name'], servers))
+
+ @attr(type='positive')
def test_list_servers_detailed_limit_results(self):
# Verify only the expected number of detailed results are returned
params = {'limit': 1}
diff --git a/tempest/tests/compute/servers/test_multiple_create.py b/tempest/tests/compute/servers/test_multiple_create.py
index ad5d604..47a38b1 100644
--- a/tempest/tests/compute/servers/test_multiple_create.py
+++ b/tempest/tests/compute/servers/test_multiple_create.py
@@ -48,7 +48,7 @@
created_servers = self._get_created_servers(kwargs['name'])
# NOTE(maurosr): append it to cls.servers list from base.BaseCompute
# class.
- self.servers.append(created_servers)
+ self.servers.extend(created_servers)
# NOTE(maurosr): get a server list, check status of the ones with names
# that match and wait for them become active. At a first look, since
# they are building in parallel, wait inside the for doesn't seem be
diff --git a/tempest/tests/compute/servers/test_server_advanced_ops.py b/tempest/tests/compute/servers/test_server_advanced_ops.py
index ac0d7be..ad859d0 100644
--- a/tempest/tests/compute/servers/test_server_advanced_ops.py
+++ b/tempest/tests/compute/servers/test_server_advanced_ops.py
@@ -57,7 +57,7 @@
flavor_id = self.config.compute.flavor_ref
base_image_id = self.config.compute.image_ref
self.instance = self.compute_client.servers.create(
- i_name, base_image_id, flavor_id)
+ i_name, base_image_id, flavor_id)
try:
self.assertEqual(self.instance.name, i_name)
self.set_resource('instance', self.instance)
@@ -66,16 +66,18 @@
self.assertEqual(self.instance.status, 'BUILD')
instance_id = self.get_resource('instance').id
- test.status_timeout(self.compute_client.servers, instance_id, 'ACTIVE')
+ self.status_timeout(
+ self.compute_client.servers, instance_id, 'ACTIVE')
instance = self.get_resource('instance')
instance_id = instance.id
resize_flavor = self.config.compute.flavor_ref_alt
LOG.debug("Resizing instance %s from flavor %s to flavor %s",
instance.id, instance.flavor, resize_flavor)
instance.resize(resize_flavor)
- test.status_timeout(self.compute_client.servers, instance_id,
+ self.status_timeout(self.compute_client.servers, instance_id,
'VERIFY_RESIZE')
LOG.debug("Confirming resize of instance %s", instance_id)
instance.confirm_resize()
- test.status_timeout(self.compute_client.servers, instance_id, 'ACTIVE')
+ self.status_timeout(
+ self.compute_client.servers, instance_id, 'ACTIVE')
diff --git a/tempest/tests/compute/servers/test_server_basic_ops.py b/tempest/tests/compute/servers/test_server_basic_ops.py
index c7fad7a..fdbbd3c 100644
--- a/tempest/tests/compute/servers/test_server_basic_ops.py
+++ b/tempest/tests/compute/servers/test_server_basic_ops.py
@@ -78,7 +78,7 @@
for ruleset in rulesets:
try:
self.compute_client.security_group_rules.create(
- self.secgroup.id, **ruleset)
+ self.secgroup.id, **ruleset)
except Exception:
self.fail("Failed to create rule in security group.")
@@ -90,7 +90,7 @@
'key_name': self.get_resource('keypair').id
}
self.instance = self.compute_client.servers.create(
- i_name, base_image_id, flavor_id, **create_kwargs)
+ i_name, base_image_id, flavor_id, **create_kwargs)
try:
self.assertEqual(self.instance.name, i_name)
self.set_resource('instance', self.instance)
@@ -101,7 +101,8 @@
def wait_on_active(self):
instance_id = self.get_resource('instance').id
- test.status_timeout(self.compute_client.servers, instance_id, 'ACTIVE')
+ self.status_timeout(
+ self.compute_client.servers, instance_id, 'ACTIVE')
def pause_server(self):
instance = self.get_resource('instance')
@@ -109,7 +110,8 @@
LOG.debug("Pausing instance %s. Current status: %s",
instance_id, instance.status)
instance.pause()
- test.status_timeout(self.compute_client.servers, instance_id, 'PAUSED')
+ self.status_timeout(
+ self.compute_client.servers, instance_id, 'PAUSED')
def unpause_server(self):
instance = self.get_resource('instance')
@@ -117,7 +119,8 @@
LOG.debug("Unpausing instance %s. Current status: %s",
instance_id, instance.status)
instance.unpause()
- test.status_timeout(self.compute_client.servers, instance_id, 'ACTIVE')
+ self.status_timeout(
+ self.compute_client.servers, instance_id, 'ACTIVE')
def suspend_server(self):
instance = self.get_resource('instance')
@@ -125,7 +128,7 @@
LOG.debug("Suspending instance %s. Current status: %s",
instance_id, instance.status)
instance.suspend()
- test.status_timeout(self.compute_client.servers,
+ self.status_timeout(self.compute_client.servers,
instance_id, 'SUSPENDED')
def resume_server(self):
@@ -134,7 +137,8 @@
LOG.debug("Resuming instance %s. Current status: %s",
instance_id, instance.status)
instance.resume()
- test.status_timeout(self.compute_client.servers, instance_id, 'ACTIVE')
+ self.status_timeout(
+ self.compute_client.servers, instance_id, 'ACTIVE')
def terminate_instance(self):
instance = self.get_resource('instance')
diff --git a/tempest/tests/compute/servers/test_server_rescue.py b/tempest/tests/compute/servers/test_server_rescue.py
index 91010ce..862a86a 100644
--- a/tempest/tests/compute/servers/test_server_rescue.py
+++ b/tempest/tests/compute/servers/test_server_rescue.py
@@ -15,8 +15,6 @@
# License for the specific language governing permissions and limitations
# under the License.
-import testtools
-
from tempest.common.utils.data_utils import rand_name
import tempest.config
from tempest import exceptions
@@ -43,25 +41,25 @@
cls.sg_name = rand_name('sg')
cls.sg_desc = rand_name('sg-desc')
resp, cls.sg = \
- cls.security_groups_client.create_security_group(cls.sg_name,
- cls.sg_desc)
+ cls.security_groups_client.create_security_group(cls.sg_name,
+ cls.sg_desc)
cls.sg_id = cls.sg['id']
# Create a volume and wait for it to become ready for attach
resp, cls.volume_to_attach = \
- cls.volumes_extensions_client.create_volume(1,
- display_name=
- 'test_attach')
+ cls.volumes_extensions_client.create_volume(1,
+ display_name=
+ 'test_attach')
cls.volumes_extensions_client.wait_for_volume_status(
- cls.volume_to_attach['id'], 'available')
+ cls.volume_to_attach['id'], 'available')
# Create a volume and wait for it to become ready for attach
resp, cls.volume_to_detach = \
- cls.volumes_extensions_client.create_volume(1,
- display_name=
- 'test_detach')
+ cls.volumes_extensions_client.create_volume(1,
+ display_name=
+ 'test_detach')
cls.volumes_extensions_client.wait_for_volume_status(
- cls.volume_to_detach['id'], 'available')
+ cls.volume_to_detach['id'], 'available')
# Server for positive tests
resp, server = cls.create_server(image_id=cls.image_ref,
@@ -87,14 +85,14 @@
@classmethod
def tearDownClass(cls):
- super(ServerRescueTestJSON, cls).tearDownClass()
#Deleting the floating IP which is created in this method
cls.floating_ips_client.delete_floating_ip(cls.floating_ip_id)
client = cls.volumes_extensions_client
client.delete_volume(str(cls.volume_to_attach['id']).strip())
client.delete_volume(str(cls.volume_to_detach['id']).strip())
- resp, cls.sg = \
- cls.security_groups_client.delete_security_group(cls.sg_id)
+ resp, cls.sg = cls.security_groups_client.delete_security_group(
+ cls.sg_id)
+ super(ServerRescueTestJSON, cls).tearDownClass()
def tearDown(self):
super(ServerRescueTestJSON, self).tearDown()
@@ -155,7 +153,7 @@
self.volume_to_detach['id'],
device='/dev/%s' % self.device)
self.volumes_extensions_client.wait_for_volume_status(
- self.volume_to_detach['id'], 'in-use')
+ self.volume_to_detach['id'], 'in-use')
# Rescue the server
self.servers_client.rescue_server(self.server_id, self.password)
@@ -181,9 +179,8 @@
#Association of floating IP to a rescued vm
client = self.floating_ips_client
- resp, body =\
- client.associate_floating_ip_to_server(self.floating_ip,
- self.server_id)
+ resp, body = client.associate_floating_ip_to_server(self.floating_ip,
+ self.server_id)
self.assertEqual(202, resp.status)
#Disassociation of floating IP that was associated in this method
@@ -193,8 +190,12 @@
self.assertEqual(202, resp.status)
@attr(type='positive')
- @testtools.skip("Skipped until Bug #1126257 is resolved")
def test_rescued_vm_add_remove_security_group(self):
+ # Rescue the server
+ self.servers_client.rescue_server(
+ self.server_id, self.password)
+ self.servers_client.wait_for_server_status(self.server_id, 'RESCUE')
+
#Add Security group
resp, body = self.servers_client.add_security_group(self.server_id,
self.sg_name)
@@ -202,7 +203,7 @@
#Delete Security group
resp, body = self.servers_client.remove_security_group(self.server_id,
- self.sg_id)
+ self.sg_name)
self.assertEqual(202, resp.status)
# Unrescue the server
diff --git a/tempest/tests/compute/servers/test_servers_whitebox.py b/tempest/tests/compute/servers/test_servers_whitebox.py
index 9b75cd5..6b192dd 100644
--- a/tempest/tests/compute/servers/test_servers_whitebox.py
+++ b/tempest/tests/compute/servers/test_servers_whitebox.py
@@ -55,9 +55,9 @@
def test_create_server_vcpu_quota_full(self):
# Disallow server creation when tenant's vcpu quota is full
quotas = self.meta.tables['quotas']
- stmt = quotas.select().where(
- quotas.c.project_id == self.tenant_id).where(
- quotas.c.resource == 'cores')
+ stmt = (quotas.select().
+ where(quotas.c.project_id == self.tenant_id).
+ where(quotas.c.resource == 'cores'))
result = self.connection.execute(stmt).first()
# Set vcpu quota for tenant if not already set
@@ -87,9 +87,9 @@
def test_create_server_memory_quota_full(self):
# Disallow server creation when tenant's memory quota is full
quotas = self.meta.tables['quotas']
- stmt = quotas.select().where(
- quotas.c.project_id == self.tenant_id).where(
- quotas.c.resource == 'ram')
+ stmt = (quotas.select().
+ where(quotas.c.project_id == self.tenant_id).
+ where(quotas.c.resource == 'ram'))
result = self.connection.execute(stmt).first()
# Set memory quota for tenant if not already set
diff --git a/tempest/tests/compute/test_authorization.py b/tempest/tests/compute/test_authorization.py
index 91cf39f..6edc946 100644
--- a/tempest/tests/compute/test_authorization.py
+++ b/tempest/tests/compute/test_authorization.py
@@ -234,8 +234,7 @@
# Reset the base_url...
self.alt_security_client.base_url = self.saved_base_url
if resp['status'] is not None:
- #TODO(afazekas): body not defined
- self.alt_security_client.delete_security_group(body['id'])
+ self.alt_security_client.delete_security_group(resp['id'])
self.fail("Create Security Group request should not happen if"
"the tenant id does not match the current user")
@@ -274,8 +273,7 @@
# Reset the base_url...
self.alt_security_client.base_url = self.saved_base_url
if resp['status'] is not None:
- self.alt_security_client.delete_security_group_rule(
- body['id']) # BUG
+ self.alt_security_client.delete_security_group_rule(resp['id'])
self.fail("Create security group rule request should not "
"happen if the tenant id does not match the"
" current user")
diff --git a/tempest/tests/compute/test_live_block_migration.py b/tempest/tests/compute/test_live_block_migration.py
index e438098..30ff882 100644
--- a/tempest/tests/compute/test_live_block_migration.py
+++ b/tempest/tests/compute/test_live_block_migration.py
@@ -31,11 +31,7 @@
_host_key = 'OS-EXT-SRV-ATTR:host'
_interface = 'json'
- live_migration_available = (
- config.TempestConfig().compute.live_migration_available)
- use_block_migration_for_live_migration = (
- config.TempestConfig().compute.use_block_migration_for_live_migration)
- run_ssh = config.TempestConfig().compute.run_ssh
+ CONF = config.TempestConfig()
@classmethod
def setUpClass(cls):
@@ -63,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.use_block_migration_for_live_migration)
+ server_id, dest_host,
+ self.config.compute.use_block_migration_for_live_migration)
return body
def _get_host_other_than(self, host):
@@ -95,8 +92,8 @@
return server_id
@attr(type='positive')
- @testtools.skipIf(not live_migration_available,
- 'Block Live migration not available')
+ @testtools.skipIf(not CONF.compute.live_migration_available,
+ 'Live migration not available')
def test_live_block_migration(self):
# Live block migrate an instance to another host
if len(self._get_compute_hostnames()) < 2:
@@ -109,11 +106,10 @@
self.servers_client.wait_for_server_status(server_id, 'ACTIVE')
self.assertEquals(target_host, self._get_host_for_server(server_id))
- @testtools.skipIf(not live_migration_available,
- 'Block Live migration not available')
+ @testtools.skipIf(not CONF.compute.live_migration_available,
+ 'Live migration not available')
def test_invalid_host_for_migration(self):
# Migrating to an invalid host should not change the status
-
server_id = self._get_an_active_server()
target_host = self._get_non_existing_host_name()
diff --git a/tempest/tests/identity/admin/test_users.py b/tempest/tests/identity/admin/test_users.py
index 0573b21..f9772ac 100644
--- a/tempest/tests/identity/admin/test_users.py
+++ b/tempest/tests/identity/admin/test_users.py
@@ -309,8 +309,8 @@
for i in body:
fetched_user_ids.append(i['id'])
#verifying the user Id in the list
- missing_users =\
- [user for user in user_ids if user not in fetched_user_ids]
+ missing_users = [missing_user for missing_user in user_ids
+ if missing_user not in fetched_user_ids]
self.assertEqual(0, len(missing_users),
"Failed to find user %s in fetched list" %
', '.join(m_user for m_user in missing_users))
diff --git a/tempest/tests/identity/admin/v3/test_endpoints.py b/tempest/tests/identity/admin/v3/test_endpoints.py
old mode 100755
new mode 100644
index 98fab57..3ad9b28
--- a/tempest/tests/identity/admin/v3/test_endpoints.py
+++ b/tempest/tests/identity/admin/v3/test_endpoints.py
@@ -126,7 +126,6 @@
description=s_description)
self.service_ids.append(self.service2['id'])
#Updating endpoint with new values
- service_id = self.service2['id']
region2 = rand_name('region')
url2 = rand_name('url')
interface2 = 'internal'
diff --git a/tempest/tests/identity/admin/v3/test_services.py b/tempest/tests/identity/admin/v3/test_services.py
new file mode 100644
index 0000000..fef3bca
--- /dev/null
+++ b/tempest/tests/identity/admin/v3/test_services.py
@@ -0,0 +1,56 @@
+#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.common.utils.data_utils import rand_name
+from tempest.tests.identity import base
+
+
+class ServicesTestJSON(base.BaseIdentityAdminTest):
+ _interface = 'json'
+
+ def test_update_service(self):
+ # Update description attribute of service
+ name = rand_name('service-')
+ type = rand_name('type--')
+ description = rand_name('description-')
+ resp, body = self.client.create_service(
+ name, type, description=description)
+ self.assertEqual('200', resp['status'])
+ #Deleting the service created in this method
+ self.addCleanup(self.client.delete_service, body['id'])
+
+ s_id = body['id']
+ resp1_desc = body['description']
+
+ s_desc2 = rand_name('desc2-')
+ resp, body = self.service_client.update_service(
+ s_id, description=s_desc2)
+ resp2_desc = body['description']
+ self.assertEqual('200', resp['status'])
+ self.assertNotEqual(resp1_desc, resp2_desc)
+
+ #Get service
+ resp, body = self.client.get_service(s_id)
+ resp3_desc = body['description']
+
+ self.assertNotEqual(resp1_desc, resp3_desc)
+ self.assertEqual(resp2_desc, resp3_desc)
+
+
+class ServicesTestXML(ServicesTestJSON):
+ _interface = 'xml'
diff --git a/tempest/tests/identity/admin/v3/test_users.py b/tempest/tests/identity/admin/v3/test_users.py
new file mode 100644
index 0000000..39b8ca1
--- /dev/null
+++ b/tempest/tests/identity/admin/v3/test_users.py
@@ -0,0 +1,123 @@
+# 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.common.utils.data_utils import rand_name
+from tempest.test import attr
+from tempest.tests.identity import base
+
+
+class UsersV3TestJSON(base.BaseIdentityAdminTest):
+ _interface = 'json'
+
+ @attr('smoke')
+ def test_user_update(self):
+ # Test case to check if updating of user attributes is successful.
+ #Creating first user
+ u_name = rand_name('user-')
+ u_desc = u_name + 'description'
+ u_email = u_name + '@testmail.tm'
+ u_password = rand_name('pass-')
+ resp, user = self.v3_client.create_user(
+ u_name, description=u_desc, password=u_password,
+ email=u_email, enabled=False)
+ # Delete the User at the end of this method
+ self.addCleanup(self.v3_client.delete_user, user['id'])
+ #Creating second project for updation
+ resp, project = self.v3_client.create_project(
+ rand_name('project-'), description=rand_name('project-desc-'))
+ # Delete the Project at the end of this method
+ self.addCleanup(self.v3_client.delete_project, project['id'])
+ #Updating user details with new values
+ u_name2 = rand_name('user2-')
+ u_email2 = u_name2 + '@testmail.tm'
+ u_description2 = u_name2 + ' description'
+ resp, update_user = self.v3_client.update_user(
+ user['id'], name=u_name2, description=u_description2,
+ project_id=project['id'],
+ email=u_email2, enabled=False)
+ #Assert response body of update user.
+ self.assertEqual(200, resp.status)
+ self.assertEqual(u_name2, update_user['name'])
+ self.assertEqual(u_description2, update_user['description'])
+ self.assertEqual(project['id'],
+ update_user['project_id'])
+ self.assertEqual(u_email2, update_user['email'])
+ self.assertEqual('false', str(update_user['enabled']).lower())
+ #GET by id after updation
+ resp, new_user_get = self.v3_client.get_user(user['id'])
+ #Assert response body of GET after updation
+ self.assertEqual(u_name2, new_user_get['name'])
+ self.assertEqual(u_description2, new_user_get['description'])
+ self.assertEqual(project['id'],
+ new_user_get['project_id'])
+ self.assertEqual(u_email2, new_user_get['email'])
+ self.assertEqual('false', str(new_user_get['enabled']).lower())
+
+ @attr('smoke')
+ def test_list_user_projects(self):
+ #List the projects that a user has access upon
+ assigned_project_ids = list()
+ fetched_project_ids = list()
+ _, u_project = self.v3_client.create_project(
+ rand_name('project-'), description=rand_name('project-desc-'))
+ # Delete the Project at the end of this method
+ self.addCleanup(self.v3_client.delete_project, u_project['id'])
+ #Create a user.
+ u_name = rand_name('user-')
+ u_desc = u_name + 'description'
+ u_email = u_name + '@testmail.tm'
+ u_password = rand_name('pass-')
+ _, user_body = self.v3_client.create_user(
+ u_name, description=u_desc, password=u_password,
+ email=u_email, enabled=False, project_id=u_project['id'])
+ # Delete the User at the end of this method
+ self.addCleanup(self.v3_client.delete_user, user_body['id'])
+ # Creating Role
+ _, role_body = self.v3_client.create_role(rand_name('role-'))
+ # Delete the Role at the end of this method
+ self.addCleanup(self.v3_client.delete_role, role_body['id'])
+
+ _, user = self.v3_client.get_user(user_body['id'])
+ _, role = self.v3_client.get_role(role_body['id'])
+ for i in range(2):
+ # Creating project so as to assign role
+ _, project_body = self.v3_client.create_project(
+ rand_name('project-'), description=rand_name('project-desc-'))
+ _, project = self.v3_client.get_project(project_body['id'])
+ # Delete the Project at the end of this method
+ self.addCleanup(self.v3_client.delete_project, project_body['id'])
+ #Assigning roles to user on project
+ self.v3_client.assign_user_role(project['id'],
+ user['id'],
+ role['id'])
+ assigned_project_ids.append(project['id'])
+ resp, body = self.v3_client.list_user_projects(user['id'])
+ self.assertEqual(200, resp.status)
+ for i in body:
+ fetched_project_ids.append(i['id'])
+ #verifying the project ids in list
+ missing_projects =\
+ [p for p in assigned_project_ids
+ if p not in fetched_project_ids]
+ self.assertEqual(0, len(missing_projects),
+ "Failed to find project %s in fetched list" %
+ ', '.join(m_project for m_project
+ in missing_projects))
+
+
+class UsersV3TestXML(UsersV3TestJSON):
+ _interface = 'xml'
diff --git a/tempest/tests/identity/base.py b/tempest/tests/identity/base.py
index 64b8993..6980425 100644
--- a/tempest/tests/identity/base.py
+++ b/tempest/tests/identity/base.py
@@ -29,6 +29,8 @@
cls.client = os.identity_client
cls.token_client = os.token_client
cls.endpoints_client = os.endpoints_client
+ cls.v3_client = os.identity_v3_client
+ cls.service_client = os.service_client
if not cls.client.has_admin_extensions():
raise cls.skipException("Admin extensions disabled")
diff --git a/tempest/tests/image/v1/test_images.py b/tempest/tests/image/v1/test_images.py
index 0065d27..19c0aa0 100644
--- a/tempest/tests/image/v1/test_images.py
+++ b/tempest/tests/image/v1/test_images.py
@@ -81,32 +81,10 @@
self.assertEqual(properties['key2'], 'value2')
def test_register_http_image(self):
- container_client = self.os.container_client
- object_client = self.os.object_client
- container_name = "image_container"
- object_name = "test_image.img"
- container_client.create_container(container_name)
- self.addCleanup(container_client.delete_container, container_name)
- cont_headers = {'X-Container-Read': '.r:*'}
- resp, _ = container_client.update_container_metadata(
- container_name,
- metadata=cont_headers,
- metadata_prefix='')
- self.assertEqual(resp['status'], '204')
-
- data = "TESTIMAGE"
- resp, _ = object_client.create_object(container_name,
- object_name, data)
- self.addCleanup(object_client.delete_object, container_name,
- object_name)
- self.assertEqual(resp['status'], '201')
- object_url = '/'.join((object_client.base_url,
- container_name,
- object_name))
resp, body = self.create_image(name='New Http Image',
container_format='bare',
disk_format='raw', is_public=True,
- copy_from=object_url)
+ copy_from=self.config.images.http_image)
self.assertTrue('id' in body)
image_id = body.get('id')
self.created_images.append(image_id)
@@ -115,7 +93,6 @@
self.client.wait_for_image_status(image_id, 'active')
resp, body = self.client.get_image(image_id)
self.assertEqual(resp['status'], '200')
- self.assertEqual(body, data)
@attr(type='image')
def test_register_image_with_min_ram(self):
@@ -270,7 +247,7 @@
@attr(type='image')
def test_index_name(self):
resp, images_list = self.client.image_list_detail(
- name='New Remote Image dup')
+ name='New Remote Image dup')
self.assertEqual(resp['status'], '200')
result_set = set(map(lambda x: x['id'], images_list))
for image in images_list:
diff --git a/tempest/tests/network/common.py b/tempest/tests/network/common.py
index 1cff2c4..6811acf 100644
--- a/tempest/tests/network/common.py
+++ b/tempest/tests/network/common.py
@@ -125,7 +125,6 @@
@classmethod
def setUpClass(cls):
super(TestNetworkSmokeCommon, cls).setUpClass()
- cfg = cls.config.network
cls.tenant_id = cls.manager._get_identity_client(
cls.config.identity.username,
cls.config.identity.password,
@@ -246,10 +245,7 @@
port=dict(name=name,
network_id=network.id,
tenant_id=network.tenant_id))
- try:
- result = self.network_client.create_port(body=body)
- except Exception as e:
- raise
+ result = self.network_client.create_port(body=body)
self.assertIsNotNone(result, 'Unable to allocate port')
port = DeletablePort(client=self.network_client,
**result['port'])
@@ -273,7 +269,7 @@
self.set_resource(name, server)
except AttributeError:
self.fail("Server not successfully created.")
- test.status_timeout(client.servers, server.id, 'ACTIVE')
+ self.status_timeout(client.servers, server.id, 'ACTIVE')
# The instance retrieved on creation is missing network
# details, necessitating retrieval after it becomes active to
# ensure correct details.
diff --git a/tempest/tests/object_storage/test_container_services.py b/tempest/tests/object_storage/test_container_services.py
index 2c5b1ff..223744c 100644
--- a/tempest/tests/object_storage/test_container_services.py
+++ b/tempest/tests/object_storage/test_container_services.py
@@ -124,7 +124,7 @@
# List container metadata
resp, _ = self.container_client.list_container_metadata(
- container_name)
+ container_name)
self.assertEqual(resp['status'], '204')
self.assertIn('x-container-meta-name', resp)
self.assertIn('x-container-meta-description', resp)
@@ -132,10 +132,9 @@
self.assertEqual(resp['x-container-meta-description'], 'Travel')
# Delete container metadata
- resp, _ = \
- self.container_client.delete_container_metadata(
- container_name,
- metadata=metadata.keys())
+ resp, _ = self.container_client.delete_container_metadata(
+ container_name,
+ metadata=metadata.keys())
self.assertEqual(resp['status'], '204')
resp, _ = self.container_client.list_container_metadata(container_name)
diff --git a/tempest/tests/object_storage/test_object_expiry.py b/tempest/tests/object_storage/test_object_expiry.py
index c12ec3d..e1b1dbd 100644
--- a/tempest/tests/object_storage/test_object_expiry.py
+++ b/tempest/tests/object_storage/test_object_expiry.py
@@ -21,7 +21,7 @@
from tempest.test import attr
from tempest.tests.object_storage import base
import testtools
-from time import sleep
+import time
class ObjectExpiryTest(base.BaseObjectTest):
@@ -88,7 +88,7 @@
# Check data
self.assertEqual(body, data)
# Sleep for over 5 seconds, so that object is expired
- sleep(5)
+ time.sleep(5)
# Verification of raised exception after object gets expired
self.assertRaises(exceptions.NotFound, self.object_client.get_object,
self.container_name, object_name)
diff --git a/tempest/tests/object_storage/test_object_services.py b/tempest/tests/object_storage/test_object_services.py
index 1edce92..4fcc617 100644
--- a/tempest/tests/object_storage/test_object_services.py
+++ b/tempest/tests/object_storage/test_object_services.py
@@ -21,7 +21,7 @@
from tempest.test import attr
from tempest.tests.object_storage import base
import testtools
-from time import time
+import time
class ObjectTest(base.BaseObjectTest):
@@ -617,7 +617,7 @@
self.object_client.create_object(self.container_name,
object_name, data)
- expires = int(time() + 10)
+ expires = int(time.time() + 10)
#Trying to GET object using temp URL with in expiry time
_, body = self.object_client.get_object_using_temp_url(
diff --git a/tempest/tests/object_storage/test_object_version.py b/tempest/tests/object_storage/test_object_version.py
index bc1c045..80cfc27 100644
--- a/tempest/tests/object_storage/test_object_version.py
+++ b/tempest/tests/object_storage/test_object_version.py
@@ -59,7 +59,7 @@
# Create a containers
vers_container_name = rand_name(name='TestVersionContainer')
resp, body = self.container_client.create_container(
- vers_container_name)
+ vers_container_name)
self.containers.append(vers_container_name)
self.assertIn(resp['status'], ('202', '201'))
self.assertContainer(vers_container_name, '0', '0',
@@ -68,9 +68,9 @@
base_container_name = rand_name(name='TestBaseContainer')
headers = {'X-versions-Location': vers_container_name}
resp, body = self.container_client.create_container(
- base_container_name,
- metadata=headers,
- metadata_prefix='')
+ base_container_name,
+ metadata=headers,
+ metadata_prefix='')
self.containers.append(base_container_name)
self.assertIn(resp['status'], ('202', '201'))
self.assertContainer(base_container_name, '0', '0',
diff --git a/tempest/tests/volume/admin/test_multi_backend.py b/tempest/tests/volume/admin/test_multi_backend.py
new file mode 100644
index 0000000..c50586c
--- /dev/null
+++ b/tempest/tests/volume/admin/test_multi_backend.py
@@ -0,0 +1,159 @@
+# 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.
+
+import logging
+import testtools
+
+from tempest.common.utils.data_utils import rand_name
+from tempest import config
+from tempest.services.volume.json.admin import volume_types_client
+from tempest.services.volume.json import volumes_client
+from tempest.test import attr
+from tempest.tests.volume import base
+
+LOG = logging.getLogger(__name__)
+
+
+class VolumeMultiBackendTest(base.BaseVolumeAdminTest):
+ _interface = "json"
+
+ multi_backend_enabled = config.TempestConfig().volume.multi_backend_enabled
+ backend1_name = config.TempestConfig().volume.backend1_name
+ backend2_name = config.TempestConfig().volume.backend2_name
+ backend_names_equal = False
+ if (backend1_name == backend2_name):
+ backend_names_equal = True
+
+ @classmethod
+ @testtools.skipIf(not multi_backend_enabled,
+ "Cinder multi-backend feature is not available")
+ def setUpClass(cls):
+ super(VolumeMultiBackendTest, cls).setUpClass()
+
+ adm_user = cls.config.identity.admin_username
+ adm_pass = cls.config.identity.admin_password
+ adm_tenant = cls.config.identity.admin_tenant_name
+ auth_url = cls.config.identity.uri
+
+ cls.client = volumes_client.VolumesClientJSON(cls.config,
+ adm_user,
+ adm_pass,
+ auth_url,
+ adm_tenant)
+ cls.client2 = volume_types_client.VolumeTypesClientJSON(cls.config,
+ adm_user,
+ adm_pass,
+ auth_url,
+ adm_tenant)
+
+ ## variables initialization
+ type_name1 = rand_name('type-')
+ type_name2 = rand_name('type-')
+ cls.volume_type_list = []
+
+ vol_name1 = rand_name('Volume-')
+ vol_name2 = rand_name('Volume-')
+ cls.volume_id_list = []
+
+ try:
+ ## Volume types creation
+ extra_specs1 = {"volume_backend_name": cls.backend1_name}
+ resp, cls.body1 = cls.client2.create_volume_type(
+ type_name1, extra_specs=extra_specs1)
+ cls.volume_type_list.append(cls.body1)
+
+ extra_specs2 = {"volume_backend_name": cls.backend2_name}
+ resp, cls.body2 = cls.client2.create_volume_type(
+ type_name2, extra_specs=extra_specs2)
+ cls.volume_type_list.append(cls.body2)
+
+ ## Volumes creation
+ resp, cls.volume1 = cls.client.create_volume(
+ size=1, display_name=vol_name1, volume_type=type_name1)
+ cls.client.wait_for_volume_status(cls.volume1['id'], 'available')
+ cls.volume_id_list.append(cls.volume1['id'])
+
+ resp, cls.volume2 = cls.client.create_volume(
+ size=1, display_name=vol_name2, volume_type=type_name2)
+ cls.client.wait_for_volume_status(cls.volume2['id'], 'available')
+ cls.volume_id_list.append(cls.volume2['id'])
+ except Exception:
+ LOG.exception("setup failed")
+ cls.tearDownClass()
+ raise
+
+ @classmethod
+ def tearDownClass(cls):
+ ## volumes deletion
+ for volume_id in cls.volume_id_list:
+ cls.client.delete_volume(volume_id)
+ cls.client.wait_for_resource_deletion(volume_id)
+
+ ## volume types deletion
+ for volume_type in cls.volume_type_list:
+ cls.client2.delete_volume_type(volume_type)
+
+ super(VolumeMultiBackendTest, cls).tearDownClass()
+
+ @attr(type=['smoke', 'gate'])
+ def test_multi_backend_enabled(self):
+ # this test checks that multi backend is enabled for at least the
+ # computes where the volumes created in setUp were made
+ # if multi-backend is enabled: os-vol-attr:host should be like:
+ # host@backend_name
+ # this test fails if:
+ # - multi backend is not enabled
+ resp, fetched_volume = self.client.get_volume(self.volume1['id'])
+ self.assertEqual(200, resp.status)
+
+ volume_host1 = fetched_volume['os-vol-host-attr:host']
+ msg = ("Multi-backend is not available for at least host "
+ "%(volume_host1)s") % locals()
+ self.assertTrue(len(volume_host1.split("@")) > 1, msg)
+
+ resp, fetched_volume = self.client.get_volume(self.volume2['id'])
+ self.assertEqual(200, resp.status)
+
+ volume_host2 = fetched_volume['os-vol-host-attr:host']
+ msg = ("Multi-backend is not available for at least host "
+ "%(volume_host2)s") % locals()
+ self.assertTrue(len(volume_host2.split("@")) > 1, msg)
+
+ @attr(type='gate')
+ def test_backend_name_distinction(self):
+ # this test checks that the two volumes created at setUp doesn't
+ # belong to the same backend (if they are in the same backend, that
+ # means, volume_backend_name distinction is not working properly)
+ # this test fails if:
+ # - tempest.conf is not well configured
+ # - the two volumes belongs to the same backend
+
+ # checks tempest.conf
+ msg = ("tempest.conf is not well configured, "
+ "backend1_name and backend2_name are equal")
+ self.assertEqual(self.backend_names_equal, False, msg)
+
+ # checks the two volumes belongs to different backend
+ resp, fetched_volume = self.client.get_volume(self.volume1['id'])
+ volume_host1 = fetched_volume['os-vol-host-attr:host']
+
+ resp, fetched_volume = self.client.get_volume(self.volume2['id'])
+ volume_host2 = fetched_volume['os-vol-host-attr:host']
+
+ msg = ("volume2 was created in the same backend as volume1: "
+ "%(volume_host2)s.") % locals()
+ self.assertNotEqual(volume_host2, volume_host1, msg)
diff --git a/tempest/tests/volume/admin/test_volume_types.py b/tempest/tests/volume/admin/test_volume_types.py
index 38ac74a..8fccd24 100644
--- a/tempest/tests/volume/admin/test_volume_types.py
+++ b/tempest/tests/volume/admin/test_volume_types.py
@@ -17,6 +17,7 @@
from tempest.common.utils.data_utils import rand_name
from tempest.services.volume.json.admin import volume_types_client
+from tempest.test import attr
from tempest.tests.volume.base import BaseVolumeTest
@@ -37,6 +38,7 @@
auth_url,
adm_tenant)
+ @attr(type=['smoke', 'gate'])
def test_volume_type_list(self):
# List Volume types.
try:
@@ -46,6 +48,7 @@
except Exception:
self.fail("Could not list volume types")
+ @attr(type=['smoke', 'gate'])
def test_create_get_delete_volume_with_volume_type_and_extra_specs(self):
# Create/get/delete volume with volume_type and extra spec.
try:
@@ -55,15 +58,15 @@
extra_specs = {"storage_protocol": "iSCSI",
"vendor_name": "Open Source"}
body = {}
- resp, body = self.client.create_volume_type(vol_type_name,
- extra_specs=
- extra_specs)
+ resp, body = self.client.create_volume_type(
+ vol_type_name,
+ extra_specs=extra_specs)
self.assertEqual(200, resp.status)
self.assertTrue('id' in body)
self.assertTrue('name' in body)
- resp, volume = self.volumes_client.\
- create_volume(size=1, display_name=vol_name,
- volume_type=vol_type_name)
+ resp, volume = self.volumes_client.create_volume(
+ size=1, display_name=vol_name,
+ volume_type=vol_type_name)
self.assertEqual(200, resp.status)
self.assertTrue('id' in volume)
self.assertTrue('display_name' in volume)
@@ -74,8 +77,7 @@
"Field volume id is empty or not found.")
self.volumes_client.wait_for_volume_status(volume['id'],
'available')
- resp, fetched_volume = self.volumes_client.\
- get_volume(volume['id'])
+ resp, fetched_volume = self.volumes_client.get_volume(volume['id'])
self.assertEqual(200, resp.status)
self.assertEqual(vol_name, fetched_volume['display_name'],
'The fetched Volume is different '
@@ -98,14 +100,16 @@
resp, _ = self.client.delete_volume_type(body['id'])
self.assertEqual(202, resp.status)
+ @attr(type=['smoke', 'gate'])
def test_volume_type_create_delete(self):
# Create/Delete volume type.
try:
name = rand_name("volume-type-")
extra_specs = {"storage_protocol": "iSCSI",
"vendor_name": "Open Source"}
- resp, body = self.client.\
- create_volume_type(name, extra_specs=extra_specs)
+ resp, body = self.client.create_volume_type(
+ name,
+ extra_specs=extra_specs)
self.assertEqual(200, resp.status)
self.assertTrue('id' in body)
self.assertTrue('name' in body)
@@ -114,12 +118,12 @@
"to the requested name")
self.assertTrue(body['id'] is not None,
"Field volume_type id is empty or not found.")
- resp, fetched_volume_type = self.client.\
- delete_volume_type(body['id'])
+ resp, _ = self.client.delete_volume_type(body['id'])
self.assertEqual(202, resp.status)
except Exception:
self.fail("Could not create a volume_type")
+ @attr(type=['smoke', 'gate'])
def test_volume_type_create_get(self):
# Create/get volume type.
try:
@@ -127,8 +131,9 @@
name = rand_name("volume-type-")
extra_specs = {"storage_protocol": "iSCSI",
"vendor_name": "Open Source"}
- resp, body = self.client.\
- create_volume_type(name, extra_specs=extra_specs)
+ resp, body = self.client.create_volume_type(
+ name,
+ extra_specs=extra_specs)
self.assertEqual(200, resp.status)
self.assertTrue('id' in body)
self.assertTrue('name' in body)
diff --git a/tempest/tests/volume/admin/test_volume_types_extra_specs.py b/tempest/tests/volume/admin/test_volume_types_extra_specs.py
index 31e2879..85edd64 100644
--- a/tempest/tests/volume/admin/test_volume_types_extra_specs.py
+++ b/tempest/tests/volume/admin/test_volume_types_extra_specs.py
@@ -16,6 +16,7 @@
# under the License.
from tempest.common.utils.data_utils import rand_name
+from tempest.test import attr
from tempest.tests.volume import base
@@ -30,20 +31,21 @@
@classmethod
def tearDownClass(cls):
- super(VolumeTypesExtraSpecsTest, cls).tearDownClass()
cls.client.delete_volume_type(cls.volume_type['id'])
+ super(VolumeTypesExtraSpecsTest, cls).tearDownClass()
+ @attr(type=['smoke', 'gate'])
def test_volume_type_extra_specs_list(self):
# List Volume types extra specs.
try:
extra_specs = {"spec1": "val1"}
- resp, body = self.client.\
- create_volume_type_extra_specs(self.volume_type['id'], extra_specs)
+ resp, body = self.client.create_volume_type_extra_specs(
+ self.volume_type['id'], extra_specs)
self.assertEqual(200, resp.status)
self.assertEqual(extra_specs, body,
"Volume type extra spec incorrectly created")
- resp, body = self.client.\
- list_volume_types_extra_specs(self.volume_type['id'])
+ resp, body = self.client.list_volume_types_extra_specs(
+ self.volume_type['id'])
self.assertEqual(200, resp.status)
self.assertTrue(type(body), dict)
self.assertTrue('spec1' in body, "Incorrect volume type extra"
@@ -51,21 +53,22 @@
except Exception:
self.fail("Could not list volume types extra specs")
+ @attr(type=['gate'])
def test_volume_type_extra_specs_update(self):
# Update volume type extra specs
try:
extra_specs = {"spec2": "val1"}
- resp, body = self.client.\
- create_volume_type_extra_specs(self.volume_type['id'], extra_specs)
+ resp, body = self.client.create_volume_type_extra_specs(
+ self.volume_type['id'], extra_specs)
self.assertEqual(200, resp.status)
self.assertEqual(extra_specs, body,
"Volume type extra spec incorrectly created")
extra_spec = {"spec2": "val2"}
- resp, body = self.client.\
- update_volume_type_extra_specs(self.volume_type['id'],
- extra_spec.keys()[0],
- extra_spec)
+ resp, body = self.client.update_volume_type_extra_specs(
+ self.volume_type['id'],
+ extra_spec.keys()[0],
+ extra_spec)
self.assertEqual(200, resp.status)
self.assertTrue('spec2' in body,
"Volume type extra spec incorrectly updated")
@@ -74,26 +77,28 @@
except Exception:
self.fail("Couldnt update volume type extra spec")
+ @attr(type=['smoke', 'gate'])
def test_volume_type_extra_spec_create_get_delete(self):
# Create/Get/Delete volume type extra spec.
try:
extra_specs = {"spec3": "val1"}
- resp, body = self.client.\
- create_volume_type_extra_specs(self.volume_type['id'], extra_specs)
+ resp, body = self.client.create_volume_type_extra_specs(
+ self.volume_type['id'],
+ extra_specs)
self.assertEqual(200, resp.status)
self.assertEqual(extra_specs, body,
"Volume type extra spec incorrectly created")
- resp, fetched_vol_type_extra_spec = self.client.\
- get_volume_type_extra_specs(self.volume_type['id'],
- extra_specs.keys()[0])
+ resp, _ = self.client.get_volume_type_extra_specs(
+ self.volume_type['id'],
+ extra_specs.keys()[0])
self.assertEqual(200, resp.status)
self.assertEqual(extra_specs, body,
"Volume type extra spec incorrectly fetched")
- resp, _ = self.client.\
- delete_volume_type_extra_specs(self.volume_type['id'],
- extra_specs.keys()[0])
+ resp, _ = self.client.delete_volume_type_extra_specs(
+ self.volume_type['id'],
+ extra_specs.keys()[0])
self.assertEqual(202, resp.status)
except Exception:
self.fail("Could not create a volume_type extra spec")
diff --git a/tempest/tests/volume/admin/test_volume_types_extra_specs_negative.py b/tempest/tests/volume/admin/test_volume_types_extra_specs_negative.py
index 13fcbbf..4a1a0b2 100644
--- a/tempest/tests/volume/admin/test_volume_types_extra_specs_negative.py
+++ b/tempest/tests/volume/admin/test_volume_types_extra_specs_negative.py
@@ -19,6 +19,7 @@
from tempest.common.utils.data_utils import rand_name
from tempest import exceptions
+from tempest.test import attr
from tempest.tests.volume import base
@@ -36,9 +37,10 @@
@classmethod
def tearDownClass(cls):
- super(ExtraSpecsNegativeTest, cls).tearDownClass()
cls.client.delete_volume_type(cls.volume_type['id'])
+ super(ExtraSpecsNegativeTest, cls).tearDownClass()
+ @attr(type='gate')
def test_update_no_body(self):
# Should not update volume type extra specs with no body
extra_spec = {"spec1": "val2"}
@@ -46,6 +48,7 @@
self.client.update_volume_type_extra_specs,
self.volume_type['id'], extra_spec.keys()[0], None)
+ @attr(type='gate')
def test_update_nonexistent_extra_spec_id(self):
# Should not update volume type extra specs with nonexistent id.
extra_spec = {"spec1": "val2"}
@@ -54,6 +57,7 @@
self.volume_type['id'], str(uuid.uuid4()),
extra_spec)
+ @attr(type='gate')
def test_update_none_extra_spec_id(self):
# Should not update volume type extra specs with none id.
extra_spec = {"spec1": "val2"}
@@ -61,6 +65,7 @@
self.client.update_volume_type_extra_specs,
self.volume_type['id'], None, extra_spec)
+ @attr(type='gate')
def test_update_multiple_extra_spec(self):
# Should not update volume type extra specs with multiple specs as
# body.
@@ -70,6 +75,7 @@
self.volume_type['id'], extra_spec.keys()[0],
extra_spec)
+ @attr(type='gate')
def test_create_nonexistent_type_id(self):
# Should not create volume type extra spec for nonexistent volume
# type id.
@@ -78,18 +84,21 @@
self.client.create_volume_type_extra_specs,
str(uuid.uuid4()), extra_specs)
+ @attr(type='gate')
def test_create_none_body(self):
# Should not create volume type extra spec for none POST body.
self.assertRaises(exceptions.BadRequest,
self.client.create_volume_type_extra_specs,
self.volume_type['id'], None)
+ @attr(type='gate')
def test_create_invalid_body(self):
# Should not create volume type extra spec for invalid POST body.
self.assertRaises(exceptions.BadRequest,
self.client.create_volume_type_extra_specs,
self.volume_type['id'], ['invalid'])
+ @attr(type='gate')
def test_delete_nonexistent_volume_type_id(self):
# Should not delete volume type extra spec for nonexistent
# type id.
@@ -98,12 +107,14 @@
self.client.delete_volume_type_extra_specs,
str(uuid.uuid4()), extra_specs.keys()[0])
+ @attr(type='gate')
def test_list_nonexistent_volume_type_id(self):
# Should not list volume type extra spec for nonexistent type id.
self.assertRaises(exceptions.NotFound,
self.client.list_volume_types_extra_specs,
str(uuid.uuid4()))
+ @attr(type='gate')
def test_get_nonexistent_volume_type_id(self):
# Should not get volume type extra spec for nonexistent type id.
extra_specs = {"spec1": "val1"}
@@ -111,6 +122,7 @@
self.client.get_volume_type_extra_specs,
str(uuid.uuid4()), extra_specs.keys()[0])
+ @attr(type='gate')
def test_get_nonexistent_extra_spec_id(self):
# Should not get volume type extra spec for nonexistent extra spec
# id.
diff --git a/tempest/tests/volume/admin/test_volume_types_negative.py b/tempest/tests/volume/admin/test_volume_types_negative.py
index daf804d..bd358b8 100644
--- a/tempest/tests/volume/admin/test_volume_types_negative.py
+++ b/tempest/tests/volume/admin/test_volume_types_negative.py
@@ -18,12 +18,14 @@
import uuid
from tempest import exceptions
+from tempest.test import attr
from tempest.tests.volume import base
class VolumeTypesNegativeTest(base.BaseVolumeAdminTest):
_interface = 'json'
+ @attr(type='gate')
def test_create_with_nonexistent_volume_type(self):
# Should not be able to create volume with nonexistent volume_type.
self.assertRaises(exceptions.NotFound,
@@ -31,16 +33,19 @@
display_name=str(uuid.uuid4()),
volume_type=str(uuid.uuid4()))
+ @attr(type='gate')
def test_create_with_empty_name(self):
# Should not be able to create volume type with an empty name.
self.assertRaises(exceptions.BadRequest,
self.client.create_volume_type, '')
+ @attr(type='gate')
def test_get_nonexistent_type_id(self):
# Should not be able to get volume type with nonexistent type id.
self.assertRaises(exceptions.NotFound, self.client.get_volume_type,
str(uuid.uuid4()))
+ @attr(type='gate')
def test_delete_nonexistent_type_id(self):
# Should not be able to delete volume type with nonexistent type id.
self.assertRaises(exceptions.NotFound, self.client.delete_volume_type,
diff --git a/tempest/tests/volume/base.py b/tempest/tests/volume/base.py
index 00e8668..978ec53 100644
--- a/tempest/tests/volume/base.py
+++ b/tempest/tests/volume/base.py
@@ -149,13 +149,13 @@
def clear_volumes(cls):
for volume in cls.volumes:
try:
- cls.volume_client.delete_volume(volume['id'])
+ cls.volumes_client.delete_volume(volume['id'])
except Exception:
pass
for volume in cls.volumes:
try:
- cls.servers_client.wait_for_resource_deletion(volume['id'])
+ cls.volumes_client.wait_for_resource_deletion(volume['id'])
except Exception:
pass
diff --git a/tempest/tests/volume/test_volumes_actions.py b/tempest/tests/volume/test_volumes_actions.py
index fb9b975..5396fa4 100644
--- a/tempest/tests/volume/test_volumes_actions.py
+++ b/tempest/tests/volume/test_volumes_actions.py
@@ -43,7 +43,6 @@
@classmethod
def tearDownClass(cls):
- super(VolumesActionsTest, cls).tearDownClass()
# Delete the test instance and volume
cls.client.delete_volume(cls.volume['id'])
cls.client.wait_for_resource_deletion(cls.volume['id'])
@@ -51,7 +50,9 @@
cls.servers_client.delete_server(cls.server['id'])
cls.client.wait_for_resource_deletion(cls.server['id'])
- @attr(type='smoke')
+ super(VolumesActionsTest, cls).tearDownClass()
+
+ @attr(type=['smoke', 'gate'])
def test_attach_detach_volume_to_instance(self):
# Volume is attached and detached successfully from an instance
try:
@@ -69,6 +70,7 @@
self.assertEqual(202, resp.status)
self.client.wait_for_volume_status(self.volume['id'], 'available')
+ @attr(type='gate')
def test_get_volume_attachment(self):
# Verify that a volume's attachment information is retrieved
mountpoint = '/dev/vdc'
diff --git a/tempest/tests/volume/test_volumes_get.py b/tempest/tests/volume/test_volumes_get.py
index 8e80e18..fdaf09b 100644
--- a/tempest/tests/volume/test_volumes_get.py
+++ b/tempest/tests/volume/test_volumes_get.py
@@ -78,7 +78,7 @@
self.assertEqual(202, resp.status)
self.client.wait_for_resource_deletion(volume['id'])
- @attr(type='positive')
+ @attr(type='gate')
def test_volume_get_metadata_none(self):
# Create a volume without passing metadata, get details, and delete
try:
@@ -105,11 +105,11 @@
self.assertEqual(202, resp.status)
self.client.wait_for_resource_deletion(volume['id'])
- @attr(type='smoke')
+ @attr(type=['smoke', 'gate'])
def test_volume_create_get_delete(self):
self._volume_create_get_delete(image_ref=None)
- @attr(type='smoke')
+ @attr(type=['smoke', 'gate'])
def test_volume_from_image(self):
self._volume_create_get_delete(image_ref=self.config.compute.image_ref)
diff --git a/tempest/tests/volume/test_volumes_list.py b/tempest/tests/volume/test_volumes_list.py
index a8fedb9..2468705 100644
--- a/tempest/tests/volume/test_volumes_list.py
+++ b/tempest/tests/volume/test_volumes_list.py
@@ -76,7 +76,7 @@
cls.client.wait_for_resource_deletion(volid)
super(VolumesListTest, cls).tearDownClass()
- @attr(type='smoke')
+ @attr(type=['smoke', 'gate'])
def test_volume_list(self):
# Get a list of Volumes
# Fetch all volumes
@@ -89,7 +89,7 @@
', '.join(m_vol['display_name']
for m_vol in missing_vols))
- @attr(type='smoke')
+ @attr(type='gate')
def test_volume_list_with_details(self):
# Get a list of Volumes with details
# Fetch all Volumes
diff --git a/tempest/tests/volume/test_volumes_negative.py b/tempest/tests/volume/test_volumes_negative.py
index c7d4374..f02bb3f 100644
--- a/tempest/tests/volume/test_volumes_negative.py
+++ b/tempest/tests/volume/test_volumes_negative.py
@@ -17,6 +17,7 @@
from tempest.common.utils.data_utils import rand_name
from tempest import exceptions
+from tempest.test import attr
from tempest.tests.volume import base
@@ -28,6 +29,7 @@
super(VolumesNegativeTest, cls).setUpClass()
cls.client = cls.volumes_client
+ @attr(type='gate')
def test_volume_get_nonexistant_volume_id(self):
# Should not be able to get a nonexistant volume
#Creating a nonexistant volume id
@@ -43,6 +45,7 @@
self.assertRaises(exceptions.NotFound, self.client.get_volume,
non_exist_id)
+ @attr(type='gate')
def test_volume_delete_nonexistant_volume_id(self):
# Should not be able to delete a nonexistant Volume
# Creating nonexistant volume id
@@ -58,6 +61,7 @@
self.assertRaises(exceptions.NotFound, self.client.delete_volume,
non_exist_id)
+ @attr(type='gate')
def test_create_volume_with_invalid_size(self):
# Should not be able to create volume with invalid size
# in request
@@ -66,6 +70,7 @@
self.assertRaises(exceptions.BadRequest, self.client.create_volume,
size='#$%', display_name=v_name, metadata=metadata)
+ @attr(type='gate')
def test_create_volume_with_out_passing_size(self):
# Should not be able to create volume without passing size
# in request
@@ -74,6 +79,7 @@
self.assertRaises(exceptions.BadRequest, self.client.create_volume,
size='', display_name=v_name, metadata=metadata)
+ @attr(type='gate')
def test_create_volume_with_size_zero(self):
# Should not be able to create volume with size zero
v_name = rand_name('Volume-')
@@ -81,20 +87,24 @@
self.assertRaises(exceptions.BadRequest, self.client.create_volume,
size='0', display_name=v_name, metadata=metadata)
+ @attr(type='gate')
def test_get_invalid_volume_id(self):
# Should not be able to get volume with invalid id
self.assertRaises(exceptions.NotFound, self.client.get_volume,
'#$%%&^&^')
+ @attr(type='gate')
def test_get_volume_without_passing_volume_id(self):
# Should not be able to get volume when empty ID is passed
self.assertRaises(exceptions.NotFound, self.client.get_volume, '')
+ @attr(type='gate')
def test_delete_invalid_volume_id(self):
# Should not be able to delete volume when invalid ID is passed
self.assertRaises(exceptions.NotFound, self.client.delete_volume,
'!@#$%^&*()')
+ @attr(type='gate')
def test_delete_volume_without_passing_volume_id(self):
# Should not be able to delete volume when empty ID is passed
self.assertRaises(exceptions.NotFound, self.client.delete_volume, '')
diff --git a/tempest/tests/volume/test_volumes_snapshots.py b/tempest/tests/volume/test_volumes_snapshots.py
index e7fa97d..edc02ac 100644
--- a/tempest/tests/volume/test_volumes_snapshots.py
+++ b/tempest/tests/volume/test_volumes_snapshots.py
@@ -12,27 +12,60 @@
# License for the specific language governing permissions and limitations
# under the License.
+import logging
+
+from tempest.test import attr
from tempest.tests.volume import base
+LOG = logging.getLogger(__name__)
+
class VolumesSnapshotTest(base.BaseVolumeTest):
_interface = "json"
- def test_volume_from_snapshot(self):
- volume_origin = self.create_volume(size=1)
- snapshot = self.create_snapshot(volume_origin['id'])
- volume_snap = self.create_volume(size=1,
- snapshot_id=
- snapshot['id'])
+ @classmethod
+ def setUpClass(cls):
+ super(VolumesSnapshotTest, cls).setUpClass()
+ try:
+ cls.volume_origin = cls.create_volume()
+ except Exception:
+ LOG.exception("setup failed")
+ cls.tearDownClass()
+ raise
+
+ @classmethod
+ def tearDownClass(cls):
+ super(VolumesSnapshotTest, cls).tearDownClass()
+
+ @attr(type=['smoke', 'gate'])
+ def test_snapshot_create_get_delete(self):
+ # Create a snapshot, get some of the details and then deletes it
+ resp, snapshot = self.snapshots_client.create_snapshot(
+ self.volume_origin['id'])
+ self.assertEqual(200, resp.status)
+ self.snapshots_client.wait_for_snapshot_status(snapshot['id'],
+ 'available')
+ errmsg = "Referred volume origin ID mismatch"
+ self.assertEqual(self.volume_origin['id'],
+ snapshot['volume_id'],
+ errmsg)
self.snapshots_client.delete_snapshot(snapshot['id'])
- self.volumes_client.delete_volume(volume_snap['id'])
self.snapshots_client.wait_for_resource_deletion(snapshot['id'])
- self.snapshots.remove(snapshot)
- self.volumes_client.delete_volume(volume_origin['id'])
- self.volumes_client.wait_for_resource_deletion(volume_snap['id'])
- self.volumes.remove(volume_snap)
- self.volumes_client.wait_for_resource_deletion(volume_origin['id'])
- self.volumes.remove(volume_origin)
+
+ @attr(type=['smoke', 'gate'])
+ def test_volume_from_snapshot(self):
+ # Create a temporary snap using wrapper method from base, then
+ # create a snap based volume, check resp code and deletes it
+ snapshot = self.create_snapshot(self.volume_origin['id'])
+ # NOTE: size is required also when passing snapshot_id
+ resp, volume = self.volumes_client.create_volume(
+ size=1,
+ snapshot_id=snapshot['id'])
+ self.assertEqual(200, resp.status)
+ self.volumes_client.wait_for_volume_status(volume['id'], 'available')
+ self.volumes_client.delete_volume(volume['id'])
+ self.volumes_client.wait_for_resource_deletion(volume['id'])
+ self.clear_snapshots()
class VolumesSnapshotTestXML(VolumesSnapshotTest):
diff --git a/tempest/whitebox.py b/tempest/whitebox.py
index bfcc373..cf9fff0 100644
--- a/tempest/whitebox.py
+++ b/tempest/whitebox.py
@@ -111,7 +111,7 @@
image_id = cls.image_ref
resp, server = cls.servers_client.create_server(
- server_name, image_id, flavor)
+ server_name, image_id, flavor)
cls.servers_client.wait_for_server_status(server['id'], 'ACTIVE')
cls.servers.append(server)
return server
diff --git a/tools/check_source.sh b/tools/check_source.sh
deleted file mode 100755
index 089ad70..0000000
--- a/tools/check_source.sh
+++ /dev/null
@@ -1,24 +0,0 @@
-#!/usr/bin/env bash
-
-python tools/hacking.py --ignore=E122,E125,E126 --repeat --show-source --exclude=.venv,.tox,dist,doc,openstack,*egg .
-pep8_ret=$?
-
-pyflakes tempest stress setup.py tools cli bin | grep "imported but unused"
-unused_ret=$?
-
-ret=0
-if [ $pep8_ret != 0 ]; then
- echo "hacking.py/pep8 test FAILED!" >&2
- (( ret += 1 ))
-else
- echo "hacking.py/pep8 test OK!" >&2
-fi
-
-if [ $unused_ret == 0 ]; then
- echo "Unused import test FAILED!" >&2
- (( ret += 2 ))
-else
- echo "Unused import test OK!" >&2
-fi
-
-exit $ret
diff --git a/tools/hacking.py b/tools/hacking.py
deleted file mode 100755
index 7e46b74..0000000
--- a/tools/hacking.py
+++ /dev/null
@@ -1,525 +0,0 @@
-#!/usr/bin/env python
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright (c) 2012, Cloudscaling
-# All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-"""tempest HACKING file compliance testing
-
-built on top of pep8.py
-"""
-
-import inspect
-import logging
-import os
-import re
-import subprocess
-import sys
-import tokenize
-import warnings
-
-import pep8
-
-# Don't need this for testing
-logging.disable('LOG')
-
-#T1xx comments
-#T2xx except
-#T3xx imports
-#T4xx docstrings
-#T5xx dictionaries/lists
-#T6xx calling methods
-#T7xx localization
-#N8xx git commit messages
-
-IMPORT_EXCEPTIONS = ['sqlalchemy', 'migrate']
-DOCSTRING_TRIPLE = ['"""', "'''"]
-VERBOSE_MISSING_IMPORT = os.getenv('HACKING_VERBOSE_MISSING_IMPORT', 'False')
-
-
-# Monkey patch broken excluded filter in pep8
-# See https://github.com/jcrocholl/pep8/pull/111
-def excluded(self, filename):
- """
- Check if options.exclude contains a pattern that matches filename.
- """
- basename = os.path.basename(filename)
- return any((pep8.filename_match(filename, self.options.exclude,
- default=False),
- pep8.filename_match(basename, self.options.exclude,
- default=False)))
-
-
-def input_dir(self, dirname):
- """Check all files in this directory and all subdirectories."""
- dirname = dirname.rstrip('/')
- if self.excluded(dirname):
- return 0
- counters = self.options.report.counters
- verbose = self.options.verbose
- filepatterns = self.options.filename
- runner = self.runner
- for root, dirs, files in os.walk(dirname):
- if verbose:
- print('directory ' + root)
- counters['directories'] += 1
- for subdir in sorted(dirs):
- if self.excluded(os.path.join(root, subdir)):
- dirs.remove(subdir)
- for filename in sorted(files):
- # contain a pattern that matches?
- if ((pep8.filename_match(filename, filepatterns) and
- not self.excluded(filename))):
- runner(os.path.join(root, filename))
-
-
-def is_import_exception(mod):
- return (mod in IMPORT_EXCEPTIONS or
- any(mod.startswith(m + '.') for m in IMPORT_EXCEPTIONS))
-
-
-def import_normalize(line):
- # convert "from x import y" to "import x.y"
- # handle "from x import y as z" to "import x.y as z"
- split_line = line.split()
- if ("import" in line and line.startswith("from ") and "," not in line and
- split_line[2] == "import" and split_line[3] != "*" and
- split_line[1] != "__future__" and
- (len(split_line) == 4 or
- (len(split_line) == 6 and split_line[4] == "as"))):
- return "import %s.%s" % (split_line[1], split_line[3])
- else:
- return line
-
-
-def tempest_todo_format(physical_line):
- """Check for 'TODO()'.
-
- tempest HACKING guide recommendation for TODO:
- Include your name with TODOs as in "#TODO(termie)"
- T101
- """
- pos = physical_line.find('TODO')
- pos1 = physical_line.find('TODO(')
- pos2 = physical_line.find('#') # make sure it's a comment
- if (pos != pos1 and pos2 >= 0 and pos2 < pos):
- return pos, "T101: Use TODO(NAME)"
-
-
-def tempest_except_format(logical_line):
- """Check for 'except:'.
-
- tempest HACKING guide recommends not using except:
- Do not write "except:", use "except Exception:" at the very least
- T201
- """
- if logical_line.startswith("except:"):
- yield 6, "T201: no 'except:' at least use 'except Exception:'"
-
-
-def tempest_except_format_assert(logical_line):
- """Check for 'assertRaises(Exception'.
-
- tempest HACKING guide recommends not using assertRaises(Exception...):
- Do not use overly broad Exception type
- T202
- """
- if logical_line.startswith("self.assertRaises(Exception"):
- yield 1, "T202: assertRaises Exception too broad"
-
-
-def tempest_one_import_per_line(logical_line):
- """Check for import format.
-
- tempest HACKING guide recommends one import per line:
- Do not import more than one module per line
-
- Examples:
- BAD: from tempest.common.rest_client import RestClient, RestClientXML
- T301
- """
- pos = logical_line.find(',')
- parts = logical_line.split()
- if (pos > -1 and (parts[0] == "import" or
- parts[0] == "from" and parts[2] == "import") and
- not is_import_exception(parts[1])):
- yield pos, "T301: one import per line"
-
-_missingImport = set([])
-
-
-def tempest_import_module_only(logical_line):
- """Check for import module only.
-
- tempest HACKING guide recommends importing only modules:
- Do not import objects, only modules
- T302 import only modules
- T303 Invalid Import
- T304 Relative Import
- """
- def importModuleCheck(mod, parent=None, added=False):
- """
- If can't find module on first try, recursively check for relative
- imports
- """
- current_path = os.path.dirname(pep8.current_file)
- try:
- with warnings.catch_warnings():
- warnings.simplefilter('ignore', DeprecationWarning)
- valid = True
- if parent:
- if is_import_exception(parent):
- return
- parent_mod = __import__(parent, globals(), locals(),
- [mod], -1)
- valid = inspect.ismodule(getattr(parent_mod, mod))
- else:
- __import__(mod, globals(), locals(), [], -1)
- valid = inspect.ismodule(sys.modules[mod])
- if not valid:
- if added:
- sys.path.pop()
- added = False
- return logical_line.find(mod), ("T304: No "
- "relative imports. "
- "'%s' is a relative "
- "import"
- % logical_line)
- return logical_line.find(mod), ("T302: import only"
- " modules. '%s' does not "
- "import a module"
- % logical_line)
-
- except (ImportError, NameError) as exc:
- if not added:
- added = True
- sys.path.append(current_path)
- return importModuleCheck(mod, parent, added)
- else:
- name = logical_line.split()[1]
- if name not in _missingImport:
- if VERBOSE_MISSING_IMPORT != 'False':
- print >> sys.stderr, ("ERROR: import '%s' in %s "
- "failed: %s" %
- (name, pep8.current_file, exc))
- _missingImport.add(name)
- added = False
- sys.path.pop()
- return
-
- except AttributeError:
- # Invalid import
- return logical_line.find(mod), ("T303: Invalid import, "
- "AttributeError raised")
-
- # convert "from x import y" to " import x.y"
- # convert "from x import y as z" to " import x.y"
- import_normalize(logical_line)
- split_line = logical_line.split()
-
- if (logical_line.startswith("import ") and "," not in logical_line and
- (len(split_line) == 2 or
- (len(split_line) == 4 and split_line[2] == "as"))):
- mod = split_line[1]
- rval = importModuleCheck(mod)
- if rval is not None:
- yield rval
-
- # TODO(jogo) handle "from x import *"
-
-#TODO(jogo): import template: T305
-
-
-def tempest_import_alphabetical(logical_line, line_number, lines):
- """Check for imports in alphabetical order.
-
- Tempest HACKING guide recommendation for imports:
- imports in human alphabetical order
- T306
- """
- # handle import x
- # use .lower since capitalization shouldn't dictate order
- split_line = import_normalize(logical_line.strip()).lower().split()
- split_previous = import_normalize(lines[
- line_number - 2]).strip().lower().split()
- # with or without "as y"
- length = [2, 4]
- if (len(split_line) in length and len(split_previous) in length and
- split_line[0] == "import" and split_previous[0] == "import"):
- if split_line[1] < split_previous[1]:
- yield (0, "T306: imports not in alphabetical order"
- " (%s, %s)"
- % (split_previous[1], split_line[1]))
-
-
-def tempest_docstring_start_space(physical_line):
- """Check for docstring not start with space.
-
- tempest HACKING guide recommendation for docstring:
- Docstring should not start with space
- T401
- """
- pos = max([physical_line.find(i) for i in DOCSTRING_TRIPLE]) # start
- end = max([physical_line[-4:-1] == i for i in DOCSTRING_TRIPLE]) # end
- if (pos != -1 and end and len(physical_line) > pos + 4):
- if (physical_line[pos + 3] == ' '):
- return (pos, "T401: one line docstring should not start"
- " with a space")
-
-
-def tempest_docstring_one_line(physical_line):
- """Check one line docstring end.
-
- tempest HACKING guide recommendation for one line docstring:
- A one line docstring looks like this and ends in a period.
- T402
- """
- pos = max([physical_line.find(i) for i in DOCSTRING_TRIPLE]) # start
- end = max([physical_line[-4:-1] == i for i in DOCSTRING_TRIPLE]) # end
- if (pos != -1 and end and len(physical_line) > pos + 4):
- if (physical_line[-5] != '.'):
- return pos, "T402: one line docstring needs a period"
-
-
-def tempest_docstring_multiline_end(physical_line):
- """Check multi line docstring end.
-
- Tempest HACKING guide recommendation for docstring:
- Docstring should end on a new line
- T403
- """
- pos = max([physical_line.find(i) for i in DOCSTRING_TRIPLE]) # start
- if (pos != -1 and len(physical_line) == pos):
- if (physical_line[pos + 3] == ' '):
- return (pos, "T403: multi line docstring end on new line")
-
-
-def tempest_no_test_docstring(physical_line, previous_logical, filename):
- """Check that test_ functions don't have docstrings
-
- This ensure we get better results out of tempest, instead
- of them being hidden behind generic descriptions of the
- functions.
-
- T404
- """
- if "tempest/test" in filename:
- pos = max([physical_line.find(i) for i in DOCSTRING_TRIPLE])
- if pos != -1:
- if previous_logical.startswith("def test_"):
- return (pos, "T404: test functions must "
- "not have doc strings")
-
-SKIP_DECORATOR = '@testtools.skip('
-
-
-def tempest_skip_bugs(physical_line):
- """Check skip lines for proper bug entries
-
- T601: Bug not in skip line
- T602: Bug in message formatted incorrectly
- """
-
- pos = physical_line.find(SKIP_DECORATOR)
-
- skip_re = re.compile(r'^\s*@testtools.skip.*')
-
- if pos != -1 and skip_re.match(physical_line):
- bug = re.compile(r'^.*\bbug\b.*', re.IGNORECASE)
- if bug.match(physical_line) is None:
- return (pos, 'T601: skips must have an associated bug')
-
- bug_re = re.compile(r'.*skip\(.*Bug\s\#\d+', re.IGNORECASE)
-
- if bug_re.match(physical_line) is None:
- return (pos, 'T602: Bug number formatted incorrectly')
-
-
-FORMAT_RE = re.compile("%(?:"
- "%|" # Ignore plain percents
- "(\(\w+\))?" # mapping key
- "([#0 +-]?" # flag
- "(?:\d+|\*)?" # width
- "(?:\.\d+)?" # precision
- "[hlL]?" # length mod
- "\w))") # type
-
-
-class LocalizationError(Exception):
- pass
-
-
-def check_i18n():
- """Generator that checks token stream for localization errors.
-
- Expects tokens to be ``send``ed one by one.
- Raises LocalizationError if some error is found.
- """
- while True:
- try:
- token_type, text, _, _, line = yield
- except GeneratorExit:
- return
- if (token_type == tokenize.NAME and text == "_" and
- not line.startswith('def _(msg):')):
-
- while True:
- token_type, text, start, _, _ = yield
- if token_type != tokenize.NL:
- break
- if token_type != tokenize.OP or text != "(":
- continue # not a localization call
-
- format_string = ''
- while True:
- token_type, text, start, _, _ = yield
- if token_type == tokenize.STRING:
- format_string += eval(text)
- elif token_type == tokenize.NL:
- pass
- else:
- break
-
- if not format_string:
- raise LocalizationError(start,
- "T701: Empty localization "
- "string")
- if token_type != tokenize.OP:
- raise LocalizationError(start,
- "T701: Invalid localization "
- "call")
- if text != ")":
- if text == "%":
- raise LocalizationError(start,
- "T702: Formatting "
- "operation should be outside"
- " of localization method call")
- elif text == "+":
- raise LocalizationError(start,
- "T702: Use bare string "
- "concatenation instead of +")
- else:
- raise LocalizationError(start,
- "T702: Argument to _ must"
- " be just a string")
-
- format_specs = FORMAT_RE.findall(format_string)
- positional_specs = [(key, spec) for key, spec in format_specs
- if not key and spec]
- # not spec means %%, key means %(smth)s
- if len(positional_specs) > 1:
- raise LocalizationError(start,
- "T703: Multiple positional "
- "placeholders")
-
-
-def tempest_localization_strings(logical_line, tokens):
- """Check localization in line.
-
- T701: bad localization call
- T702: complex expression instead of string as argument to _()
- T703: multiple positional placeholders
- """
-
- gen = check_i18n()
- next(gen)
- try:
- map(gen.send, tokens)
- gen.close()
- except LocalizationError as e:
- yield e.args
-
-#TODO(jogo) Dict and list objects
-
-current_file = ""
-
-
-def readlines(filename):
- """Record the current file being tested."""
- pep8.current_file = filename
- return open(filename).readlines()
-
-
-def add_tempest():
- """Monkey patch in tempest guidelines.
-
- Look for functions that start with tempest_ and have arguments
- and add them to pep8 module
- Assumes you know how to write pep8.py checks
- """
- for name, function in globals().items():
- if not inspect.isfunction(function):
- continue
- args = inspect.getargspec(function)[0]
- if args and name.startswith("tempest"):
- exec("pep8.%s = %s" % (name, name))
-
-
-def once_git_check_commit_title():
- """Check git commit messages.
-
- tempest HACKING recommends not referencing a bug or blueprint
- in first line, it should provide an accurate description of the change
- T801
- T802 Title limited to 50 chars
- """
- #Get title of most recent commit
-
- subp = subprocess.Popen(['git', 'log', '--no-merges', '--pretty=%s', '-1'],
- stdout=subprocess.PIPE)
- title = subp.communicate()[0]
- if subp.returncode:
- raise Exception("git log failed with code %s" % subp.returncode)
-
- #From https://github.com/openstack/openstack-ci-puppet
- # /blob/master/modules/gerrit/manifests/init.pp#L74
- #Changeid|bug|blueprint
- git_keywords = (r'(I[0-9a-f]{8,40})|'
- '([Bb]ug|[Ll][Pp])[\s\#:]*(\d+)|'
- '([Bb]lue[Pp]rint|[Bb][Pp])[\s\#:]*([A-Za-z0-9\\-]+)')
- GIT_REGEX = re.compile(git_keywords)
-
- error = False
- #NOTE(jogo) if match regex but over 3 words, acceptable title
- if GIT_REGEX.search(title) is not None and len(title.split()) <= 3:
- print ("T801: git commit title ('%s') should provide an accurate "
- "description of the change, not just a reference to a bug "
- "or blueprint" % title.strip())
- error = True
- if len(title.decode('utf-8')) > 72:
- print ("T802: git commit title ('%s') should be under 50 chars"
- % title.strip())
- error = True
- return error
-
-if __name__ == "__main__":
- #include tempest path
- sys.path.append(os.getcwd())
- #Run once tests (not per line)
- once_error = once_git_check_commit_title()
- #TEMPEST error codes start with a T
- pep8.ERRORCODE_REGEX = re.compile(r'[EWT]\d{3}')
- add_tempest()
- pep8.current_file = current_file
- pep8.readlines = readlines
- pep8.StyleGuide.excluded = excluded
- pep8.StyleGuide.input_dir = input_dir
- try:
- pep8._main()
- sys.exit(once_error)
- finally:
- if len(_missingImport) > 0:
- print >> sys.stderr, ("%i imports missing in this test environment"
- % len(_missingImport))
diff --git a/tools/install_venv.py b/tools/install_venv.py
index ef7b0a8..5d4b290 100644
--- a/tools/install_venv.py
+++ b/tools/install_venv.py
@@ -3,7 +3,7 @@
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
-#
+# flake8: noqa
# Copyright 2010 OpenStack, LLC
# Copyright 2013 IBM Corp.
#
diff --git a/tools/pip-requires b/tools/pip-requires
index 758442c..4873d75 100644
--- a/tools/pip-requires
+++ b/tools/pip-requires
@@ -1,3 +1,5 @@
+d2to1>=0.2.10,<0.3
+pbr>=0.5,<0.6
anyjson
nose
httplib2>=0.7.0
diff --git a/tools/tempest_coverage.py b/tools/tempest_coverage.py
index 9dcbd46..5b926f9 100755
--- a/tools/tempest_coverage.py
+++ b/tools/tempest_coverage.py
@@ -165,7 +165,7 @@
resp, body = coverage_client.report_coverage_xml(file=CLI.filename)
elif CLI.html:
resp, body = coverage_client.report_coverage_html(
- file=CLI.filename)
+ file=CLI.filename)
else:
resp, body = coverage_client.report_coverage(file=CLI.filename)
if not resp['status'] == '200':
diff --git a/tools/test-requires b/tools/test-requires
index f701dab..cba42a4 100644
--- a/tools/test-requires
+++ b/tools/test-requires
@@ -1,5 +1,8 @@
-pep8==1.3.3
-pylint==0.19
+# Install bounded pep8/pyflakes first, then let flake8 install
+pep8==1.4.5
+pyflakes==0.7.2
+flake8==2.0
+hacking>=0.5.3,<0.6
+#
#TODO(afazekas): ensure pg_config installed
psycopg2
-pyflakes
diff --git a/tox.ini b/tox.ini
index 85a0d86..7d3d245 100644
--- a/tox.ini
+++ b/tox.ini
@@ -9,14 +9,52 @@
NOSE_OPENSTACK_YELLOW=3
NOSE_OPENSTACK_SHOW_ELAPSED=1
NOSE_OPENSTACK_STDOUT=1
-deps = -r{toxinidir}/tools/pip-requires
- -r{toxinidir}/tools/test-requires
-commands = nosetests {posargs}
+
+[testenv:full]
+sitepackages = True
+setenv = VIRTUAL_ENV={envdir}
+ NOSE_WITH_OPENSTACK=1
+ NOSE_OPENSTACK_COLOR=1
+ NOSE_OPENSTACK_RED=15
+ NOSE_OPENSTACK_YELLOW=3
+ NOSE_OPENSTACK_SHOW_ELAPSED=1
+ NOSE_OPENSTACK_STDOUT=1
+commands =
+ nosetests --logging-format '%(asctime)-15s %(message)s' --with-xunit --xunit-file=nosetests-full.xml -sv tempest/tests tempest/cli
+
+[testenv:smoke]
+sitepackages = True
+setenv = VIRTUAL_ENV={envdir}
+ NOSE_WITH_OPENSTACK=1
+ NOSE_OPENSTACK_COLOR=1
+ NOSE_OPENSTACK_RED=15
+ NOSE_OPENSTACK_YELLOW=3
+ NOSE_OPENSTACK_SHOW_ELAPSED=1
+ NOSE_OPENSTACK_STDOUT=1
+commands =
+ nosetests --logging-format '%(asctime)-15s %(message)s' --with-xunit -sv --attr=type=smoke --xunit-file=nosetests-smoke.xml tempest
+
[testenv:coverage]
-commands = python -m tools/tempest_coverage -c start --combine
- nosetests {posargs}
- python -m tools/tempest_coverage -c report --html
+sitepackages = True
+setenv = VIRTUAL_ENV={envdir}
+ NOSE_WITH_OPENSTACK=1
+ NOSE_OPENSTACK_COLOR=1
+ NOSE_OPENSTACK_RED=15
+ NOSE_OPENSTACK_YELLOW=3
+ NOSE_OPENSTACK_SHOW_ELAPSED=1
+ NOSE_OPENSTACK_STDOUT=1
+commands =
+ python -m tools/tempest_coverage -c start --combine
+ nosetests --logging-format '%(asctime)-15s %(message)s' --with-xunit --xunit-file=nosetests-full.xml -sv tempest/tests tempest/cli
+ python -m tools/tempest_coverage -c report --html
[testenv:pep8]
-commands = bash tools/check_source.sh
+commands = flake8
+deps = -r{toxinidir}/tools/pip-requires
+ -r{toxinidir}/tools/test-requires
+
+[flake8]
+ignore = E125,H302,H404
+show-source = True
+exclude = .git,.venv,.tox,dist,doc,openstack,*egg