diff --git a/rally/plugins/openstack/context/api_versions.py b/rally/plugins/openstack/context/api_versions.py new file mode 100644 index 00000000..12c441f8 --- /dev/null +++ b/rally/plugins/openstack/context/api_versions.py @@ -0,0 +1,248 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# 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 six + +from rally.common.i18n import _, _LE +from rally.common import logging +from rally import consts +from rally import exceptions +from rally import osclients +from rally.task import context + +LOG = logging.getLogger(__name__) + + +@context.configure(name="api_versions", order=150) +class OpenStackAPIVersions(context.Context): + """Context for specifying OpenStack clients versions and service types. + + Some OpenStack services support several API versions. To recognize + the endpoints of each version, separate service types are provided in + Keystone service catalog. + + Rally has the map of default service names - service types. But since + service type is an entity, which can be configured manually by admin( + via keystone api) without relation to service name, such map can be + insufficient. + + Also, Keystone service catalog does not provide a map types to name + (this statement is true for keystone < 3.3 ). + + This context was designed for not-default service types and not-default + API versions usage. + + An example of specifying API version: + + .. code-block:: json + + # In this example we will launch NovaKeypair.create_and_list_keypairs + # scenario on 2.2 api version. + { + "NovaKeypair.create_and_list_keypairs": [ + { + "args": { + "key_type": "x509" + }, + "runner": { + "type": "constant", + "times": 10, + "concurrency": 2 + }, + "context": { + "users": { + "tenants": 3, + "users_per_tenant": 2 + }, + "api_versions": { + "nova": { + "version": 2.2 + } + } + } + } + ] + } + + An example of specifying API version along with service type: + + .. code-block:: json + + # In this example we will launch CinderVolumes.create_and_attach_volume + # scenario on Cinder V2 + { + "CinderVolumes.create_and_attach_volume": [ + { + "args": { + "size": 10, + "image": { + "name": "^cirros.*uec$" + }, + "flavor": { + "name": "m1.tiny" + }, + "create_volume_params": { + "availability_zone": "nova" + } + }, + "runner": { + "type": "constant", + "times": 5, + "concurrency": 1 + }, + "context": { + "users": { + "tenants": 2, + "users_per_tenant": 2 + }, + "api_versions": { + "cinder": { + "version": 2, + "service_type": "volumev2" + } + } + } + } + ] + } + + Also, it possible to use service name as an identifier of service endpoint, + but an admin user is required (Keystone can return map of service + names - types, but such API is permitted only for admin). An example: + + .. code-block:: json + + # Similar to the previous example, but `service_name` argument is used + # instead of `service_type` + { + "CinderVolumes.create_and_attach_volume": [ + { + "args": { + "size": 10, + "image": { + "name": "^cirros.*uec$" + }, + "flavor": { + "name": "m1.tiny" + }, + "create_volume_params": { + "availability_zone": "nova" + } + }, + "runner": { + "type": "constant", + "times": 5, + "concurrency": 1 + }, + "context": { + "users": { + "tenants": 2, + "users_per_tenant": 2 + }, + "api_versions": { + "cinder": { + "version": 2, + "service_name": "cinderv2" + } + } + } + } + ] + } + + """ + + CONFIG_SCHEMA = { + "type": "object", + "$schema": consts.JSON_SCHEMA, + "patternProperties": { + "^[a-z]+$": { + "type": "object", + "properties": { + "version": { + "anyOf": [{"type": "string"}, {"type": "number"}] + }, + "service_name": { + "type": "string" + }, + "service_type": { + "type": "string" + } + }, + "additionalProperties": False + } + }, + "additionalProperties": False + } + + def setup(self): + # FIXME(andreykurilin): move all checks to validate method. + + # use admin only when `service_name` is presented + admin_clients = osclients.Clients( + self.context.get("admin", {}).get("credential")) + clients = osclients.Clients(random.choice( + self.context["users"])["credential"]) + services = clients.keystone().service_catalog.get_endpoints() + services_from_admin = None + for client_name, conf in six.iteritems(self.config): + if "service_type" in conf and conf["service_type"] not in services: + raise exceptions.ValidationError(_( + "There is no service with '%s' type in your environment.") + % conf["service_type"]) + elif "service_name" in conf: + if not self.context.get("admin", {}).get("credential"): + raise exceptions.BenchmarkSetupFailure(_( + "Setting 'service_name' is allowed only for 'admin' " + "user.")) + if not services_from_admin: + services_from_admin = dict( + [(s.name, s.type) + for s in admin_clients.keystone().services.list()]) + if conf["service_name"] not in services_from_admin: + raise exceptions.ValidationError( + _("There is no '%s' service in your environment") % + conf["service_name"]) + + self.context["config"]["api_versions"][client_name][ + "service_type"] = services_from_admin[conf["service_name"]] + + def cleanup(self): + # nothing to do here + pass + + @classmethod + def validate(cls, config, non_hidden=False): + super(OpenStackAPIVersions, cls).validate(config, + non_hidden=non_hidden) + for client in config: + client_cls = osclients.OSClient.get(client) + if ("service_type" in config[client] and + "service_name" in config[client]): + raise exceptions.ValidationError(_LE( + "Setting both 'service_type' and 'service_name' properties" + " is restricted.")) + try: + if ("service_type" in config[client] or + "service_name" in config[client]): + client_cls.is_service_type_configurable() + + if "version" in config[client]: + client_cls.validate_version(config[client]["version"]) + + except exceptions.RallyException as e: + raise exceptions.ValidationError( + _LE("Invalid settings for '%(client)s': %(error)s") % { + "client": client, + "error": e.format_message()}) diff --git a/tests/unit/plugins/openstack/context/test_api_versions.py b/tests/unit/plugins/openstack/context/test_api_versions.py new file mode 100644 index 00000000..a52cf287 --- /dev/null +++ b/tests/unit/plugins/openstack/context/test_api_versions.py @@ -0,0 +1,127 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +import jsonschema +import mock + +from rally.common import utils +from rally import exceptions +from rally.plugins.openstack.context import api_versions +from tests.unit import test + + +class OpenStackServicesTestCase(test.TestCase): + + def setUp(self): + super(OpenStackServicesTestCase, self).setUp() + self.mock_clients = mock.patch("rally.osclients.Clients").start() + self.mock_kc = self.mock_clients.return_value.keystone.return_value + self.mock_kc.service_catalog.get_endpoints.return_value = [] + self.mock_kc.services.list.return_value = [] + + def test_validate_correct_config(self): + api_versions.OpenStackAPIVersions.validate({ + "nova": {"service_type": "compute", "version": 2}, + "cinder": {"service_name": "cinderv2", "version": 2}, + "neutron": {"service_type": "network"}, + "glance": {"service_name": "glance"}, + "heat": {"version": 1} + }) + + def test_validate_wrong_configs(self): + self.assertRaises( + exceptions.PluginNotFound, + api_versions.OpenStackAPIVersions.validate, + {"invalid": {"service_type": "some_type"}}, + "Non-existing clients should be caught.") + + self.assertRaises( + jsonschema.ValidationError, + api_versions.OpenStackAPIVersions.validate, + {"nova": {"some_key": "some_value"}}, + "Additional properties should be restricted.") + + self.assertRaises( + exceptions.ValidationError, + api_versions.OpenStackAPIVersions.validate, + {"keystone": {"service_type": "identity"}}, + "Setting service_type is allowed only for those clients, which " + "support it.") + + self.assertRaises( + exceptions.ValidationError, + api_versions.OpenStackAPIVersions.validate, + {"keystone": {"service_name": "keystone"}}, + "Setting service_name is allowed only for those clients, which " + "support it.") + + self.assertRaises( + exceptions.ValidationError, + api_versions.OpenStackAPIVersions.validate, + {"keystone": {"version": 1}}, + "Setting version is allowed only for those clients, which " + "support it.") + + self.assertRaises( + exceptions.ValidationError, + api_versions.OpenStackAPIVersions.validate, + {"nova": {"version": 666}}, + "Unsupported version should be caught.") + + def test_setup_with_wrong_service_name(self): + context = { + "config": {api_versions.OpenStackAPIVersions.get_name(): { + "nova": {"service_name": "service_name"}}}, + "admin": {"credential": mock.MagicMock()}, + "users": [{"credential": mock.MagicMock()}]} + ctx = api_versions.OpenStackAPIVersions(context) + self.assertRaises(exceptions.ValidationError, ctx.setup) + self.mock_kc.service_catalog.get_endpoints.assert_called_once_with() + self.mock_kc.services.list.assert_called_once_with() + + def test_setup_with_wrong_service_name_and_without_admin(self): + context = { + "config": {api_versions.OpenStackAPIVersions.get_name(): { + "nova": {"service_name": "service_name"}}}, + "users": [{"credential": mock.MagicMock()}]} + ctx = api_versions.OpenStackAPIVersions(context) + self.assertRaises(exceptions.BenchmarkSetupFailure, ctx.setup) + self.mock_kc.service_catalog.get_endpoints.assert_called_once_with() + self.assertFalse(self.mock_kc.services.list.called) + + def test_setup_with_wrong_service_type(self): + context = { + "config": {api_versions.OpenStackAPIVersions.get_name(): { + "nova": {"service_type": "service_type"}}}, + "users": [{"credential": mock.MagicMock()}]} + ctx = api_versions.OpenStackAPIVersions(context) + self.assertRaises(exceptions.ValidationError, ctx.setup) + self.mock_kc.service_catalog.get_endpoints.assert_called_once_with() + + def test_setup_with_service_name(self): + self.mock_kc.services.list.return_value = [ + utils.Struct(type="computev21", name="NovaV21")] + context = { + "config": {api_versions.OpenStackAPIVersions.get_name(): { + "nova": {"service_name": "NovaV21"}}}, + "admin": {"credential": mock.MagicMock()}, + "users": [{"credential": mock.MagicMock()}]} + ctx = api_versions.OpenStackAPIVersions(copy.deepcopy(context)) + + ctx.setup() + + self.mock_kc.service_catalog.get_endpoints.assert_called_once_with() + self.mock_kc.services.list.assert_called_once_with() + + self.assertEqual("computev21", ctx.config["nova"]["service_type"])