diff --git a/rally-jobs/rally.yaml b/rally-jobs/rally.yaml index 6efcb79807..fb226cde68 100755 --- a/rally-jobs/rally.yaml +++ b/rally-jobs/rally.yaml @@ -296,6 +296,26 @@ failure_rate: max: 0 + CeilometerResource.get_tenant_resources: + - + runner: + type: "constant" + times: 10 + concurrency: 5 + context: + users: + tenants: 2 + users_per_tenant: 2 + ceilometer: + counter_name: "cpu_util" + counter_type: "gauge" + counter_volume: 1.0 + counter_unit: "instance" + resources_per_tenant: 3 + sla: + failure_rate: + max: 0 + CeilometerSamples.list_samples: - runner: @@ -306,6 +326,13 @@ users: tenants: 1 users_per_tenant: 1 + ceilometer: + counter_name: "cpu_util" + counter_type: "gauge" + counter_unit: "instance" + counter_volume: 1.0 + resources_per_tenant: 3 + samples_per_resource: 3 sla: failure_rate: max: 0 diff --git a/rally/benchmark/context/ceilometer.py b/rally/benchmark/context/ceilometer.py new file mode 100644 index 0000000000..8f7306cf5a --- /dev/null +++ b/rally/benchmark/context/ceilometer.py @@ -0,0 +1,100 @@ +# 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.context import base +from rally.benchmark.scenarios.ceilometer import utils as ceilometer_utils +from rally.common.i18n import _ +from rally.common import log as logging +from rally.common import utils as rutils +from rally import consts +from rally import osclients + + +LOG = logging.getLogger(__name__) + + +@base.context(name="ceilometer", order=450) +class CeilometerSampleGenerator(base.Context): + """Context for creating samples and collecting resources for benchmarks.""" + + CONFIG_SCHEMA = { + "type": "object", + "$schema": consts.JSON_SCHEMA, + "properties": { + "counter_name": { + "type": "string" + }, + "counter_type": { + "type": "string" + }, + "counter_unit": { + "type": "string" + }, + "counter_volume": { + "type": "number", + "minimum": 0 + }, + "resources_per_tenant": { + "type": "integer", + "minimum": 1 + }, + "samples_per_resource": { + "type": "integer", + "minimum": 1 + }, + }, + "required": ["counter_name", "counter_type", "counter_unit", + "counter_volume"], + "additionalProperties": False + } + + DEFAULT_CONFIG = { + "resources_per_tenant": 5, + "samples_per_resource": 5 + } + + @rutils.log_task_wrapper(LOG.info, _("Enter context: `Ceilometer`")) + def setup(self): + counter_name = self.config["counter_name"] + counter_type = self.config["counter_type"] + counter_unit = self.config["counter_unit"] + counter_volume = self.config["counter_volume"] + resources_per_tenant = self.config["resources_per_tenant"] + samples_per_resource = self.config["samples_per_resource"] + for user, tenant_id in rutils.iterate_per_tenants( + self.context["users"]): + self.context["tenants"][tenant_id]["samples"] = [] + self.context["tenants"][tenant_id]["resources"] = [] + clients = osclients.Clients(user["endpoint"]) + scenario = ceilometer_utils.CeilometerScenario( + clients=clients) + for i in range(resources_per_tenant): + for j in range(samples_per_resource): + try: + sample = scenario._create_sample(counter_name, + counter_type, + counter_unit, + counter_volume) + self.context["tenants"][tenant_id]["samples"].append( + sample[0].to_dict()) + except Exception as e: + LOG.error("Creating a sample failed: %(error)s" + % {"error": e}) + self.context["tenants"][tenant_id]["resources"].append( + sample[0].resource_id) + + @rutils.log_task_wrapper(LOG.info, _("Exit context: `Ceilometer`")) + def cleanup(self): + # We don't have API for removal of samples and resources + pass \ No newline at end of file diff --git a/rally/benchmark/scenarios/ceilometer/resources.py b/rally/benchmark/scenarios/ceilometer/resources.py index e737cc2381..c95614f44a 100644 --- a/rally/benchmark/scenarios/ceilometer/resources.py +++ b/rally/benchmark/scenarios/ceilometer/resources.py @@ -16,6 +16,7 @@ from rally.benchmark.scenarios import base from rally.benchmark.scenarios.ceilometer import utils as ceilometerutils from rally.benchmark import validation from rally import consts +from rally import exceptions class CeilometerResource(ceilometerutils.CeilometerScenario): @@ -29,4 +30,21 @@ class CeilometerResource(ceilometerutils.CeilometerScenario): This scenario fetches list of all resources using GET /v2/resources. """ - self._list_resources() \ No newline at end of file + self._list_resources() + + @validation.required_services(consts.Service.CEILOMETER) + @validation.required_openstack(users=True) + @base.scenario() + def get_tenant_resources(self): + """Get all tenant resources. + + This scenario retrieves information about tenant resources using + GET /v2/resources/(resource_id) + """ + resources = self.context["tenant"].get("resources", []) + if not resources: + msg = ("No resources found for tenant: %s" + % self.context["tenant"].get("name")) + raise exceptions.NotFoundException(message=msg) + for res_id in resources: + self._get_resource(res_id) \ No newline at end of file diff --git a/rally/benchmark/scenarios/ceilometer/utils.py b/rally/benchmark/scenarios/ceilometer/utils.py index 1a865c95ab..4b68221334 100644 --- a/rally/benchmark/scenarios/ceilometer/utils.py +++ b/rally/benchmark/scenarios/ceilometer/utils.py @@ -138,6 +138,11 @@ class CeilometerScenario(base.Scenario): """ return self.clients("ceilometer").samples.list() + @base.atomic_action_timer("ceilometer.get_resource") + def _get_resource(self, resource_id): + """Retrieve details about one resource.""" + return self.clients("ceilometer").resources.get(resource_id) + @base.atomic_action_timer("ceilometer.get_stats") def _get_stats(self, meter_name): """Get stats for a specific meter. @@ -191,7 +196,7 @@ class CeilometerScenario(base.Scenario): @base.atomic_action_timer("ceilometer.create_sample") def _create_sample(self, counter_name, counter_type, counter_unit, - counter_volume, resource_id, **kwargs): + counter_volume, resource_id=None, **kwargs): """Create a Sample with specified parameters. :param counter_name: specifies name of the counter @@ -206,7 +211,9 @@ class CeilometerScenario(base.Scenario): "counter_type": counter_type, "counter_unit": counter_unit, "counter_volume": counter_volume, - "resource_id": resource_id}) + "resource_id": resource_id if resource_id + else self._generate_random_name( + prefix="rally_ctx_resource_")}) return self.clients("ceilometer").samples.create(**kwargs) @base.atomic_action_timer("ceilometer.query_samples") diff --git a/samples/tasks/scenarios/ceilometer/get-tenant-resources.json b/samples/tasks/scenarios/ceilometer/get-tenant-resources.json new file mode 100644 index 0000000000..a1e7467ad4 --- /dev/null +++ b/samples/tasks/scenarios/ceilometer/get-tenant-resources.json @@ -0,0 +1,23 @@ +{ + "CeilometerResource.get_tenant_resources": [ + { + "runner": { + "type": "constant", + "times": 10, + "concurrency": 5 + }, + "context": { + "users": { + "tenants": 2, + "users_per_tenant": 2 + }, + "ceilometer": { + "counter_name": "cpu_util", + "counter_type": "gauge", + "counter_unit": "instance", + "counter_volume": 1.0 + } + } + } + ] +} \ No newline at end of file diff --git a/samples/tasks/scenarios/ceilometer/get-tenant-resources.yaml b/samples/tasks/scenarios/ceilometer/get-tenant-resources.yaml new file mode 100644 index 0000000000..a58d3c8f52 --- /dev/null +++ b/samples/tasks/scenarios/ceilometer/get-tenant-resources.yaml @@ -0,0 +1,16 @@ +--- + CeilometerResource.get_tenant_resources: + - + runner: + type: "constant" + times: 10 + concurrency: 5 + context: + users: + tenants: 2 + users_per_tenant: 2 + ceilometer: + counter_name: "cpu_util" + counter_type: "gauge" + counter_volume: 1.0 + counter_unit: "instance" \ No newline at end of file diff --git a/samples/tasks/scenarios/ceilometer/list-samples.json b/samples/tasks/scenarios/ceilometer/list-samples.json index 393c8d3699..1b4b397335 100644 --- a/samples/tasks/scenarios/ceilometer/list-samples.json +++ b/samples/tasks/scenarios/ceilometer/list-samples.json @@ -10,6 +10,14 @@ "users": { "tenants": 2, "users_per_tenant": 2 + }, + "ceilometer": { + "counter_name": "cpu_util", + "counter_type": "gauge", + "counter_unit": "instance", + "counter_volume": 1.0, + "resources_per_tenant": 3, + "samples_per_resource": 3 } } } diff --git a/samples/tasks/scenarios/ceilometer/list-samples.yaml b/samples/tasks/scenarios/ceilometer/list-samples.yaml index 7c8e053c37..6c4547b9f2 100644 --- a/samples/tasks/scenarios/ceilometer/list-samples.yaml +++ b/samples/tasks/scenarios/ceilometer/list-samples.yaml @@ -9,3 +9,10 @@ users: tenants: 2 users_per_tenant: 2 + ceilometer: + counter_name: "cpu_util" + counter_type: "gauge" + counter_unit: "instance" + counter_volume: 1.0 + resources_per_tenant: 3 + samples_per_resource: 3 diff --git a/tests/unit/benchmark/context/test_ceilometer.py b/tests/unit/benchmark/context/test_ceilometer.py new file mode 100644 index 0000000000..6fae0485a2 --- /dev/null +++ b/tests/unit/benchmark/context/test_ceilometer.py @@ -0,0 +1,123 @@ +# 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 copy + +import mock + +from rally.benchmark.context import ceilometer +from tests.unit import fakes +from tests.unit import test + +CTX = "rally.benchmark.context" +SCN = "rally.benchmark.scenarios" + + +class CeilometerSampleGeneratorTestCase(test.TestCase): + + def _gen_tenants(self, count): + tenants = {} + for id in range(count): + tenants[str(id)] = dict(name=str(id)) + return tenants + + def _gen_context(self, tenants_count, users_per_tenant, + resources_per_tenant, samples_per_resource): + tenants = self._gen_tenants(tenants_count) + users = [] + for id_ in tenants.keys(): + for i in range(users_per_tenant): + users.append({"id": i, "tenant_id": id_, + "endpoint": "endpoint"}) + context = { + "config": { + "users": { + "tenants": tenants_count, + "users_per_tenant": users_per_tenant, + "concurrent": 10, + }, + "ceilometer": { + "counter_name": "fake-counter-name", + "counter_type": "fake-counter-type", + "counter_unit": "fake-counter-unit", + "counter_volume": 100, + "resources_per_tenant": resources_per_tenant, + "samples_per_resource": samples_per_resource + } + }, + "admin": { + "endpoint": mock.MagicMock() + }, + "task": mock.MagicMock(), + "users": users, + "tenants": tenants + } + return tenants, context + + def test_init(self): + context = {} + context["task"] = mock.MagicMock() + context["config"] = { + "ceilometer": { + "counter_name": "cpu_util", + "counter_type": "gauge", + "counter_unit": "instance", + "counter_volume": 1.0, + "resources_per_tenant": 5, + "samples_per_resource": 5 + } + } + + inst = ceilometer.CeilometerSampleGenerator(context) + self.assertEqual(inst.config, context["config"]["ceilometer"]) + + @mock.patch("%s.ceilometer.osclients" % CTX) + def test_setup(self, mock_osclients): + fc = fakes.FakeClients() + mock_osclients.Clients.return_value = fc + + tenants_count = 2 + users_per_tenant = 5 + resources_per_tenant = 5 + samples_per_resource = 5 + + tenants, real_context = self._gen_context( + tenants_count, users_per_tenant, + resources_per_tenant, samples_per_resource) + + new_context = copy.deepcopy(real_context) + for id_ in tenants.keys(): + new_context["tenants"][id_].setdefault("samples", list()) + new_context["tenants"][id_].setdefault("resources", list()) + for i in range(resources_per_tenant): + for j in range(samples_per_resource): + new_context["tenants"][id_]["samples"].append( + {"counter_name": "fake-counter-name", + "counter_type": "fake-counter-type", + "counter_unit": "fake-counter-unit", + "counter_volume": 100, + "resource_id": "fake-resource-id"}) + new_context["tenants"][id_]["resources"].append( + "fake-resource-id") + + ceilometer_ctx = ceilometer.CeilometerSampleGenerator(real_context) + ceilometer_ctx.setup() + self.assertEqual(new_context, ceilometer_ctx.context) + + @mock.patch("%s.ceilometer.osclients" % CTX) + def test_cleanup(self, mock_osclients): + tenants, context = self._gen_context(2, 5, 3, 3) + ceilometer_ctx = ceilometer.CeilometerSampleGenerator(context) + ceilometer_ctx.cleanup() \ No newline at end of file diff --git a/tests/unit/benchmark/scenarios/ceilometer/test_resources.py b/tests/unit/benchmark/scenarios/ceilometer/test_resources.py index 63c169139b..a20a71929a 100644 --- a/tests/unit/benchmark/scenarios/ceilometer/test_resources.py +++ b/tests/unit/benchmark/scenarios/ceilometer/test_resources.py @@ -15,6 +15,7 @@ import mock from rally.benchmark.scenarios.ceilometer import resources +from rally import exceptions from tests.unit import test @@ -23,5 +24,24 @@ class CeilometerResourcesTestCase(test.TestCase): scenario = resources.CeilometerResource() scenario._list_resources = mock.MagicMock() scenario.list_resources() + scenario._list_resources.assert_called_once_with() - scenario._list_resources.assert_called_once_with() \ No newline at end of file + def test_get_tenant_resources(self): + scenario = resources.CeilometerResource() + resource_list = ["id1", "id2", "id3", "id4"] + context = {"user": {"tenant_id": "fake"}, + "tenant": {"id": "fake", "resources": resource_list}} + scenario.context = context + scenario._get_resource = mock.MagicMock() + scenario.get_tenant_resources() + for resource_id in resource_list: + scenario._get_resource.assert_any_call(resource_id) + + def test_get_tenant_resources_with_exception(self): + scenario = resources.CeilometerResource() + resource_list = [] + context = {"user": {"tenant_id": "fake"}, + "tenant": {"id": "fake", "resources": resource_list}} + scenario.context = context + self.assertRaises(exceptions.NotFoundException, + scenario.get_tenant_resources) \ No newline at end of file diff --git a/tests/unit/benchmark/scenarios/ceilometer/test_samples.py b/tests/unit/benchmark/scenarios/ceilometer/test_samples.py index 9e26986413..6fe3f61ea6 100644 --- a/tests/unit/benchmark/scenarios/ceilometer/test_samples.py +++ b/tests/unit/benchmark/scenarios/ceilometer/test_samples.py @@ -23,5 +23,4 @@ class CeilometerSamplesTestCase(test.TestCase): scenario = samples.CeilometerSamples() scenario._list_samples = mock.MagicMock() scenario.list_samples() - scenario._list_samples.assert_called_once_with() diff --git a/tests/unit/benchmark/scenarios/ceilometer/test_utils.py b/tests/unit/benchmark/scenarios/ceilometer/test_utils.py index b01f276e7d..65c0e22b47 100644 --- a/tests/unit/benchmark/scenarios/ceilometer/test_utils.py +++ b/tests/unit/benchmark/scenarios/ceilometer/test_utils.py @@ -122,6 +122,10 @@ class CeilometerScenarioTestCase(test.TestCase): fake_samples = self.scenario._list_samples() self.assertEqual(fake_samples, ["fake-samples"]) + def test__get_resource(self): + fake_resource_info = self.scenario._get_resource("fake-resource-id") + self.assertEqual(fake_resource_info, ["fake-resource-info"]) + def test__get_stats(self): """Test _get_stats function.""" fake_statistics = self.scenario._get_stats("fake-meter") diff --git a/tests/unit/fakes.py b/tests/unit/fakes.py index fbd8016a9a..230647606a 100644 --- a/tests/unit/fakes.py +++ b/tests/unit/fakes.py @@ -219,6 +219,7 @@ class FakeAlarm(FakeResource): self.threshold = kwargs.get("threshold") self.state = kwargs.get("state", "fake-alarm-state") self.alarm_id = kwargs.get("alarm_id", "fake-alarm-id") + self.state = kwargs.get("state", "ok") self.optional_args = kwargs.get("optional_args", {}) @@ -229,7 +230,17 @@ class FakeSample(FakeResource): self.counter_type = kwargs.get("counter_type", "fake-counter-type") self.counter_unit = kwargs.get("counter_unit", "fake-counter-unit") self.counter_volume = kwargs.get("counter_volume", 100) - self.resource_id = kwargs.get("resource_id", "fake-resource-id") + + @property + def resource_id(self): + return "fake-resource-id" + + def to_dict(self): + return {"counter_name": self.counter_name, + "counter_type": self.counter_type, + "counter_unit": self.counter_unit, + "counter_volume": self.counter_volume, + "resource_id": self.resource_id} class FakeVolume(FakeResource): @@ -775,6 +786,9 @@ class FakeMeterManager(FakeManager): class FakeCeilometerResourceManager(FakeManager): + def get(self, resource_id): + return ["fake-resource-info"] + def list(self): return ["fake-resource"]