diff --git a/rally-jobs/rally-ironic.yaml b/rally-jobs/rally-ironic.yaml new file mode 100644 index 00000000..905f195b --- /dev/null +++ b/rally-jobs/rally-ironic.yaml @@ -0,0 +1,18 @@ +--- + {% for s in ("create_and_list_node", "create_and_delete_node") %} + IronicNodes.{{s}}: + - + args: + driver: "pxe_ssh" + runner: + type: "constant" + times: 10 + concurrency: 2 + context: + users: + tenants: 5 + users_per_tenant: 1 + sla: + failure_rate: + max: 0 + {% endfor %} \ No newline at end of file diff --git a/rally/consts.py b/rally/consts.py index d0f79659..1ee980be 100644 --- a/rally/consts.py +++ b/rally/consts.py @@ -105,6 +105,7 @@ class _Service(utils.ImmutableMixin, utils.EnumMixin): SWIFT = "swift" MISTRAL = "mistral" MURANO = "murano" + IRONIC = "ironic" class _ServiceType(utils.ImmutableMixin, utils.EnumMixin): @@ -130,6 +131,7 @@ class _ServiceType(utils.ImmutableMixin, utils.EnumMixin): OBJECT_STORE = "object-store" WORKFLOW_EXECUTION = "workflowv2" APPLICATION_CATALOG = "application_catalog" + BARE_METAL = "baremetal" def __init__(self): self.__names = { @@ -152,7 +154,8 @@ class _ServiceType(utils.ImmutableMixin, utils.EnumMixin): self.DATA_PROCESSING: _Service.SAHARA, self.OBJECT_STORE: _Service.SWIFT, self.WORKFLOW_EXECUTION: _Service.MISTRAL, - self.APPLICATION_CATALOG: _Service.MURANO + self.APPLICATION_CATALOG: _Service.MURANO, + self.BARE_METAL: _Service.IRONIC, } def __getitem__(self, service_type): diff --git a/rally/plugins/openstack/scenarios/ironic/__init__.py b/rally/plugins/openstack/scenarios/ironic/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rally/plugins/openstack/scenarios/ironic/nodes.py b/rally/plugins/openstack/scenarios/ironic/nodes.py new file mode 100644 index 00000000..e9869847 --- /dev/null +++ b/rally/plugins/openstack/scenarios/ironic/nodes.py @@ -0,0 +1,78 @@ +# 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 import consts +from rally.plugins.openstack.scenarios.ironic import utils +from rally.task.scenarios import base +from rally.task import validation + + +class IronicNodes(utils.IronicScenario): + """Base class for Ironic scenarios with basic atomic actions.""" + + @validation.required_parameters("driver") + @validation.required_services(consts.Service.IRONIC) + @validation.required_openstack(admin=True) + @base.scenario(context={"admin_cleanup": ["ironic"]}) + def create_and_list_node( + self, associated=None, maintenance=None, + marker=None, limit=None, detail=False, sort_key=None, + sort_dir=None, **kwargs): + """Create and list nodes. + + :param associated: Optional. Either a Boolean or a string + representation of a Boolean that indicates whether + to return a list of associated (True or "True") or + unassociated (False or "False") nodes. + :param maintenance: Optional. Either a Boolean or a string + representation of a Boolean that indicates whether + to return nodes in maintenance mode (True or + "True"), or not in maintenance mode (False or + "False"). + :param marker: Optional, the UUID of a node, eg the last + node from a previous result set. Return + the next result set. + :param limit: The maximum number of results to return per + request, if: + 1) limit > 0, the maximum number of nodes to return. + 2) limit == 0, return the entire list of nodes. + 3) limit param is NOT specified (None), the number of items + returned respect the maximum imposed by the Ironic API + (see Ironic's api.max_limit option). + :param detail: Optional, boolean whether to return detailed + information about nodes. + :param sort_key: Optional, field used for sorting. + :param sort_dir: Optional, direction of sorting, either 'asc' (the + default) or 'desc'. + :param kwargs: Optional additional arguments for node creation + """ + + self._create_node(**kwargs) + + self._list_nodes( + associated=associated, maintenance=maintenance, marker=marker, + limit=limit, detail=detail, sort_key=sort_key, sort_dir=sort_dir) + + @validation.required_parameters("driver") + @validation.required_services(consts.Service.IRONIC) + @validation.required_openstack(admin=True) + @base.scenario(context={"admin_cleanup": ["ironic"]}) + def create_and_delete_node(self, **kwargs): + """Create and delete node. + + :param kwargs: Optional additional arguments for node creation + """ + node = self._create_node(**kwargs) + self._delete_node(node.uuid) diff --git a/rally/plugins/openstack/scenarios/ironic/utils.py b/rally/plugins/openstack/scenarios/ironic/utils.py new file mode 100644 index 00000000..9b148dc8 --- /dev/null +++ b/rally/plugins/openstack/scenarios/ironic/utils.py @@ -0,0 +1,102 @@ +# 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 string + +from oslo_config import cfg + + +from rally.common import utils +from rally.plugins.openstack import scenario +from rally.task.scenarios import base + + +IRONIC_BENCHMARK_OPTS = [ + cfg.FloatOpt("ironic_node_create_poll_interval", + default=1.0, + help="Interval(in sec) between checks when waiting for node " + "creation."), +] + +CONF = cfg.CONF +benchmark_group = cfg.OptGroup(name="benchmark", title="benchmark options") +CONF.register_opts(IRONIC_BENCHMARK_OPTS, group=benchmark_group) + + +class IronicScenario(scenario.OpenStackScenario): + """Base class for Ironic scenarios with basic atomic actions.""" + + @base.atomic_action_timer("ironic.create_node") + def _create_node(self, **kwargs): + """Create node immediately. + + :param kwargs: optional parameters to create image + :returns: node object + """ + if "name" not in kwargs: + # NOTE(rvasilets): can't use _generate_random_name() because + # ironic have specific format for node name. + # Check that the supplied hostname conforms to: + # * http://en.wikipedia.org/wiki/Hostname + # * http://tools.ietf.org/html/rfc952 + # * http://tools.ietf.org/html/rfc1123 + # or the name could be just uuid. + kwargs["name"] = utils.generate_random_name( + prefix="rally", choice=string.ascii_lowercase + string.digits) + + return self.admin_clients("ironic").node.create(**kwargs) + + @base.atomic_action_timer("ironic.list_nodes") + def _list_nodes(self, associated=None, maintenance=None, marker=None, + limit=None, detail=False, sort_key=None, sort_dir=None): + """Return list of nodes. + + :param associated: Optional. Either a Boolean or a string + representation of a Boolean that indicates whether + to return a list of associated (True or "True") or + unassociated (False or "False") nodes. + :param maintenance: Optional. Either a Boolean or a string + representation of a Boolean that indicates whether + to return nodes in maintenance mode (True or + "True"), or not in maintenance mode (False or + "False"). + :param marker: Optional, the UUID of a node, eg the last + node from a previous result set. Return + the next result set. + :param limit: The maximum number of results to return per + request, if: + 1) limit > 0, the maximum number of nodes to return. + 2) limit == 0, return the entire list of nodes. + 3) limit param is NOT specified (None), the number of items + returned respect the maximum imposed by the Ironic API + (see Ironic's api.max_limit option). + :param detail: Optional, boolean whether to return detailed information + about nodes. + :param sort_key: Optional, field used for sorting. + :param sort_dir: Optional, direction of sorting, either 'asc' (the + default) or 'desc'. + :returns: A list of nodes. + """ + return self.admin_clients("ironic").node.list( + associated=associated, maintenance=maintenance, marker=marker, + limit=limit, detail=detail, sort_key=sort_key, sort_dir=sort_dir) + + @base.atomic_action_timer("ironic.delete_node") + def _delete_node(self, node_id): + """Delete the node with specific id. + + :param node_id: id of the node to be deleted + """ + self.admin_clients("ironic").node.delete(node_id) diff --git a/samples/tasks/scenarios/ironic/create-and-delete-node.json b/samples/tasks/scenarios/ironic/create-and-delete-node.json new file mode 100644 index 00000000..1e4a42fd --- /dev/null +++ b/samples/tasks/scenarios/ironic/create-and-delete-node.json @@ -0,0 +1,20 @@ +{ + "IronicNodes.create_and_delete_node": [ + { + "args": { + "driver": "pxe_ssh" + }, + "runner": { + "type": "constant", + "times": 10, + "concurrency": 2 + }, + "context": { + "users": { + "tenants": 5, + "users_per_tenant": 1 + } + } + } + ] +} diff --git a/samples/tasks/scenarios/ironic/create-and-delete-node.yaml b/samples/tasks/scenarios/ironic/create-and-delete-node.yaml new file mode 100644 index 00000000..9fb1b33c --- /dev/null +++ b/samples/tasks/scenarios/ironic/create-and-delete-node.yaml @@ -0,0 +1,13 @@ +--- + IronicNodes.create_and_delete_node: + - + args: + driver: "pxe_ssh" + runner: + type: "constant" + times: 10 + concurrency: 2 + context: + users: + tenants: 5 + users_per_tenant: 1 diff --git a/samples/tasks/scenarios/ironic/create-and-list-node.json b/samples/tasks/scenarios/ironic/create-and-list-node.json new file mode 100644 index 00000000..683d3404 --- /dev/null +++ b/samples/tasks/scenarios/ironic/create-and-list-node.json @@ -0,0 +1,20 @@ +{ + "IronicNodes.create_and_list_node": [ + { + "args": { + "driver": "pxe_ssh" + }, + "runner": { + "type": "constant", + "times": 10, + "concurrency": 2 + }, + "context": { + "users": { + "tenants": 5, + "users_per_tenant": 1 + } + } + } + ] +} diff --git a/samples/tasks/scenarios/ironic/create-and-list-node.yaml b/samples/tasks/scenarios/ironic/create-and-list-node.yaml new file mode 100644 index 00000000..6f091b7d --- /dev/null +++ b/samples/tasks/scenarios/ironic/create-and-list-node.yaml @@ -0,0 +1,13 @@ +--- + IronicNodes.create_and_list_node: + - + args: + driver: "pxe_ssh" + runner: + type: "constant" + times: 10 + concurrency: 2 + context: + users: + tenants: 5 + users_per_tenant: 1 diff --git a/tests/unit/plugins/openstack/scenarios/ironic/__init__.py b/tests/unit/plugins/openstack/scenarios/ironic/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/plugins/openstack/scenarios/ironic/test_nodes.py b/tests/unit/plugins/openstack/scenarios/ironic/test_nodes.py new file mode 100644 index 00000000..613868c6 --- /dev/null +++ b/tests/unit/plugins/openstack/scenarios/ironic/test_nodes.py @@ -0,0 +1,56 @@ +# 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.plugins.openstack.scenarios.ironic import nodes +from tests.unit import test + + +class IronicNodesTestCase(test.ScenarioTestCase): + + def test_create_and_list_node(self): + scenario = nodes.IronicNodes() + scenario._create_node = mock.Mock() + scenario._list_nodes = mock.Mock() + fake_params = { + "sort_dir": "foo1", + "associated": "foo2", + "sort_key": "foo3", + "detail": True, + "limit": "foo4", + "maintenance": "foo5", + "marker": "foo6", + "fake_parameter1": "foo7" + } + scenario.create_and_list_node(**fake_params) + + scenario._create_node.assert_called_once_with(fake_parameter1="foo7") + scenario._list_nodes.assert_called_once_with( + sort_dir="foo1", associated="foo2", sort_key="foo3", detail=True, + limit="foo4", maintenance="foo5", marker="foo6") + + def test_create_and_delete_node(self): + fake_node = mock.Mock(uuid="fake_uuid") + scenario = nodes.IronicNodes() + scenario._create_node = mock.Mock(return_value=fake_node) + scenario._delete_node = mock.Mock() + + scenario.create_and_delete_node(fake_parameter1="fake1", + fake_parameter2="fake2") + scenario._create_node.assert_called_once_with(fake_parameter1="fake1", + fake_parameter2="fake2") + + scenario._delete_node.assert_called_once_with("fake_uuid") diff --git a/tests/unit/plugins/openstack/scenarios/ironic/test_utils.py b/tests/unit/plugins/openstack/scenarios/ironic/test_utils.py new file mode 100644 index 00000000..7a5f34ac --- /dev/null +++ b/tests/unit/plugins/openstack/scenarios/ironic/test_utils.py @@ -0,0 +1,68 @@ +# 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.plugins.openstack.scenarios.ironic import utils +from tests.unit import test + +IRONIC_UTILS = "rally.plugins.openstack.scenarios.ironic.utils" + + +class IronicScenarioTestCase(test.ScenarioTestCase): + + @mock.patch("rally.common.utils.generate_random_name") + def test__create_node(self, mock_generate_random_name): + mock_generate_random_name.return_value = "rally_fake_random_string" + self.admin_clients("ironic").node.create.return_value = "fake_node" + scenario = utils.IronicScenario() + create_node = scenario._create_node(fake_param="foo") + + self.assertEqual("fake_node", create_node) + self.admin_clients("ironic").node.create.assert_called_once_with( + fake_param="foo", name="rally_fake_random_string") + self._test_atomic_action_timer(scenario.atomic_actions(), + "ironic.create_node") + + def test__delete_node(self): + mock_node_delete = mock.Mock() + self.admin_clients("ironic").node.delete = mock_node_delete + scenario = utils.IronicScenario() + scenario._delete_node("fake_id") + + self.admin_clients("ironic").node.delete.assert_called_once_with( + "fake_id") + self._test_atomic_action_timer(scenario.atomic_actions(), + "ironic.delete_node") + + def test__list_nodes(self): + self.admin_clients("ironic").node.list.return_value = ["fake"] + scenario = utils.IronicScenario() + fake_params = { + "sort_dir": "foo1", + "associated": "foo2", + "sort_key": "foo3", + "detail": True, + "limit": "foo4", + "maintenance": "foo5", + "marker": "foo6" + } + return_nodes_list = scenario._list_nodes(**fake_params) + self.assertEqual(["fake"], return_nodes_list) + self.admin_clients("ironic").node.list.assert_called_once_with( + sort_dir="foo1", associated="foo2", sort_key="foo3", detail=True, + limit="foo4", maintenance="foo5", marker="foo6") + self._test_atomic_action_timer(scenario.atomic_actions(), + "ironic.list_nodes")