Merge "Fix the v2 image client variable name"
diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst
index 9a7ce15..32cd3ef 100644
--- a/doc/source/configuration.rst
+++ b/doc/source/configuration.rst
@@ -205,6 +205,8 @@
Enabling Remote Access to Created Servers
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Network Creation/Usage for Servers
+""""""""""""""""""""""""""""""""""
When Tempest creates servers for testing, some tests require being able to
connect those servers. Depending on the configuration of the cloud, the methods
for doing this can be different. In certain configurations it is required to
@@ -214,23 +216,8 @@
run. This section covers the different methods of configuring Tempest to provide
a network when creating servers.
-The ``validation`` group gathers all the connection options to remotely access the
-created servers.
-
-To enable remote access to servers, at least the three following options need to be
-set:
-
-* The ``run_validation`` option needs be set to ``true``.
-
-* The ``connect_method`` option. Two connect methods are available: ``fixed`` and
- ``floating``, the later being set by default.
-
-* The ``auth_method`` option. Currently, only authentication by keypair is
- available.
-
-
Fixed Network Name
-""""""""""""""""""
+''''''''''''''''''
This is the simplest method of specifying how networks should be used. You can
just specify a single network name/label to use for all server creations. The
limitation with this is that all projects and users must be able to see
@@ -252,7 +239,7 @@
Accounts File
-"""""""""""""
+'''''''''''''
If you are using an accounts file to provide credentials for running Tempest
then you can leverage it to also specify which network should be used with
server creations on a per project and user pair basis. This provides
@@ -277,7 +264,7 @@
With Dynamic Credentials
-""""""""""""""""""""""""
+''''''''''''''''''''''''
With dynamic credentials enabled and using nova-network, your only option for
configuration is to either set a fixed network name or not. However, in most
cases it shouldn't matter because nova-network should have no problem booting a
@@ -302,6 +289,34 @@
network available for the server creation, or use ``fixed_network_name`` to
inform Tempest which network to use.
+SSH Connection Configuration
+""""""""""""""""""""""""""""
+There are also several different ways to actually establish a connection and
+authenticate/login on the server. After a server is booted with a provided
+network there are still details needed to know how to actually connect to
+the server. The ``validation`` group gathers all the options regarding
+connecting to and remotely accessing the created servers.
+
+To enable remote access to servers, there are 3 options at a minimum that are used:
+
+ #. ``run_validation``
+ #. ``connect_method``
+ #. ``auth_method``
+
+The ``run_validation`` is used to enable or disable ssh connectivity for
+all tests (with the exception of scenario tests which do not have a flag for
+enabling or disabling ssh) To enable ssh connectivity this needs be set to ``true``.
+
+The ``connect_method`` option is used to tell tempest what kind of IP to use for
+establishing a connection to the server. Two methods are available: ``fixed``
+and ``floating``, the later being set by default. If this is set to floating
+tempest will create a floating ip for the server before attempted to connect
+to it. The IP for the floating ip is what is used for the connection.
+
+For the ``auth_method`` option there is currently, only one valid option,
+``keypair``. With this set to ``keypair`` tempest will create an ssh keypair
+and use that for authenticating against the created server.
+
Configuring Available Services
------------------------------
OpenStack is really a constellation of several different projects which
diff --git a/doc/source/index.rst b/doc/source/index.rst
index 10364db..98b006d 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -51,6 +51,7 @@
account_generator
cleanup
javelin
+ workspace
==================
Indices and tables
diff --git a/doc/source/microversion_testing.rst b/doc/source/microversion_testing.rst
index 3568470..17059e4 100644
--- a/doc/source/microversion_testing.rst
+++ b/doc/source/microversion_testing.rst
@@ -19,6 +19,7 @@
multiple Microversion tests in a single Tempest operation, configuration
options should represent the range of test target Microversions.
New configuration options are:
+
* min_microversion
* max_microversion
@@ -130,8 +131,9 @@
If that range is out of configured Microversion range then, test
will be skipped.
-*NOTE: Microversion testing is supported at test class level not at individual
-test case level.*
+.. note:: Microversion testing is supported at test class level not at
+ individual test case level.
+
For example:
Below test is applicable for Microversion from 2.2 till 2.9::
@@ -211,3 +213,7 @@
* `2.10`_
.. _2.10: http://docs.openstack.org/developer/nova/api_microversion_history.html#id9
+
+ * `2.20`_
+
+ .. _2.20: http://docs.openstack.org/developer/nova/api_microversion_history.html#id18
diff --git a/doc/source/workspace.rst b/doc/source/workspace.rst
new file mode 100644
index 0000000..41325b2
--- /dev/null
+++ b/doc/source/workspace.rst
@@ -0,0 +1,5 @@
+-----------------
+Tempest Workspace
+-----------------
+
+.. automodule:: tempest.cmd.workspace
diff --git a/releasenotes/releasenotes/notes/add-tempest-workspaces-228a2ba4690b5589.yaml b/releasenotes/releasenotes/notes/add-tempest-workspaces-228a2ba4690b5589.yaml
new file mode 100644
index 0000000..9a1cef6
--- /dev/null
+++ b/releasenotes/releasenotes/notes/add-tempest-workspaces-228a2ba4690b5589.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - Adds tempest workspaces command and WorkspaceManager.
+ This is used to have a centralized repository for managing
+ different tempest configurations.
diff --git a/setup.cfg b/setup.cfg
index 24e0214..0bf493c 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -41,6 +41,7 @@
run-stress = tempest.cmd.run_stress:TempestRunStress
list-plugins = tempest.cmd.list_plugins:TempestListPlugins
verify-config = tempest.cmd.verify_tempest_config:TempestVerifyConfig
+ workspace = tempest.cmd.workspace:TempestWorkspace
oslo.config.opts =
tempest.config = tempest.config:list_opts
diff --git a/tempest/api/image/base.py b/tempest/api/image/base.py
index edf16dc..3fefc81 100644
--- a/tempest/api/image/base.py
+++ b/tempest/api/image/base.py
@@ -93,12 +93,14 @@
@classmethod
def setup_clients(cls):
super(BaseV1ImageMembersTest, cls).setup_clients()
+ cls.image_member_client = cls.os.image_member_client
+ cls.alt_image_member_client = cls.os_alt.image_member_client
cls.alt_img_cli = cls.os_alt.image_client
@classmethod
def resource_setup(cls):
super(BaseV1ImageMembersTest, cls).resource_setup()
- cls.alt_tenant_id = cls.alt_img_cli.tenant_id
+ cls.alt_tenant_id = cls.alt_image_member_client.tenant_id
def _create_image(self):
image_file = moves.cStringIO(data_utils.random_bytes())
@@ -123,6 +125,9 @@
def setup_clients(cls):
super(BaseV2ImageTest, cls).setup_clients()
cls.client = cls.os.image_client_v2
+ cls.namespaces_client = cls.os.namespaces_client
+ cls.resource_types_client = cls.os.resource_types_client
+ cls.schemas_client = cls.os.schemas_client
class BaseV2MemberImageTest(BaseV2ImageTest):
diff --git a/tempest/api/image/v1/test_image_members.py b/tempest/api/image/v1/test_image_members.py
index eb6969b..0bad96a 100644
--- a/tempest/api/image/v1/test_image_members.py
+++ b/tempest/api/image/v1/test_image_members.py
@@ -22,8 +22,8 @@
@test.idempotent_id('1d6ef640-3a20-4c84-8710-d95828fdb6ad')
def test_add_image_member(self):
image = self._create_image()
- self.client.add_member(self.alt_tenant_id, image)
- body = self.client.list_image_members(image)
+ self.image_member_client.add_member(self.alt_tenant_id, image)
+ body = self.image_member_client.list_image_members(image)
members = body['members']
members = map(lambda x: x['member_id'], members)
self.assertIn(self.alt_tenant_id, members)
@@ -33,10 +33,11 @@
@test.idempotent_id('6a5328a5-80e8-4b82-bd32-6c061f128da9')
def test_get_shared_images(self):
image = self._create_image()
- self.client.add_member(self.alt_tenant_id, image)
+ self.image_member_client.add_member(self.alt_tenant_id, image)
share_image = self._create_image()
- self.client.add_member(self.alt_tenant_id, share_image)
- body = self.client.list_shared_images(self.alt_tenant_id)
+ self.image_member_client.add_member(self.alt_tenant_id, share_image)
+ body = self.image_member_client.list_shared_images(
+ self.alt_tenant_id)
images = body['shared_images']
images = map(lambda x: x['image_id'], images)
self.assertIn(share_image, images)
@@ -45,8 +46,8 @@
@test.idempotent_id('a76a3191-8948-4b44-a9d6-4053e5f2b138')
def test_remove_member(self):
image_id = self._create_image()
- self.client.add_member(self.alt_tenant_id, image_id)
- self.client.delete_member(self.alt_tenant_id, image_id)
- body = self.client.list_image_members(image_id)
+ self.image_member_client.add_member(self.alt_tenant_id, image_id)
+ self.image_member_client.delete_member(self.alt_tenant_id, image_id)
+ body = self.image_member_client.list_image_members(image_id)
members = body['members']
self.assertEqual(0, len(members), str(members))
diff --git a/tempest/api/image/v1/test_image_members_negative.py b/tempest/api/image/v1/test_image_members_negative.py
index 16a4ba6..d46a836 100644
--- a/tempest/api/image/v1/test_image_members_negative.py
+++ b/tempest/api/image/v1/test_image_members_negative.py
@@ -25,7 +25,8 @@
def test_add_member_with_non_existing_image(self):
# Add member with non existing image.
non_exist_image = data_utils.rand_uuid()
- self.assertRaises(lib_exc.NotFound, self.client.add_member,
+ self.assertRaises(lib_exc.NotFound,
+ self.image_member_client.add_member,
self.alt_tenant_id, non_exist_image)
@test.attr(type=['negative'])
@@ -33,7 +34,8 @@
def test_delete_member_with_non_existing_image(self):
# Delete member with non existing image.
non_exist_image = data_utils.rand_uuid()
- self.assertRaises(lib_exc.NotFound, self.client.delete_member,
+ self.assertRaises(lib_exc.NotFound,
+ self.image_member_client.delete_member,
self.alt_tenant_id, non_exist_image)
@test.attr(type=['negative'])
@@ -42,7 +44,8 @@
# Delete member with non existing tenant.
image_id = self._create_image()
non_exist_tenant = data_utils.rand_uuid_hex()
- self.assertRaises(lib_exc.NotFound, self.client.delete_member,
+ self.assertRaises(lib_exc.NotFound,
+ self.image_member_client.delete_member,
non_exist_tenant, image_id)
@test.attr(type=['negative'])
diff --git a/tempest/api/image/v2/test_images.py b/tempest/api/image/v2/test_images.py
index 04582c6..1fb9c52 100644
--- a/tempest/api/image/v2/test_images.py
+++ b/tempest/api/image/v2/test_images.py
@@ -254,12 +254,12 @@
def test_get_image_schema(self):
# Test to get image schema
schema = "image"
- body = self.client.show_schema(schema)
+ body = self.schemas_client.show_schema(schema)
self.assertEqual("image", body['name'])
@test.idempotent_id('25c8d7b2-df21-460f-87ac-93130bcdc684')
def test_get_images_schema(self):
# Test to get images schema
schema = "images"
- body = self.client.show_schema(schema)
+ body = self.schemas_client.show_schema(schema)
self.assertEqual("images", body['name'])
diff --git a/tempest/api/image/v2/test_images_member.py b/tempest/api/image/v2/test_images_member.py
index 6110f23..fe8dd65 100644
--- a/tempest/api/image/v2/test_images_member.py
+++ b/tempest/api/image/v2/test_images_member.py
@@ -83,12 +83,12 @@
@test.idempotent_id('634dcc3f-f6e2-4409-b8fd-354a0bb25d83')
def test_get_image_member_schema(self):
- body = self.client.show_schema("member")
+ body = self.schemas_client.show_schema("member")
self.assertEqual("member", body['name'])
@test.idempotent_id('6ae916ef-1052-4e11-8d36-b3ae14853cbb')
def test_get_image_members_schema(self):
- body = self.client.show_schema("members")
+ body = self.schemas_client.show_schema("members")
self.assertEqual("members", body['name'])
@test.idempotent_id('cb961424-3f68-4d21-8e36-30ad66fb6bfb')
diff --git a/tempest/api/image/v2/test_images_metadefs_namespaces.py b/tempest/api/image/v2/test_images_metadefs_namespaces.py
index da0f4c1..6fced00 100644
--- a/tempest/api/image/v2/test_images_metadefs_namespaces.py
+++ b/tempest/api/image/v2/test_images_metadefs_namespaces.py
@@ -26,43 +26,47 @@
@test.idempotent_id('319b765e-7f3d-4b3d-8b37-3ca3876ee768')
def test_basic_metadata_definition_namespaces(self):
# get the available resource types and use one resource_type
- body = self.client.list_resource_types()
+ body = self.resource_types_client.list_resource_types()
resource_name = body['resource_types'][0]['name']
name = [{'name': resource_name}]
namespace_name = data_utils.rand_name('namespace')
# create the metadef namespace
- body = self.client.create_namespace(namespace=namespace_name,
- visibility='public',
- description='Tempest',
- display_name=namespace_name,
- resource_type_associations=name,
- protected=True)
+ body = self.namespaces_client.create_namespace(
+ namespace=namespace_name,
+ visibility='public',
+ description='Tempest',
+ display_name=namespace_name,
+ resource_type_associations=name,
+ protected=True)
self.addCleanup(test_utils.call_and_ignore_notfound_exc,
self._cleanup_namespace, namespace_name)
# get namespace details
- body = self.client.show_namespace(namespace_name)
+ body = self.namespaces_client.show_namespace(namespace_name)
self.assertEqual(namespace_name, body['namespace'])
self.assertEqual('public', body['visibility'])
# unable to delete protected namespace
- self.assertRaises(lib_exc.Forbidden, self.client.delete_namespace,
+ self.assertRaises(lib_exc.Forbidden,
+ self.namespaces_client.delete_namespace,
namespace_name)
# update the visibility to private and protected to False
- body = self.client.update_namespace(namespace=namespace_name,
- description='Tempest',
- visibility='private',
- display_name=namespace_name,
- protected=False)
+ body = self.namespaces_client.update_namespace(
+ namespace=namespace_name,
+ description='Tempest',
+ visibility='private',
+ display_name=namespace_name,
+ protected=False)
self.assertEqual('private', body['visibility'])
self.assertEqual(False, body['protected'])
# now able to delete the non-protected namespace
- self.client.delete_namespace(namespace_name)
+ self.namespaces_client.delete_namespace(namespace_name)
def _cleanup_namespace(self, namespace_name):
- body = self.client.show_namespace(namespace_name)
+ body = self.namespaces_client.show_namespace(namespace_name)
self.assertEqual(namespace_name, body['namespace'])
- body = self.client.update_namespace(namespace=namespace_name,
- description='Tempest',
- visibility='private',
- display_name=namespace_name,
- protected=False)
- self.client.delete_namespace(namespace_name)
+ body = self.namespaces_client.update_namespace(
+ namespace=namespace_name,
+ description='Tempest',
+ visibility='private',
+ display_name=namespace_name,
+ protected=False)
+ self.namespaces_client.delete_namespace(namespace_name)
diff --git a/tempest/clients.py b/tempest/clients.py
index 894704c..f71d3ce 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -132,10 +132,15 @@
from tempest.services.identity.v3.json.users_clients import \
UsersClient as UsersV3Client
from tempest.services.image.v1.json.images_client import ImagesClient
+from tempest.services.image.v1.json.members_client import MembersClient
from tempest.services.image.v2.json.images_client import \
ImagesClient as ImagesV2Client
from tempest.services.image.v2.json.members_client import MembersClient \
as MembersClientV2
+from tempest.services.image.v2.json.namespaces_client import NamespacesClient
+from tempest.services.image.v2.json.resource_types_client import \
+ ResourceTypesClient
+from tempest.services.image.v2.json.schemas_client import SchemasClient
from tempest.services.object_storage.account_client import AccountClient
from tempest.services.object_storage.container_client import ContainerClient
from tempest.services.object_storage.object_client import ObjectClient
@@ -334,6 +339,14 @@
build_interval=CONF.image.build_interval,
build_timeout=CONF.image.build_timeout,
**self.default_params)
+ self.image_member_client = MembersClient(
+ self.auth_provider,
+ CONF.image.catalog_type,
+ CONF.image.region or CONF.identity.region,
+ endpoint_type=CONF.image.endpoint_type,
+ build_interval=CONF.image.build_interval,
+ build_timeout=CONF.image.build_timeout,
+ **self.default_params)
self.image_client_v2 = ImagesV2Client(
self.auth_provider,
CONF.image.catalog_type,
@@ -350,6 +363,30 @@
build_interval=CONF.image.build_interval,
build_timeout=CONF.image.build_timeout,
**self.default_params)
+ self.namespaces_client = NamespacesClient(
+ self.auth_provider,
+ CONF.image.catalog_type,
+ CONF.image.region or CONF.identity.region,
+ endpoint_type=CONF.image.endpoint_type,
+ build_interval=CONF.image.build_interval,
+ build_timeout=CONF.image.build_timeout,
+ **self.default_params)
+ self.resource_types_client = ResourceTypesClient(
+ self.auth_provider,
+ CONF.image.catalog_type,
+ CONF.image.region or CONF.identity.region,
+ endpoint_type=CONF.image.endpoint_type,
+ build_interval=CONF.image.build_interval,
+ build_timeout=CONF.image.build_timeout,
+ **self.default_params)
+ self.schemas_client = SchemasClient(
+ self.auth_provider,
+ CONF.image.catalog_type,
+ CONF.image.region or CONF.identity.region,
+ endpoint_type=CONF.image.endpoint_type,
+ build_interval=CONF.image.build_interval,
+ build_timeout=CONF.image.build_timeout,
+ **self.default_params)
self.orchestration_client = OrchestrationClient(
self.auth_provider,
CONF.orchestration.catalog_type,
diff --git a/tempest/cmd/init.py b/tempest/cmd/init.py
index 633b9e9..77d62d3 100644
--- a/tempest/cmd/init.py
+++ b/tempest/cmd/init.py
@@ -21,6 +21,8 @@
from oslo_log import log as logging
from six import moves
+from tempest.cmd.workspace import WorkspaceManager
+
LOG = logging.getLogger(__name__)
TESTR_CONF = """[DEFAULT]
@@ -89,6 +91,10 @@
action='store_true', dest='show_global_dir',
help="Print the global config dir location, "
"then exit")
+ parser.add_argument('--name', help="The workspace name", default=None)
+ parser.add_argument('--workspace-path', default=None,
+ help="The path to the workspace file, the default "
+ "is ~/.tempest/workspace")
return parser
def generate_testr_conf(self, local_path):
@@ -114,15 +120,22 @@
config_parse.write(conf_file)
def copy_config(self, etc_dir, config_dir):
- shutil.copytree(config_dir, etc_dir)
+ if os.path.isdir(config_dir):
+ shutil.copytree(config_dir, etc_dir)
+ else:
+ LOG.warning("Global config dir %s can't be found" % config_dir)
def generate_sample_config(self, local_dir, config_dir):
- conf_generator = os.path.join(config_dir,
- 'config-generator.tempest.conf')
+ if os.path.isdir(config_dir):
+ conf_generator = os.path.join(config_dir,
+ 'config-generator.tempest.conf')
- subprocess.call(['oslo-config-generator', '--config-file',
- conf_generator],
- cwd=local_dir)
+ subprocess.call(['oslo-config-generator', '--config-file',
+ conf_generator],
+ cwd=local_dir)
+ else:
+ LOG.warning("Skipping sample config generation because global "
+ "config dir %s can't be found" % config_dir)
def create_working_dir(self, local_dir, config_dir):
# Create local dir if missing
@@ -159,6 +172,10 @@
subprocess.call(['testr', 'init'], cwd=local_dir)
def take_action(self, parsed_args):
+ workspace_manager = WorkspaceManager(parsed_args.workspace_path)
+ name = parsed_args.name or parsed_args.dir.split(os.path.sep)[-1]
+ workspace_manager.register_new_workspace(
+ name, parsed_args.dir, init=True)
config_dir = parsed_args.config_dir or get_tempest_default_config_dir()
if parsed_args.show_global_dir:
print("Global config dir is located at: %s" % config_dir)
diff --git a/tempest/cmd/workspace.py b/tempest/cmd/workspace.py
new file mode 100644
index 0000000..cc82284
--- /dev/null
+++ b/tempest/cmd/workspace.py
@@ -0,0 +1,218 @@
+# Copyright 2016 Rackspace
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""
+Manages Tempest workspaces
+
+This command is used for managing tempest workspaces
+
+Commands
+========
+
+list
+----
+Outputs the name and path of all known tempest workspaces
+
+register
+--------
+Registers a new tempest workspace via a given --name and --path
+
+rename
+------
+Renames a tempest workspace from --old-name to --new-name
+
+move
+----
+Changes the path of a given tempest workspace --name to --path
+
+remove
+------
+Deletes the entry for a given tempest workspace --name
+
+General Options
+===============
+
+ **--workspace_path**: Allows the user to specify a different location for the
+ workspace.yaml file containing the workspace definitions
+ instead of ~/.tempest/workspace.yaml
+"""
+
+import os
+import sys
+
+from cliff import command
+from oslo_concurrency import lockutils
+from oslo_log import log as logging
+import prettytable
+import yaml
+
+from tempest import config
+
+LOG = logging.getLogger(__name__)
+CONF = config.CONF
+
+
+class WorkspaceManager(object):
+ def __init__(self, path=None):
+ lockutils.get_lock_path(CONF)
+ self.path = path or os.path.join(
+ os.path.expanduser("~"), ".tempest", "workspace.yaml")
+ if not os.path.isdir(os.path.dirname(self.path)):
+ os.makedirs(self.path.rsplit(os.path.sep, 1)[0])
+ self.workspaces = {}
+
+ @lockutils.synchronized('workspaces', external=True)
+ def get_workspace(self, name):
+ """Returns the workspace that has the given name"""
+ self._populate()
+ return self.workspaces.get(name)
+
+ @lockutils.synchronized('workspaces', external=True)
+ def rename_workspace(self, old_name, new_name):
+ self._populate()
+ self._name_exists(old_name)
+ self._workspace_name_exists(new_name)
+ self.workspaces[new_name] = self.workspaces.pop(old_name)
+ self._write_file()
+
+ @lockutils.synchronized('workspaces', external=True)
+ def move_workspace(self, name, path):
+ self._populate()
+ path = os.path.abspath(os.path.expanduser(path))
+ self._name_exists(name)
+ self._validate_path(path)
+ self.workspaces[name] = path
+ self._write_file()
+
+ def _name_exists(self, name):
+ if name not in self.workspaces:
+ print("A workspace was not found with name: {0}".format(name))
+ sys.exit(1)
+
+ @lockutils.synchronized('workspaces', external=True)
+ def remove_workspace(self, name):
+ self._populate()
+ self._name_exists(name)
+ self.workspaces.pop(name)
+ self._write_file()
+
+ @lockutils.synchronized('workspaces', external=True)
+ def list_workspaces(self):
+ self._populate()
+ self._validate_workspaces()
+ return self.workspaces
+
+ def _workspace_name_exists(self, name):
+ if name in self.workspaces:
+ print("A workspace already exists with name: {0}.".format(
+ name))
+ sys.exit(1)
+
+ def _validate_path(self, path):
+ if not os.path.exists(path):
+ print("Path does not exist.")
+ sys.exit(1)
+
+ @lockutils.synchronized('workspaces', external=True)
+ def register_new_workspace(self, name, path, init=False):
+ """Adds the new workspace and writes out the new workspace config"""
+ self._populate()
+ path = os.path.abspath(os.path.expanduser(path))
+ # This only happens when register is called from outside of init
+ if not init:
+ self._validate_path(path)
+ self._workspace_name_exists(name)
+ self.workspaces[name] = path
+ self._write_file()
+
+ def _validate_workspaces(self):
+ if self.workspaces is not None:
+ self.workspaces = {n: p for n, p in self.workspaces.items()
+ if os.path.exists(p)}
+ self._write_file()
+
+ def _write_file(self):
+ with open(self.path, 'w') as f:
+ f.write(yaml.dump(self.workspaces))
+
+ def _populate(self):
+ if not os.path.isfile(self.path):
+ return
+ with open(self.path, 'r') as f:
+ self.workspaces = yaml.load(f) or {}
+
+
+class TempestWorkspace(command.Command):
+ def take_action(self, parsed_args):
+ self.manager = WorkspaceManager(parsed_args.workspace_path)
+ if getattr(parsed_args, 'register', None):
+ self.manager.register_new_workspace(
+ parsed_args.name, parsed_args.path)
+ elif getattr(parsed_args, 'rename', None):
+ self.manager.rename_workspace(
+ parsed_args.old_name, parsed_args.new_name)
+ elif getattr(parsed_args, 'move', None):
+ self.manager.move_workspace(
+ parsed_args.name, parsed_args.path)
+ elif getattr(parsed_args, 'remove', None):
+ self.manager.remove_workspace(
+ parsed_args.name)
+ else:
+ self._print_workspaces()
+ sys.exit(0)
+
+ def get_description(self):
+ return 'Tempest workspace actions'
+
+ def get_parser(self, prog_name):
+ parser = super(TempestWorkspace, self).get_parser(prog_name)
+
+ parser.add_argument(
+ '--workspace-path', required=False, default=None,
+ help="The path to the workspace file, the default is "
+ "~/.tempest/workspace.yaml")
+
+ subparsers = parser.add_subparsers()
+
+ list_parser = subparsers.add_parser('list')
+ list_parser.set_defaults(list=True)
+
+ register_parser = subparsers.add_parser('register')
+ register_parser.add_argument('--name', required=True)
+ register_parser.add_argument('--path', required=True)
+ register_parser.set_defaults(register=True)
+
+ update_parser = subparsers.add_parser('rename')
+ update_parser.add_argument('--old-name', required=True)
+ update_parser.add_argument('--new-name', required=True)
+ update_parser.set_defaults(rename=True)
+
+ move_parser = subparsers.add_parser('move')
+ move_parser.add_argument('--name', required=True)
+ move_parser.add_argument('--path', required=True)
+ move_parser.set_defaults(move=True)
+
+ remove_parser = subparsers.add_parser('remove')
+ remove_parser.add_argument('--name', required=True)
+ remove_parser.set_defaults(remove=True)
+
+ return parser
+
+ def _print_workspaces(self):
+ output = prettytable.PrettyTable(["Name", "Path"])
+ if self.manager.list_workspaces() is not None:
+ for name, path in self.manager.list_workspaces().items():
+ output.add_row([name, path])
+
+ print(output)
diff --git a/tempest/common/preprov_creds.py b/tempest/common/preprov_creds.py
index 42c69d5..5992d24 100644
--- a/tempest/common/preprov_creds.py
+++ b/tempest/common/preprov_creds.py
@@ -86,10 +86,8 @@
self.test_accounts_file = test_accounts_file
if test_accounts_file:
accounts = read_accounts_yaml(self.test_accounts_file)
- self.use_default_creds = False
else:
- accounts = {}
- self.use_default_creds = True
+ raise lib_exc.InvalidCredentials("No accounts file specified")
self.hash_dict = self.get_hash_dict(
accounts, admin_role, object_storage_operator_role,
object_storage_reseller_admin_role)
@@ -165,12 +163,7 @@
return hash_dict
def is_multi_user(self):
- # Default credentials is not a valid option with locking Account
- if self.use_default_creds:
- raise lib_exc.InvalidCredentials(
- "Account file %s doesn't exist" % self.test_accounts_file)
- else:
- return len(self.hash_dict['creds']) > 1
+ return len(self.hash_dict['creds']) > 1
def is_multi_tenant(self):
return self.is_multi_user()
@@ -245,9 +238,6 @@
return temp_creds
def _get_creds(self, roles=None):
- if self.use_default_creds:
- raise lib_exc.InvalidCredentials(
- "Account file %s doesn't exist" % self.test_accounts_file)
useable_hashes = self._get_match_hash_list(roles)
if len(useable_hashes) == 0:
msg = 'No users configured for type/roles %s' % roles
@@ -329,12 +319,9 @@
return self.get_creds_by_roles([self.admin_role])
def is_role_available(self, role):
- if self.use_default_creds:
- return False
- else:
- if self.hash_dict['roles'].get(role):
- return True
- return False
+ if self.hash_dict['roles'].get(role):
+ return True
+ return False
def admin_available(self):
return self.is_role_available(self.admin_role)
diff --git a/tempest/services/image/v1/json/images_client.py b/tempest/services/image/v1/json/images_client.py
index 63fb59d..4ffaf3b 100644
--- a/tempest/services/image/v1/json/images_client.py
+++ b/tempest/services/image/v1/json/images_client.py
@@ -188,36 +188,3 @@
def resource_type(self):
"""Returns the primary type of resource this client works with."""
return 'image_meta'
-
- def list_image_members(self, image_id):
- url = 'images/%s/members' % image_id
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return rest_client.ResponseBody(resp, body)
-
- def list_shared_images(self, tenant_id):
- """List shared images with the specified tenant"""
- url = 'shared-images/%s' % tenant_id
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return rest_client.ResponseBody(resp, body)
-
- def add_member(self, member_id, image_id, **kwargs):
- """Add a member to an image.
-
- Available params: see http://developer.openstack.org/
- api-ref-image-v1.html#addMember-v1
- """
- url = 'images/%s/members/%s' % (image_id, member_id)
- body = json.dumps({'member': kwargs})
- resp, __ = self.put(url, body)
- self.expected_success(204, resp.status)
- return rest_client.ResponseBody(resp)
-
- def delete_member(self, member_id, image_id):
- url = 'images/%s/members/%s' % (image_id, member_id)
- resp, __ = self.delete(url)
- self.expected_success(204, resp.status)
- return rest_client.ResponseBody(resp)
diff --git a/tempest/services/image/v1/json/members_client.py b/tempest/services/image/v1/json/members_client.py
new file mode 100644
index 0000000..95cee1c
--- /dev/null
+++ b/tempest/services/image/v1/json/members_client.py
@@ -0,0 +1,63 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+
+
+class MembersClient(rest_client.RestClient):
+ api_version = "v1"
+
+ def list_image_members(self, image_id):
+ """List all members of an image."""
+ url = 'images/%s/members' % image_id
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_shared_images(self, tenant_id):
+ """List image memberships for the given tenant.
+
+ Available params: see http://developer.openstack.org/
+ api-ref-image-v1.html#listSharedImages-v1
+ """
+
+ url = 'shared-images/%s' % tenant_id
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def add_member(self, member_id, image_id, **kwargs):
+ """Add a member to an image.
+
+ Available params: see http://developer.openstack.org/
+ api-ref-image-v1.html#addMember-v1
+ """
+ url = 'images/%s/members/%s' % (image_id, member_id)
+ body = json.dumps({'member': kwargs})
+ resp, __ = self.put(url, body)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp)
+
+ def delete_member(self, member_id, image_id):
+ """Removes a membership from the image.
+
+ Available params: see http://developer.openstack.org/
+ api-ref-image-v1.html#removeMember-v1
+ """
+ url = 'images/%s/members/%s' % (image_id, member_id)
+ resp, __ = self.delete(url)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp)
diff --git a/tempest/services/image/v2/json/images_client.py b/tempest/services/image/v2/json/images_client.py
index 1014417..71e7c6b 100644
--- a/tempest/services/image/v2/json/images_client.py
+++ b/tempest/services/image/v2/json/images_client.py
@@ -131,59 +131,3 @@
resp, _ = self.delete(url)
self.expected_success(204, resp.status)
return rest_client.ResponseBody(resp)
-
- def show_schema(self, schema):
- url = 'schemas/%s' % schema
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return rest_client.ResponseBody(resp, body)
-
- def list_resource_types(self):
- url = 'metadefs/resource_types'
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return rest_client.ResponseBody(resp, body)
-
- def create_namespace(self, **kwargs):
- """Create a namespace.
-
- Available params: see http://developer.openstack.org/
- api-ref-image-v2.html#createNamespace-v2
- """
- data = json.dumps(kwargs)
- resp, body = self.post('metadefs/namespaces', data)
- self.expected_success(201, resp.status)
- body = json.loads(body)
- return rest_client.ResponseBody(resp, body)
-
- def show_namespace(self, namespace):
- url = 'metadefs/namespaces/%s' % namespace
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return rest_client.ResponseBody(resp, body)
-
- def update_namespace(self, namespace, **kwargs):
- """Update a namespace.
-
- Available params: see http://developer.openstack.org/
- api-ref-image-v2.html#updateNamespace-v2
- """
- # NOTE: On Glance API, we need to pass namespace on both URI
- # and a request body.
- params = {'namespace': namespace}
- params.update(kwargs)
- data = json.dumps(params)
- url = 'metadefs/namespaces/%s' % namespace
- resp, body = self.put(url, body=data)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return rest_client.ResponseBody(resp, body)
-
- def delete_namespace(self, namespace):
- url = 'metadefs/namespaces/%s' % namespace
- resp, _ = self.delete(url)
- self.expected_success(204, resp.status)
- return rest_client.ResponseBody(resp)
diff --git a/tempest/services/image/v2/json/namespaces_client.py b/tempest/services/image/v2/json/namespaces_client.py
new file mode 100644
index 0000000..97400e1
--- /dev/null
+++ b/tempest/services/image/v2/json/namespaces_client.py
@@ -0,0 +1,64 @@
+# Copyright 2013 IBM Corp.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+
+
+class NamespacesClient(rest_client.RestClient):
+ api_version = "v2"
+
+ def create_namespace(self, **kwargs):
+ """Create a namespace.
+
+ Available params: see http://developer.openstack.org/
+ api-ref-image-v2.html#createNamespace-v2
+ """
+ data = json.dumps(kwargs)
+ resp, body = self.post('metadefs/namespaces', data)
+ self.expected_success(201, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_namespace(self, namespace):
+ url = 'metadefs/namespaces/%s' % namespace
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_namespace(self, namespace, **kwargs):
+ """Update a namespace.
+
+ Available params: see http://developer.openstack.org/
+ api-ref-image-v2.html#updateNamespace-v2
+ """
+ # NOTE: On Glance API, we need to pass namespace on both URI
+ # and a request body.
+ params = {'namespace': namespace}
+ params.update(kwargs)
+ data = json.dumps(params)
+ url = 'metadefs/namespaces/%s' % namespace
+ resp, body = self.put(url, body=data)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_namespace(self, namespace):
+ url = 'metadefs/namespaces/%s' % namespace
+ resp, _ = self.delete(url)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp)
diff --git a/tempest/services/image/v2/json/resource_types_client.py b/tempest/services/image/v2/json/resource_types_client.py
new file mode 100644
index 0000000..1349c63
--- /dev/null
+++ b/tempest/services/image/v2/json/resource_types_client.py
@@ -0,0 +1,29 @@
+# Copyright 2013 IBM Corp.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+
+
+class ResourceTypesClient(rest_client.RestClient):
+ api_version = "v2"
+
+ def list_resource_types(self):
+ url = 'metadefs/resource_types'
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/services/image/v2/json/schemas_client.py b/tempest/services/image/v2/json/schemas_client.py
new file mode 100644
index 0000000..0c9db40
--- /dev/null
+++ b/tempest/services/image/v2/json/schemas_client.py
@@ -0,0 +1,29 @@
+# Copyright 2013 IBM Corp.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+
+
+class SchemasClient(rest_client.RestClient):
+ api_version = "v2"
+
+ def show_schema(self, schema):
+ url = 'schemas/%s' % schema
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/tests/cmd/test_workspace.py b/tempest/tests/cmd/test_workspace.py
new file mode 100644
index 0000000..c4bd7b2
--- /dev/null
+++ b/tempest/tests/cmd/test_workspace.py
@@ -0,0 +1,124 @@
+# Copyright 2016 Rackspace
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import os
+import shutil
+import subprocess
+import tempfile
+
+from tempest.cmd.workspace import WorkspaceManager
+from tempest.lib.common.utils import data_utils
+from tempest.tests import base
+
+
+class TestTempestWorkspaceBase(base.TestCase):
+ def setUp(self):
+ super(TestTempestWorkspaceBase, self).setUp()
+ self.name = data_utils.rand_uuid()
+ self.path = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, self.path, ignore_errors=True)
+ store_dir = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, store_dir, ignore_errors=True)
+ self.store_file = os.path.join(store_dir, 'workspace.yaml')
+ self.workspace_manager = WorkspaceManager(path=self.store_file)
+ self.workspace_manager.register_new_workspace(self.name, self.path)
+
+
+class TestTempestWorkspace(TestTempestWorkspaceBase):
+ def _run_cmd_gets_return_code(self, cmd, expected):
+ process = subprocess.Popen(cmd, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ stdout, stderr = process.communicate()
+ return_code = process.returncode
+ msg = ("%s failled with:\nstdout: %s\nstderr: %s" % (' '.join(cmd),
+ stdout, stderr))
+ self.assertEqual(return_code, expected, msg)
+
+ def test_run_workspace_list(self):
+ cmd = ['tempest', 'workspace', '--workspace-path',
+ self.store_file, 'list']
+ self._run_cmd_gets_return_code(cmd, 0)
+
+ def test_run_workspace_register(self):
+ name = data_utils.rand_uuid()
+ path = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, path, ignore_errors=True)
+ cmd = ['tempest', 'workspace', '--workspace-path', self.store_file,
+ 'register', '--name', name, '--path', path]
+ self._run_cmd_gets_return_code(cmd, 0)
+ self.assertIsNotNone(self.workspace_manager.get_workspace(name))
+
+ def test_run_workspace_rename(self):
+ new_name = data_utils.rand_uuid()
+ cmd = ['tempest', 'workspace', '--workspace-path', self.store_file,
+ 'rename', "--old-name", self.name, '--new-name', new_name]
+ self._run_cmd_gets_return_code(cmd, 0)
+ self.assertIsNone(self.workspace_manager.get_workspace(self.name))
+ self.assertIsNotNone(self.workspace_manager.get_workspace(new_name))
+
+ def test_run_workspace_move(self):
+ new_path = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, new_path, ignore_errors=True)
+ cmd = ['tempest', 'workspace', '--workspace-path', self.store_file,
+ 'move', '--name', self.name, '--path', new_path]
+ self._run_cmd_gets_return_code(cmd, 0)
+ self.assertEqual(
+ self.workspace_manager.get_workspace(self.name), new_path)
+
+ def test_run_workspace_remove(self):
+ cmd = ['tempest', 'workspace', '--workspace-path', self.store_file,
+ 'remove', '--name', self.name]
+ self._run_cmd_gets_return_code(cmd, 0)
+ self.assertIsNone(self.workspace_manager.get_workspace(self.name))
+
+
+class TestTempestWorkspaceManager(TestTempestWorkspaceBase):
+ def setUp(self):
+ super(TestTempestWorkspaceManager, self).setUp()
+ self.name = data_utils.rand_uuid()
+ self.path = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, self.path, ignore_errors=True)
+ store_dir = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, store_dir, ignore_errors=True)
+ self.store_file = os.path.join(store_dir, 'workspace.yaml')
+ self.workspace_manager = WorkspaceManager(path=self.store_file)
+ self.workspace_manager.register_new_workspace(self.name, self.path)
+
+ def test_workspace_manager_get(self):
+ self.assertIsNotNone(self.workspace_manager.get_workspace(self.name))
+
+ def test_workspace_manager_rename(self):
+ new_name = data_utils.rand_uuid()
+ self.workspace_manager.rename_workspace(self.name, new_name)
+ self.assertIsNone(self.workspace_manager.get_workspace(self.name))
+ self.assertIsNotNone(self.workspace_manager.get_workspace(new_name))
+
+ def test_workspace_manager_move(self):
+ new_path = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, new_path, ignore_errors=True)
+ self.workspace_manager.move_workspace(self.name, new_path)
+ self.assertEqual(
+ self.workspace_manager.get_workspace(self.name), new_path)
+
+ def test_workspace_manager_remove(self):
+ self.workspace_manager.remove_workspace(self.name)
+ self.assertIsNone(self.workspace_manager.get_workspace(self.name))
+
+ def test_path_expansion(self):
+ name = data_utils.rand_uuid()
+ path = os.path.join("~", name)
+ os.makedirs(os.path.expanduser(path))
+ self.addCleanup(shutil.rmtree, path, ignore_errors=True)
+ self.workspace_manager.register_new_workspace(name, path)
+ self.assertIsNotNone(self.workspace_manager.get_workspace(name))