Merge "[docs] Increase an understanding of api_versions context"
This commit is contained in:
commit
485310c16a
248
rally/plugins/openstack/context/api_versions.py
Normal file
248
rally/plugins/openstack/context/api_versions.py
Normal file
@ -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()})
|
127
tests/unit/plugins/openstack/context/test_api_versions.py
Normal file
127
tests/unit/plugins/openstack/context/test_api_versions.py
Normal file
@ -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"])
|
Loading…
Reference in New Issue
Block a user