diff --git a/optional-requirements.txt b/optional-requirements.txt new file mode 100644 index 0000000000..3a55a860ae --- /dev/null +++ b/optional-requirements.txt @@ -0,0 +1 @@ +git+git://github.com/stackforge/python-mistralclient.git diff --git a/rally-jobs/extra/mistral_wb.yaml b/rally-jobs/extra/mistral_wb.yaml new file mode 100644 index 0000000000..ab20f7f14a --- /dev/null +++ b/rally-jobs/extra/mistral_wb.yaml @@ -0,0 +1,13 @@ +--- +version: "2.0" + +name: wb + +workflows: + wf1: + type: direct + tasks: + hello: + action: std.echo output="Hello" + publish: + result: $ diff --git a/rally-jobs/rally-mistral.yaml b/rally-jobs/rally-mistral.yaml new file mode 100644 index 0000000000..13b3bde049 --- /dev/null +++ b/rally-jobs/rally-mistral.yaml @@ -0,0 +1,46 @@ +--- + MistralWorkbooks.list_workbooks: + - + runner: + type: "constant" + times: 50 + concurrency: 10 + context: + users: + tenants: 1 + users_per_tenant: 1 + sla: + failure_rate: + max: 0 + + MistralWorkbooks.create_workbook: + - + args: + definition: "/home/jenkins/.rally/extra/mistral_wb.yaml" + runner: + type: "constant" + times: 50 + concurrency: 10 + context: + users: + tenants: 1 + users_per_tenant: 1 + sla: + failure_rate: + max: 0 + + - + args: + definition: "/home/jenkins/.rally/extra/mistral_wb.yaml" + do_delete: true + runner: + type: "constant" + times: 50 + concurrency: 10 + context: + users: + tenants: 1 + users_per_tenant: 1 + sla: + failure_rate: + max: 0 diff --git a/rally/benchmark/context/cleanup/resources.py b/rally/benchmark/context/cleanup/resources.py index 3f177a7174..1cd0fe3591 100644 --- a/rally/benchmark/context/cleanup/resources.py +++ b/rally/benchmark/context/cleanup/resources.py @@ -284,6 +284,14 @@ class DesignateServer(SynchronizedDeletion, base.ResourceManager): pass +# MISTRAL + +@base.resource("mistral", "workbooks", order=1100, tenant_resource=True) +class MistralWorkbooks(SynchronizedDeletion, base.ResourceManager): + def delete(self): + self._manager().delete(self.raw_resource.name) + + # KEYSTONE _keystone_order = get_order(9000) diff --git a/rally/benchmark/scenarios/mistral/__init__.py b/rally/benchmark/scenarios/mistral/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/rally/benchmark/scenarios/mistral/utils.py b/rally/benchmark/scenarios/mistral/utils.py new file mode 100644 index 0000000000..ae50db769f --- /dev/null +++ b/rally/benchmark/scenarios/mistral/utils.py @@ -0,0 +1,49 @@ +# Copyright 2015: Mirantis Inc. +# 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 yaml + +from rally.benchmark.scenarios import base + + +class MistralScenario(base.Scenario): + """Base class for Mistral scenarios with basic atomic actions.""" + + @base.atomic_action_timer("mistral.list_workbooks") + def _list_workbooks(self): + """Gets list of existing workbooks.""" + return self.clients("mistral").workbooks.list() + + @base.atomic_action_timer("mistral.create_workbook") + def _create_workbook(self, definition): + """Create a new workbook. + + :param definition: workbook description in string + (yaml string) format + :returns: workbook object + """ + definition = yaml.safe_load(definition) + definition["name"] = self._generate_random_name(definition["name"]) + definition = yaml.safe_dump(definition) + + return self.clients("mistral").workbooks.create(definition) + + @base.atomic_action_timer("mistral.delete_workbook") + def _delete_workbook(self, wb_name): + """Delete the given workbook. + + :param wb_name: the name of workbook that would be deleted. + """ + self.clients("mistral").workbooks.delete(wb_name) diff --git a/rally/benchmark/scenarios/mistral/workbooks.py b/rally/benchmark/scenarios/mistral/workbooks.py new file mode 100644 index 0000000000..ed0a877194 --- /dev/null +++ b/rally/benchmark/scenarios/mistral/workbooks.py @@ -0,0 +1,59 @@ +# Copyright 2015: Mirantis Inc. +# 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 rally.benchmark.scenarios import base +from rally.benchmark.scenarios.mistral import utils +from rally.benchmark import types as types +from rally.benchmark import validation +from rally import consts + + +class MistralWorkbooks(utils.MistralScenario): + """Benchmark scenarios for Mistral workbook.""" + + @validation.required_clients("mistral") + @validation.required_openstack(users=True) + @validation.required_services(consts.Service.MISTRAL) + @base.scenario() + def list_workbooks(self): + """Scenario test mistral workbook-list command. + + This simple scenario tests the Mistral workbook-list + command by listing all the workbooks. + """ + self._list_workbooks() + + @types.set(definition=types.FileType) + @validation.file_exists("definition") + @validation.required_parameters("definition") + @validation.required_clients("mistral") + @validation.required_openstack(users=True) + @validation.required_services(consts.Service.MISTRAL) + @base.scenario(context={"cleanup": ["mistral"]}) + def create_workbook(self, definition, do_delete=False): + """Scenario tests workbook creation and deletion. + + This scenario is a very useful tool to measure the + "mistral workbook-create" and "mistral workbook-delete" + commands performance. + :param definition: string (yaml string) representation of given + file content (Mistral workbook definition) + :param do_delete: if False than it allows to check performance + in "create only" mode. + """ + wb = self._create_workbook(definition) + + if do_delete: + self._delete_workbook(wb.name) diff --git a/rally/benchmark/types.py b/rally/benchmark/types.py index fe58036a16..49c338fbc5 100644 --- a/rally/benchmark/types.py +++ b/rally/benchmark/types.py @@ -236,3 +236,19 @@ class NeutronNetworkResourceType(ResourceType): raise exceptions.InvalidScenarioArgument( "Neutron network with name '{name}' not found".format( name=resource_config.get("name"))) + + +class FileType(ResourceType): + + @classmethod + def transform(cls, clients, resource_config): + """Returns content of the file by its path. + + :param clients: openstack admin client handles + :param resource_config: path to file + + :returns: content of the file + """ + + with open(resource_config, "r") as f: + return f.read() diff --git a/rally/benchmark/validation.py b/rally/benchmark/validation.py index 0868ef02a8..e8ee233859 100644 --- a/rally/benchmark/validation.py +++ b/rally/benchmark/validation.py @@ -376,6 +376,23 @@ def required_services(config, clients, deployment, *required_services): False, _("Service is not available: %s") % service) +@validator +def required_clients(config, clients, task, *components): + """Validator checks if specified OpenStack clients are available. + + :param *components: list of client components names + """ + for client_component in components: + try: + getattr(clients, client_component)() + except ImportError: + return ValidationResult( + False, + _("Client for %s is not installed. To install it run " + "`pip install -r" + " optional-requirements.txt`") % client_component) + + @validator def required_contexts(config, clients, deployment, *context_names): """Validator hecks if required benchmark contexts are specified. diff --git a/rally/consts.py b/rally/consts.py index 7cd5a35516..c816b40c1a 100644 --- a/rally/consts.py +++ b/rally/consts.py @@ -108,6 +108,7 @@ class _Service(utils.ImmutableMixin, utils.EnumMixin): TROVE = "trove" SAHARA = "sahara" SWIFT = "swift" + MISTRAL = "mistral" class _ServiceType(utils.ImmutableMixin, utils.EnumMixin): @@ -130,6 +131,7 @@ class _ServiceType(utils.ImmutableMixin, utils.EnumMixin): DATABASE = "database" DATA_PROCESSING = "data_processing" OBJECT_STORE = "object-store" + WORKFLOW_EXECUTION = "workflowv2" def __init__(self): self.__names = { @@ -149,7 +151,8 @@ class _ServiceType(utils.ImmutableMixin, utils.EnumMixin): self.S3: _Service.S3, self.DATABASE: _Service.TROVE, self.DATA_PROCESSING: _Service.SAHARA, - self.OBJECT_STORE: _Service.SWIFT + self.OBJECT_STORE: _Service.SWIFT, + self.WORKFLOW_EXECUTION: _Service.MISTRAL, } def __getitem__(self, service_type): diff --git a/rally/osclients.py b/rally/osclients.py index dfa09bfab7..01f8477182 100644 --- a/rally/osclients.py +++ b/rally/osclients.py @@ -301,6 +301,23 @@ class Clients(object): cacert=CONF.https_cacert) return client + @cached + def mistral(self): + """Return Mistral client.""" + from mistralclient.api import client + kc = self.keystone() + + mistral_url = kc.service_catalog.url_for( + service_type="workflowv2", + endpoint_type=self.endpoint.endpoint_type, + region_name=self.endpoint.region_name) + + client = client.client(mistral_url=mistral_url, + service_type="workflowv2", + auth_token=kc.auth_token) + + return client + @cached def services(self): """Return available services names and types. diff --git a/samples/tasks/scenarios/mistral/create-delete-workbook.json b/samples/tasks/scenarios/mistral/create-delete-workbook.json new file mode 100644 index 0000000000..1d2013ccba --- /dev/null +++ b/samples/tasks/scenarios/mistral/create-delete-workbook.json @@ -0,0 +1,21 @@ +{ + "MistralWorkbooks.create_workbook": [ + { + "args": { + "definition": "rally-jobs/extra/mistral_wb.yaml", + "do_delete": true + }, + "runner": { + "type": "constant", + "times": 50, + "concurrency": 10 + }, + "context": { + "users": { + "tenants": 1, + "users_per_tenant": 1 + } + } + } + ] +} diff --git a/samples/tasks/scenarios/mistral/create-delete-workbook.yaml b/samples/tasks/scenarios/mistral/create-delete-workbook.yaml new file mode 100644 index 0000000000..7dc4f1fb9e --- /dev/null +++ b/samples/tasks/scenarios/mistral/create-delete-workbook.yaml @@ -0,0 +1,15 @@ +--- + MistralWorkbooks.create_workbook: + - + args: + definition: rally-jobs/extra/mistral_wb.yaml + do_delete: true + runner: + type: "constant" + times: 50 + concurrency: 10 + context: + users: + tenants: 1 + users_per_tenant: 1 + diff --git a/samples/tasks/scenarios/mistral/create-workbook.json b/samples/tasks/scenarios/mistral/create-workbook.json new file mode 100644 index 0000000000..c9c2edad7d --- /dev/null +++ b/samples/tasks/scenarios/mistral/create-workbook.json @@ -0,0 +1,20 @@ +{ + "MistralWorkbooks.create_workbook": [ + { + "args": { + "definition": "rally-jobs/extra/mistral_wb.yaml" + }, + "runner": { + "type": "constant", + "times": 50, + "concurrency": 10 + }, + "context": { + "users": { + "tenants": 1, + "users_per_tenant": 1 + } + } + } + ] +} diff --git a/samples/tasks/scenarios/mistral/create-workbook.yaml b/samples/tasks/scenarios/mistral/create-workbook.yaml new file mode 100644 index 0000000000..243356eef9 --- /dev/null +++ b/samples/tasks/scenarios/mistral/create-workbook.yaml @@ -0,0 +1,14 @@ +--- + MistralWorkbooks.create_workbook: + - + args: + definition: rally-jobs/extra/mistral_wb.yaml + runner: + type: "constant" + times: 50 + concurrency: 10 + context: + users: + tenants: 1 + users_per_tenant: 1 + diff --git a/samples/tasks/scenarios/mistral/list-workbooks.json b/samples/tasks/scenarios/mistral/list-workbooks.json new file mode 100644 index 0000000000..04c68d14d8 --- /dev/null +++ b/samples/tasks/scenarios/mistral/list-workbooks.json @@ -0,0 +1,17 @@ +{ + "MistralWorkbooks.list_workbooks": [ + { + "runner": { + "type": "constant", + "times": 50, + "concurrency": 10 + }, + "context": { + "users": { + "tenants": 1, + "users_per_tenant": 1 + } + } + } + ] +} diff --git a/samples/tasks/scenarios/mistral/list-workbooks.yaml b/samples/tasks/scenarios/mistral/list-workbooks.yaml new file mode 100644 index 0000000000..ab28301d2a --- /dev/null +++ b/samples/tasks/scenarios/mistral/list-workbooks.yaml @@ -0,0 +1,11 @@ +--- + MistralWorkbooks.list_workbooks: + - + runner: + type: "constant" + times: 50 + concurrency: 10 + context: + users: + tenants: 1 + users_per_tenant: 1 diff --git a/tests/unit/benchmark/scenarios/mistral/test_utils.py b/tests/unit/benchmark/scenarios/mistral/test_utils.py new file mode 100644 index 0000000000..ac15c9cd02 --- /dev/null +++ b/tests/unit/benchmark/scenarios/mistral/test_utils.py @@ -0,0 +1,66 @@ +# Copyright 2015: Mirantis Inc. +# 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 mock + +from rally.benchmark.scenarios.mistral import utils +from tests.unit import test + +BM_UTILS = "rally.benchmark.utils" +MISTRAL_UTILS = "rally.benchmark.scenarios.mistral.utils" + + +class MistralScenarioTestCase(test.TestCase): + + def setUp(self): + super(MistralScenarioTestCase, self).setUp() + + def _test_atomic_action_timer(self, atomic_actions, name): + action_duration = atomic_actions.get(name) + self.assertIsNotNone(action_duration) + self.assertIsInstance(action_duration, float) + + @mock.patch(MISTRAL_UTILS + ".MistralScenario.clients") + def test_list_workbooks(self, mock_clients): + wbs_list = [] + mock_clients("mistral").workbooks.list.return_value = wbs_list + scenario = utils.MistralScenario() + return_wbs_list = scenario._list_workbooks() + self.assertEqual(wbs_list, return_wbs_list) + self._test_atomic_action_timer(scenario.atomic_actions(), + "mistral.list_workbooks") + + @mock.patch(MISTRAL_UTILS + ".MistralScenario.clients") + def test_create_workbook(self, mock_clients): + definition = "version: \"2.0\"\nname: wb" + mock_clients("mistral").workbooks.create.return_value = "wb" + scenario = utils.MistralScenario() + wb = scenario._create_workbook(definition) + self.assertEqual("wb", wb) + self._test_atomic_action_timer(scenario.atomic_actions(), + "mistral.create_workbook") + + @mock.patch(MISTRAL_UTILS + ".MistralScenario.clients") + def test_delete_workbook(self, mock_clients): + wb = mock.Mock() + wb.name = "wb" + mock_clients("mistral").workbooks.delete.return_value = "ok" + scenario = utils.MistralScenario() + scenario._delete_workbook(wb.name) + mock_clients("mistral").workbooks.delete.assert_called_once_with( + wb.name + ) + self._test_atomic_action_timer(scenario.atomic_actions(), + "mistral.delete_workbook") diff --git a/tests/unit/benchmark/scenarios/mistral/test_workbooks.py b/tests/unit/benchmark/scenarios/mistral/test_workbooks.py new file mode 100644 index 0000000000..ff8a2f328c --- /dev/null +++ b/tests/unit/benchmark/scenarios/mistral/test_workbooks.py @@ -0,0 +1,54 @@ +# Copyright 2015: Mirantis Inc. +# 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 mock + +from rally.benchmark.scenarios.mistral import workbooks +from tests.unit import test + +MISTRAL_WBS = "rally.benchmark.scenarios.mistral.workbooks.MistralWorkbooks" + + +class MistralWorkbooksTestCase(test.TestCase): + + @mock.patch(MISTRAL_WBS + "._list_workbooks") + def test_list_workbooks(self, mock_list): + mistral_scenario = workbooks.MistralWorkbooks() + mistral_scenario.list_workbooks() + mock_list.assert_called_once_with() + + @mock.patch(MISTRAL_WBS + "._create_workbook") + def test_create_workbook(self, mock_create): + mistral_scenario = workbooks.MistralWorkbooks() + definition = "---\nversion: \"2.0\"\nname: wb" + fake_wb = mock.MagicMock() + fake_wb.name = "wb" + mock_create.return_value = fake_wb + mistral_scenario.create_delete_workbook(definition) + + self.assertEqual(1, mock_create.called) + + @mock.patch(MISTRAL_WBS + "._delete_workbook") + @mock.patch(MISTRAL_WBS + "._create_workbook") + def test_create_delete_workbook(self, mock_create, mock_delete): + mistral_scenario = workbooks.MistralWorkbooks() + definition = "---\nversion: \"2.0\"\nname: wb" + fake_wb = mock.MagicMock() + fake_wb.name = "wb" + mock_create.return_value = fake_wb + mistral_scenario.create_delete_workbook(definition, do_delete=True) + + self.assertEqual(1, mock_create.called) + mock_delete.assert_called_once_with(fake_wb.name) diff --git a/tests/unit/benchmark/test_types.py b/tests/unit/benchmark/test_types.py index 33a547d88d..1f0bfb5e32 100644 --- a/tests/unit/benchmark/test_types.py +++ b/tests/unit/benchmark/test_types.py @@ -251,3 +251,26 @@ class PreprocessTestCase(test.TestCase): mock_osclients.Clients.assert_called_once_with( context["admin"]["endpoint"]) self.assertEqual({"a": 20, "b": 20}, result) + + +class FileTypeTestCase(test.TestCase): + + def setUp(self): + super(FileTypeTestCase, self).setUp() + self.clients = fakes.FakeClients() + + @mock.patch("rally.benchmark.types.open", + side_effect=mock.mock_open(read_data="file_context"), + create=True) + def test_transform_by_path(self, mock_open): + resource_config = "file.yaml" + file_context = types.FileType.transform( + clients=self.clients, + resource_config=resource_config) + self.assertEqual(file_context, "file_context") + + def test_transform_by_path_no_match(self): + resource_config = "nonexistant.yaml" + self.assertRaises(IOError, + types.FileType.transform, + self.clients, resource_config) diff --git a/tests/unit/benchmark/test_validation.py b/tests/unit/benchmark/test_validation.py index 80d1e43050..a175495bbd 100644 --- a/tests/unit/benchmark/test_validation.py +++ b/tests/unit/benchmark/test_validation.py @@ -566,3 +566,17 @@ class ValidatorsTestCase(test.TestCase): result = validator(context, clients, mock.MagicMock()) self.assertFalse(result.is_valid, result.msg) + + def test_required_clients(self): + validator = self._unwrap_validator(validation.required_clients, + "keystone", "nova") + + clients = mock.MagicMock() + clients.keystone.return_value = "keystone" + clients.nova.return_value = "nova" + result = validator({}, clients, None) + self.assertTrue(result.is_valid, result.msg) + + clients.nova.side_effect = ImportError + result = validator({}, clients, None) + self.assertFalse(result.is_valid, result.msg) diff --git a/tests/unit/fakes.py b/tests/unit/fakes.py index 7a62fa4de0..88c56c7aad 100644 --- a/tests/unit/fakes.py +++ b/tests/unit/fakes.py @@ -279,6 +279,12 @@ class FakeAvailabilityZone(FakeResource): self.hosts = mock.MagicMock() +class FakeWorkbook(FakeResource): + def __init__(self, manager=None): + super(FakeWorkbook, self).__init__(manager) + self.workbook = mock.MagicMock() + + class FakeManager(object): def __init__(self): @@ -806,6 +812,15 @@ class FakeAvailabilityZonesManager(FakeManager): return [self.zones] +class FakeWorkbookManager(FakeManager): + def __init__(self): + super(FakeWorkbookManager, self).__init__() + self.workbook = FakeWorkbook() + + def list(self): + return [self.workbook] + + class FakeServiceCatalog(object): def get_endpoints(self): return {"image": [{"publicURL": "http://fake.to"}], @@ -1188,6 +1203,12 @@ class FakeTroveClient(object): self.instances = FakeDbInstanceManager() +class FakeMistralClient(object): + + def __init__(self): + self.workbook = FakeWorkbookManager() + + class FakeClients(object): def __init__(self, endpoint_=None): @@ -1202,6 +1223,7 @@ class FakeClients(object): self._ceilometer = None self._zaqar = None self._trove = None + self._mistral = None self._endpoint = endpoint_ or objects.Endpoint( "http://fake.example.org:5000/v2.0/", "fake_username", @@ -1266,6 +1288,11 @@ class FakeClients(object): self._trove = FakeTroveClient() return self._trove + def mistral(self): + if not self._mistral: + self._mistral = FakeMistralClient() + return self._mistral + class FakeRunner(object): diff --git a/tests/unit/test_osclients.py b/tests/unit/test_osclients.py index d84f49c384..0d269751b8 100644 --- a/tests/unit/test_osclients.py +++ b/tests/unit/test_osclients.py @@ -262,6 +262,30 @@ class OSClientsTestCase(test.TestCase): mock_trove.Client.assert_called_once_with("1.0", **kw) self.assertEqual(self.clients.cache["trove"], fake_trove) + def test_mistral(self): + fake_mistral = fakes.FakeMistralClient() + mock_mistral = mock.Mock() + mock_mistral.client.client.return_value = fake_mistral + + self.assertNotIn("mistral", self.clients.cache) + with mock.patch.dict( + "sys.modules", {"mistralclient": mock_mistral, + "mistralclient.api": mock_mistral}): + client = self.clients.mistral() + self.assertEqual(fake_mistral, client) + self.service_catalog.url_for.assert_called_once_with( + service_type="workflowv2", + endpoint_type=consts.EndpointType.PUBLIC, + region_name=self.endpoint.region_name + ) + fake_mistral_url = self.service_catalog.url_for.return_value + mock_mistral.client.client.assert_called_once_with( + mistral_url=fake_mistral_url, + service_type="workflowv2", + auth_token=self.fake_keystone.auth_token + ) + self.assertEqual(fake_mistral, self.clients.cache["mistral"]) + @mock.patch("rally.osclients.Clients.keystone") def test_services(self, mock_keystone): available_services = {consts.ServiceType.IDENTITY: {},