diff --git a/rally-jobs/rally-senlin.yaml b/rally-jobs/rally-senlin.yaml new file mode 100644 index 00000000..698a4687 --- /dev/null +++ b/rally-jobs/rally-senlin.yaml @@ -0,0 +1,27 @@ +--- + SenlinClusters.create_and_delete_profile_cluster: + - + args: + profile_spec: + type: os.nova.server + version: 1.0 + properties: + name: cirros_server + flavor: 1 + image: "cirros-0.3.4-x86_64-uec" + networks: + - network: private + desired_capacity: 3 + min_size: 0 + max_size: 5 + runner: + type: "constant" + times: 3 + concurrency: 2 + context: + users: + tenants: 2 + users_per_tenant: 2 + sla: + failure_rate: + max: 0 diff --git a/rally/plugins/openstack/cleanup/resources.py b/rally/plugins/openstack/cleanup/resources.py index 37fca765..ca5c2a2f 100644 --- a/rally/plugins/openstack/cleanup/resources.py +++ b/rally/plugins/openstack/cleanup/resources.py @@ -74,6 +74,37 @@ class HeatStack(base.ResourceManager): return self.raw_resource.stack_name +# SENLIN + +_senlin_order = get_order(150) + + +@base.resource(service=None, resource=None, admin_required=True) +class SenlinMixin(base.ResourceManager): + + def _manager(self): + client = self._admin_required and self.admin or self.user + return getattr(client, self._service)() + + def list(self): + return getattr(self._manager(), self._resource)() + + def delete(self): + # make singular form of resource name from plural form + res_name = self._resource[:-1] + return getattr(self._manager(), "delete_%s" % res_name)(self.id) + + +@base.resource("senlin", "clusters", order=next(_senlin_order)) +class SenlinCluster(SenlinMixin): + """Resource class for Senlin Cluster.""" + + +@base.resource("senlin", "profiles", order=next(_senlin_order)) +class SenlinProfile(SenlinMixin): + """Resource class for Senlin Profile.""" + + # NOVA _nova_order = get_order(200) diff --git a/rally/plugins/openstack/scenarios/senlin/__init__.py b/rally/plugins/openstack/scenarios/senlin/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rally/plugins/openstack/scenarios/senlin/clusters.py b/rally/plugins/openstack/scenarios/senlin/clusters.py new file mode 100644 index 00000000..a848d272 --- /dev/null +++ b/rally/plugins/openstack/scenarios/senlin/clusters.py @@ -0,0 +1,48 @@ +# 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 import scenario +from rally.plugins.openstack.scenarios.senlin import utils +from rally.task import validation + + +class SenlinClusters(utils.SenlinScenario): + """Benchmark scenarios for Senlin clusters.""" + + @validation.required_openstack(admin=True) + @validation.required_services(consts.Service.SENLIN) + @scenario.configure(context={"cleanup": ["senlin"]}) + def create_and_delete_profile_cluster(self, profile_spec, + desired_capacity=0, min_size=0, + max_size=-1, timeout=3600, + metadata=None): + """Create a profile and a cluster and then delete them. + + Measure the "senlin profile-create", "senlin profile-delete", + "senlin cluster-create" and "senlin cluster-delete" commands + performance. + + :param profile_spec: spec dictionary used to create profile + :param desired_capacity: The capacity or initial number of nodes + owned by the cluster + :param min_size: The minimum number of nodes owned by the cluster + :param max_size: The maximum number of nodes owned by the cluster. + -1 means no limit + :param timeout: The timeout value in seconds for cluster creation + :param metadata: A set of key value pairs to associate with the cluster + """ + profile = self._create_profile(profile_spec) + cluster = self._create_cluster(profile.id, desired_capacity, + min_size, max_size, timeout, metadata) + self._delete_cluster(cluster) + self._delete_profile(profile) diff --git a/rally/plugins/openstack/scenarios/senlin/utils.py b/rally/plugins/openstack/scenarios/senlin/utils.py new file mode 100644 index 00000000..f8d9baca --- /dev/null +++ b/rally/plugins/openstack/scenarios/senlin/utils.py @@ -0,0 +1,153 @@ +# 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_config import cfg + +from rally import exceptions +from rally.plugins.openstack import scenario +from rally.task import atomic +from rally.task import utils + +SENLIN_BENCHMARK_OPTS = [ + cfg.FloatOpt("senlin_action_timeout", + default=3600, + help="Time in seconds to wait for senlin action to finish."), +] + +CONF = cfg.CONF +benchmark_group = cfg.OptGroup(name="benchmark", title="benchmark options") +CONF.register_opts(SENLIN_BENCHMARK_OPTS, group=benchmark_group) + + +class SenlinScenario(scenario.OpenStackScenario): + """Base class for Senlin scenarios with basic atomic actions.""" + + @atomic.action_timer("senlin.list_clusters") + def _list_clusters(self, **queries): + """Return user cluster list. + + :param kwargs \*\*queries: Optional query parameters to be sent to + restrict the clusters to be returned. Available parameters include: + + * name: The name of a cluster. + * status: The current status of a cluster. + * sort: A list of sorting keys separated by commas. Each sorting + key can optionally be attached with a sorting direction + modifier which can be ``asc`` or ``desc``. + * limit: Requests a specified size of returned items from the + query. Returns a number of items up to the specified limit + value. + * marker: Specifies the ID of the last-seen item. Use the limit + parameter to make an initial limited request and use the ID of + the last-seen item from the response as the marker parameter + value in a subsequent limited request. + * global_project: A boolean value indicating whether clusters + from all projects will be returned. + + :returns: list of clusters according to query. + """ + return list(self.admin_clients("senlin").clusters(**queries)) + + @atomic.action_timer("senlin.create_cluster") + def _create_cluster(self, profile_id, desired_capacity=0, min_size=0, + max_size=-1, timeout=60, metadata=None): + """Create a new cluster from attributes. + + :param profile_id: ID of profile used to create cluster + :param desired_capacity: The capacity or initial number of nodes + owned by the cluster + :param min_size: The minimum number of nodes owned by the cluster + :param max_size: The maximum number of nodes owned by the cluster. + -1 means no limit + :param timeout: The timeout value in minutes for cluster creation + :param metadata: A set of key value pairs to associate with the cluster + + :returns: object of cluster created. + """ + attrs = { + "profile_id": profile_id, + "name": self.generate_random_name(), + "desired_capacity": desired_capacity, + "min_size": min_size, + "max_size": max_size, + "metadata": metadata, + "timeout": timeout + } + + cluster = self.admin_clients("senlin").create_cluster(**attrs) + cluster = utils.wait_for_status( + cluster, + ready_statuses=["ACTIVE"], + failure_statuses=["ERROR"], + update_resource=self._get_cluster, + timeout=CONF.benchmark.senlin_action_timeout) + + return cluster + + def _get_cluster(self, cluster): + """Get cluster details. + + :param cluster: cluster to get + + :returns: object of cluster + """ + try: + return self.admin_clients("senlin").get_cluster(cluster.id) + except Exception as e: + if getattr(e, "code", getattr(e, "http_status", 400)) == 404: + raise exceptions.GetResourceNotFound(resource=cluster.id) + raise exceptions.GetResourceFailure(resource=cluster.id, err=e) + + @atomic.action_timer("senlin.delete_cluster") + def _delete_cluster(self, cluster): + """Delete given cluster. + + Returns after the cluster is successfully deleted. + + :param cluster: cluster object to delete + """ + self.admin_clients("senlin").delete_cluster(cluster) + utils.wait_for_status( + cluster, + ready_statuses=["DELETED"], + failure_statuses=["ERROR"], + check_deletion=True, + update_resource=self._get_cluster, + timeout=CONF.benchmark.senlin_action_timeout) + + @atomic.action_timer("senlin.create_profile") + def _create_profile(self, spec, metadata=None): + """Create a new profile from attributes. + + :param spec: spec dictionary used to create profile + :param metadata: A set of key value pairs to associate with the + profile + + :returns: object of profile created + """ + attrs = {} + attrs["spec"] = spec + attrs["name"] = self.generate_random_name() + if metadata: + attrs["metadata"] = metadata + + return self.admin_clients("senlin").create_profile(**attrs) + + @atomic.action_timer("senlin.delete_profile") + def _delete_profile(self, profile): + """Delete given profile. + + Returns after the profile is successfully deleted. + + :param profile: profile object to be deleted + """ + self.admin_clients("senlin").delete_profile(profile) diff --git a/samples/tasks/scenarios/senlin/create-and-delete-profile-cluster.json b/samples/tasks/scenarios/senlin/create-and-delete-profile-cluster.json new file mode 100644 index 00000000..29ff697d --- /dev/null +++ b/samples/tasks/scenarios/senlin/create-and-delete-profile-cluster.json @@ -0,0 +1,34 @@ +{ + "SenlinClusters.create_and_delete_profile_cluster": [ + { + "args": { + "profile_spec": { + "type": "os.nova.server", + "version": "1.0", + "properties": { + "name": "cirros_server", + "flavor": 1, + "image": "cirros-0.3.4-x86_64-uec", + "networks": [ + { "network": "private" } + ] + } + }, + "desired_capacity": 3, + "min_size": 0, + "max_size": 5 + }, + "runner": { + "type": "constant", + "times": 3, + "concurrency": 1 + }, + "context": { + "users": { + "tenants": 1, + "users_per_tenant": 1 + } + } + } + ] +} diff --git a/samples/tasks/scenarios/senlin/create-and-delete-profile-cluster.yaml b/samples/tasks/scenarios/senlin/create-and-delete-profile-cluster.yaml new file mode 100644 index 00000000..2db234f7 --- /dev/null +++ b/samples/tasks/scenarios/senlin/create-and-delete-profile-cluster.yaml @@ -0,0 +1,24 @@ +--- + SenlinClusters.create_and_delete_profile_cluster: + - + args: + profile_spec: + type: os.nova.server + version: "1.0" + properties: + name: cirros_server + flavor: 1 + image: "cirros-0.3.4-x86_64-uec" + networks: + - network: private + desired_capacity: 3 + min_size: 0 + max_size: 5 + runner: + type: "constant" + times: 3 + concurrency: 1 + context: + users: + tenants: 1 + users_per_tenant: 1 diff --git a/tests/ci/osresources.py b/tests/ci/osresources.py index 14c6be74..f79b743f 100755 --- a/tests/ci/osresources.py +++ b/tests/ci/osresources.py @@ -208,6 +208,17 @@ class Cinder(ResourceManager): search_opts={"all_tenants": True}) +class Senlin(ResourceManager): + + REQUIRED_SERVICE = consts.Service.SENLIN + + def list_clusters(self): + return self.client.clusters() + + def list_profiles(self): + return self.client.profiles() + + class Watcher(ResourceManager): REQUIRED_SERVICE = consts.Service.WATCHER diff --git a/tests/unit/plugins/openstack/cleanup/test_resources.py b/tests/unit/plugins/openstack/cleanup/test_resources.py index ad509ae9..1df58d20 100644 --- a/tests/unit/plugins/openstack/cleanup/test_resources.py +++ b/tests/unit/plugins/openstack/cleanup/test_resources.py @@ -756,6 +756,39 @@ class FuelEnvironmentTestCase(test.TestCase): self.assertEqual(envs[:-1], fres.list()) +class SenlinMixinTestCase(test.TestCase): + + def test__manager(self): + senlin = resources.SenlinMixin() + senlin._service = "senlin" + senlin.user = mock.MagicMock() + self.assertEqual(senlin.user.senlin.return_value, senlin._manager()) + + def test_list(self): + senlin = resources.SenlinMixin() + senlin._service = "senlin" + senlin.user = mock.MagicMock() + senlin._resource = "some_resources" + + some_resources = [{"name": "resource1"}, {"name": "resource2"}] + senlin.user.senlin().some_resources.return_value = some_resources + + self.assertEqual(some_resources, senlin.list()) + senlin.user.senlin().some_resources.assert_called_once_with() + + def test_delete(self): + senlin = resources.SenlinMixin() + senlin._service = "senlin" + senlin.user = mock.MagicMock() + senlin._resource = "some_resources" + senlin.id = "TEST_ID" + senlin.user.senlin().delete_some_resource.return_value = None + + senlin.delete() + senlin.user.senlin().delete_some_resource.assert_called_once_with( + "TEST_ID") + + class WatcherTemplateTestCase(test.TestCase): def test_id(self): diff --git a/tests/unit/plugins/openstack/scenarios/senlin/__init__.py b/tests/unit/plugins/openstack/scenarios/senlin/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/plugins/openstack/scenarios/senlin/test_clusters.py b/tests/unit/plugins/openstack/scenarios/senlin/test_clusters.py new file mode 100644 index 00000000..d3497cc7 --- /dev/null +++ b/tests/unit/plugins/openstack/scenarios/senlin/test_clusters.py @@ -0,0 +1,43 @@ +# 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.senlin import clusters +from tests.unit import test + + +class SenlinClustersTestCase(test.ScenarioTestCase): + + def test_create_and_delete_cluster(self): + profile_spec = {"k1": "v1"} + mock_profile = mock.Mock(id="fake_profile_id") + mock_cluster = mock.Mock() + scenario = clusters.SenlinClusters(self.context) + scenario._create_cluster = mock.Mock(return_value=mock_cluster) + scenario._create_profile = mock.Mock(return_value=mock_profile) + scenario._delete_cluster = mock.Mock() + scenario._delete_profile = mock.Mock() + + scenario.create_and_delete_profile_cluster(profile_spec, + desired_capacity=1, + min_size=0, + max_size=3, + timeout=60, + metadata={"k2": "v2"}) + + scenario._create_profile.assert_called_once_with(profile_spec) + scenario._create_cluster.assert_called_once_with("fake_profile_id", + 1, 0, 3, 60, + {"k2": "v2"}) + scenario._delete_cluster.assert_called_once_with(mock_cluster) + scenario._delete_profile.assert_called_once_with(mock_profile) diff --git a/tests/unit/plugins/openstack/scenarios/senlin/test_utils.py b/tests/unit/plugins/openstack/scenarios/senlin/test_utils.py new file mode 100644 index 00000000..c0f40054 --- /dev/null +++ b/tests/unit/plugins/openstack/scenarios/senlin/test_utils.py @@ -0,0 +1,152 @@ +# 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 oslo_config import cfg + +from rally import exceptions +from rally.plugins.openstack.scenarios.senlin import utils +from tests.unit import test + +SENLIN_UTILS = "rally.plugins.openstack.scenarios.senlin.utils." +CONF = cfg.CONF + + +class SenlinScenarioTestCase(test.ScenarioTestCase): + + def test_list_cluster(self): + fake_cluster_list = ["cluster1", "cluster2"] + self.admin_clients("senlin").clusters.return_value = fake_cluster_list + scenario = utils.SenlinScenario(self.context) + result = scenario._list_clusters() + + self.assertEqual(list(fake_cluster_list), result) + self.admin_clients("senlin").clusters.assert_called_once_with() + + def test_list_cluster_with_queries(self): + fake_cluster_list = ["cluster1", "cluster2"] + self.admin_clients("senlin").clusters.return_value = fake_cluster_list + scenario = utils.SenlinScenario(self.context) + result = scenario._list_clusters(status="ACTIVE") + + self.assertEqual(list(fake_cluster_list), result) + self.admin_clients("senlin").clusters.assert_called_once_with( + status="ACTIVE") + + @mock.patch(SENLIN_UTILS + "SenlinScenario.generate_random_name", + return_value="test_cluster") + def test_create_cluster(self, mock_generate_random_name): + fake_cluster = mock.Mock(id="fake_cluster_id") + res_cluster = mock.Mock() + self.admin_clients("senlin").create_cluster.return_value = fake_cluster + self.mock_wait_for_status.mock.return_value = res_cluster + scenario = utils.SenlinScenario(self.context) + result = scenario._create_cluster("fake_profile_id", + desired_capacity=1, + min_size=0, + max_size=3, + metadata={"k1": "v1"}, + timeout=60) + + self.assertEqual(res_cluster, result) + self.admin_clients("senlin").create_cluster.assert_called_once_with( + profile_id="fake_profile_id", name="test_cluster", + desired_capacity=1, min_size=0, max_size=3, metadata={"k1": "v1"}, + timeout=60) + self.mock_wait_for_status.mock.assert_called_once_with( + fake_cluster, ready_statuses=["ACTIVE"], + failure_statuses=["ERROR"], + update_resource=scenario._get_cluster, + timeout=CONF.benchmark.senlin_action_timeout) + mock_generate_random_name.assert_called_once_with() + self._test_atomic_action_timer(scenario.atomic_actions(), + "senlin.create_cluster") + + def test_get_cluster(self): + fake_cluster = mock.Mock(id="fake_cluster_id") + scenario = utils.SenlinScenario(context=self.context) + scenario._get_cluster(fake_cluster) + + self.admin_clients("senlin").get_cluster.assert_called_once_with( + "fake_cluster_id") + + def test_get_cluster_notfound(self): + fake_cluster = mock.Mock(id="fake_cluster_id") + ex = Exception() + ex.code = 404 + self.admin_clients("senlin").get_cluster.side_effect = ex + scenario = utils.SenlinScenario(context=self.context) + + self.assertRaises(exceptions.GetResourceNotFound, + scenario._get_cluster, + fake_cluster) + self.admin_clients("senlin").get_cluster.assert_called_once_with( + "fake_cluster_id") + + def test_get_cluster_failed(self): + fake_cluster = mock.Mock(id="fake_cluster_id") + ex = Exception() + ex.code = 500 + self.admin_clients("senlin").get_cluster.side_effect = ex + scenario = utils.SenlinScenario(context=self.context) + + self.assertRaises(exceptions.GetResourceFailure, + scenario._get_cluster, + fake_cluster) + self.admin_clients("senlin").get_cluster.assert_called_once_with( + "fake_cluster_id") + + def test_delete_cluster(self): + fake_cluster = mock.Mock() + scenario = utils.SenlinScenario(context=self.context) + scenario._delete_cluster(fake_cluster) + + self.admin_clients("senlin").delete_cluster.assert_called_once_with( + fake_cluster) + self.mock_wait_for_status.mock.assert_called_once_with( + fake_cluster, ready_statuses=["DELETED"], + failure_statuses=["ERROR"], check_deletion=True, + update_resource=scenario._get_cluster, + timeout=CONF.benchmark.senlin_action_timeout) + self._test_atomic_action_timer(scenario.atomic_actions(), + "senlin.delete_cluster") + + @mock.patch(SENLIN_UTILS + "SenlinScenario.generate_random_name", + return_value="test_profile") + def test_create_profile(self, mock_generate_random_name): + test_spec = { + "version": "1.0", + "type": "test_type", + "properties": { + "key1": "value1" + } + } + scenario = utils.SenlinScenario(self.context) + result = scenario._create_profile(test_spec, metadata={"k2": "v2"}) + + self.assertEqual( + self.admin_clients("senlin").create_profile.return_value, result) + self.admin_clients("senlin").create_profile.assert_called_once_with( + spec=test_spec, name="test_profile", metadata={"k2": "v2"}) + mock_generate_random_name.assert_called_once_with() + self._test_atomic_action_timer(scenario.atomic_actions(), + "senlin.create_profile") + + def test_delete_profile(self): + fake_profile = mock.Mock() + scenario = utils.SenlinScenario(context=self.context) + scenario._delete_profile(fake_profile) + + self.admin_clients("senlin").delete_profile.assert_called_once_with( + fake_profile) + self._test_atomic_action_timer(scenario.atomic_actions(), + "senlin.delete_profile")