From 5951c7e043726250dafa0e9ef4cc3cbcc0007603 Mon Sep 17 00:00:00 2001 From: Marco Morais Date: Thu, 8 May 2014 08:45:11 +0000 Subject: [PATCH] Support scenarios for updating neutron quotas * Add new scenario Quotas.neutron_update to exercise updating quotas for neutron * Modify _update_quotas to be able to support clients which update quotas with an api different from nova and cinder * Add test for ZaqarQueues list() Co-Authored-By: Roman Vasilets Co-Authored-By: Marco Morais Change-Id: Ida19c02f831e1846bb96a239f4a2deb5ae417153 --- rally-jobs/rally-neutron.yaml | 16 ++++++++ rally/benchmark/context/cleanup/resources.py | 22 ++++++++--- rally/benchmark/scenarios/quotas/quotas.py | 32 +++++++++++----- rally/benchmark/scenarios/quotas/utils.py | 17 ++++++++- rally/verification/tempest/tempest.py | 2 +- .../scenarios/quotas/neutron-update.json | 20 ++++++++++ .../scenarios/quotas/neutron-update.yaml | 13 +++++++ .../context/cleanup/test_resources.py | 24 ++++++++++++ .../benchmark/context/quotas/test_quotas.py | 22 +++++++++-- .../benchmark/scenarios/nova/test_servers.py | 2 +- .../benchmark/scenarios/nova/test_utils.py | 14 +++---- .../benchmark/scenarios/quotas/test_quotas.py | 26 +++++++++++-- .../benchmark/scenarios/quotas/test_utils.py | 37 +++++++++++++++++++ 13 files changed, 212 insertions(+), 35 deletions(-) create mode 100644 samples/tasks/scenarios/quotas/neutron-update.json create mode 100644 samples/tasks/scenarios/quotas/neutron-update.yaml diff --git a/rally-jobs/rally-neutron.yaml b/rally-jobs/rally-neutron.yaml index 2f8e7fd721..18531af2aa 100644 --- a/rally-jobs/rally-neutron.yaml +++ b/rally-jobs/rally-neutron.yaml @@ -297,6 +297,22 @@ failure_rate: max: 0 + Quotas.neutron_update: + - + args: + max_quota: 1024 + runner: + type: "constant" + times: 10 + concurrency: 2 + context: + users: + tenants: 3 + users_per_tenant: 2 + sla: + failure_rate: + max: 0 + NovaServers.boot_server: - args: diff --git a/rally/benchmark/context/cleanup/resources.py b/rally/benchmark/context/cleanup/resources.py index 1cd0fe3591..c88ab58ecf 100644 --- a/rally/benchmark/context/cleanup/resources.py +++ b/rally/benchmark/context/cleanup/resources.py @@ -87,11 +87,13 @@ class NovaQuotas(QuotaMixin, base.ResourceManager): _neutron_order = get_order(300) -class NeutronMixin(SynchronizedDeletion): +@base.resource(service=None, resource=None, admin_required=True) +class NeutronMixin(SynchronizedDeletion, base.ResourceManager): # Neutron has the best client ever, so we need to override everything def _manager(self): - return getattr(self.user, self._service)() + client = self._admin_required and self.admin or self.user + return getattr(client, self._service)() def id(self): return self.raw_resource["id"] @@ -110,7 +112,7 @@ class NeutronMixin(SynchronizedDeletion): @base.resource("neutron", "port", order=next(_neutron_order), tenant_resource=True) -class NeutronPort(NeutronMixin, base.ResourceManager): +class NeutronPort(NeutronMixin): def delete(self): if self.raw_resource["device_owner"] == "network:router_interface": @@ -129,22 +131,30 @@ class NeutronPort(NeutronMixin, base.ResourceManager): @base.resource("neutron", "router", order=next(_neutron_order), tenant_resource=True) -class NeutronRouter(NeutronMixin, base.ResourceManager): +class NeutronRouter(NeutronMixin): pass @base.resource("neutron", "subnet", order=next(_neutron_order), tenant_resource=True) -class NeutronSubnet(NeutronMixin, base.ResourceManager): +class NeutronSubnet(NeutronMixin): pass @base.resource("neutron", "network", order=next(_neutron_order), tenant_resource=True) -class NeutronNetwork(NeutronMixin, base.ResourceManager): +class NeutronNetwork(NeutronMixin): pass +@base.resource("neutron", "quota", order=next(_neutron_order), + admin_required=True, tenant_resource=True) +class NeutronQuota(QuotaMixin, NeutronMixin): + + def delete(self): + self._manager().delete_quota(self.tenant_uuid) + + # CINDER _cinder_order = get_order(400) diff --git a/rally/benchmark/scenarios/quotas/quotas.py b/rally/benchmark/scenarios/quotas/quotas.py index 935e3da247..88a932bbe0 100644 --- a/rally/benchmark/scenarios/quotas/quotas.py +++ b/rally/benchmark/scenarios/quotas/quotas.py @@ -30,8 +30,8 @@ class Quotas(utils.QuotasScenario): :param max_quota: Max value to be updated for quota. """ - tenant_id = self.context["user"]["tenant_id"] - self._update_quotas("nova", tenant_id, max_quota) + self._update_quotas("nova", self.context["tenant"]["id"], + max_quota) @validation.required_services(consts.Service.NOVA) @validation.required_openstack(admin=True, users=True) @@ -42,9 +42,9 @@ class Quotas(utils.QuotasScenario): :param max_quota: Max value to be updated for quota. """ - tenant_id = self.context["user"]["tenant_id"] - self._update_quotas("nova", tenant_id, max_quota) - self._delete_quotas("nova", tenant_id) + self._update_quotas("nova", self.context["tenant"]["id"], + max_quota) + self._delete_quotas("nova", self.context["tenant"]["id"]) @validation.required_services(consts.Service.CINDER) @validation.required_openstack(admin=True, users=True) @@ -54,8 +54,8 @@ class Quotas(utils.QuotasScenario): :param max_quota: Max value to be updated for quota. """ - tenant_id = self.context["user"]["tenant_id"] - self._update_quotas("cinder", tenant_id, max_quota) + self._update_quotas("cinder", self.context["tenant"]["id"], + max_quota) @validation.required_services(consts.Service.CINDER) @validation.required_openstack(admin=True, users=True) @@ -65,6 +65,18 @@ class Quotas(utils.QuotasScenario): :param max_quota: Max value to be updated for quota. """ - tenant_id = self.context["user"]["tenant_id"] - self._update_quotas("cinder", tenant_id, max_quota) - self._delete_quotas("cinder", tenant_id) + self._update_quotas("cinder", self.context["tenant"]["id"], + max_quota) + self._delete_quotas("cinder", self.context["tenant"]["id"]) + + @validation.required_services(consts.Service.NEUTRON) + @validation.required_openstack(admin=True, users=True) + @base.scenario(context={"admin_cleanup": ["neutron.quota"]}) + def neutron_update(self, max_quota=1024): + """Update quotas for neutron. + + :param max_quota: Max value to be updated for quota. + """ + quota_update_fn = self.admin_clients("neutron").update_quota + self._update_quotas("neutron", self.context["tenant"]["id"], + max_quota, quota_update_fn) diff --git a/rally/benchmark/scenarios/quotas/utils.py b/rally/benchmark/scenarios/quotas/utils.py index 74e9932cd7..08dd7bfa40 100644 --- a/rally/benchmark/scenarios/quotas/utils.py +++ b/rally/benchmark/scenarios/quotas/utils.py @@ -22,16 +22,23 @@ class QuotasScenario(base.Scenario): """Base class for quotas scenarios with basic atomic actions.""" @base.atomic_action_timer("quotas.update_quotas") - def _update_quotas(self, component, tenant_id, max_quota=1024): - """Update quotas. + def _update_quotas(self, component, tenant_id, max_quota=1024, + quota_update_fn=None): + """Updates quotas. :param component: Component for the quotas. :param tenant_id: The project_id for the quotas to be updated. :param max_quota: Max value to be updated for quota. + :param quota_update_fn: Client quota update function. + + Standard OpenStack clients use quotas.update(). + Use `quota_update_fn` to override for non-standard clients. :returns: Updated quotas dictionary. """ quotas = self._generate_quota_values(max_quota, component) + if quota_update_fn: + return quota_update_fn(tenant_id, **quotas) return self.admin_clients(component).quotas.update(tenant_id, **quotas) @base.atomic_action_timer("quotas.delete_quotas") @@ -62,4 +69,10 @@ class QuotasScenario(base.Scenario): "snapshots": random.randint(-1, max_quota), "gigabytes": random.randint(-1, max_quota), } + elif component == "neutron": + quota = {} + for key in ["network", "subnet", "port", "router", "floatingip", + "security_group", "security_group_rule"]: + quota[key] = random.randint(-1, max_quota) + quotas = {"body": {"quota": quota}} return quotas diff --git a/rally/verification/tempest/tempest.py b/rally/verification/tempest/tempest.py index e338234e61..fdd9b11d01 100644 --- a/rally/verification/tempest/tempest.py +++ b/rally/verification/tempest/tempest.py @@ -92,7 +92,7 @@ class Tempest(object): @staticmethod def _is_git_repo(directory): # will suppress git output - with open(os.devnull, 'w') as devnull: + with open(os.devnull, "w") as devnull: return os.path.isdir(directory) and not subprocess.call( "git status", shell=True, stdout=devnull, stderr=subprocess.STDOUT, diff --git a/samples/tasks/scenarios/quotas/neutron-update.json b/samples/tasks/scenarios/quotas/neutron-update.json new file mode 100644 index 0000000000..8a0b433855 --- /dev/null +++ b/samples/tasks/scenarios/quotas/neutron-update.json @@ -0,0 +1,20 @@ +{ + "Quotas.neutron_update": [ + { + "args": { + "max_quota": 1024 + }, + "runner": { + "type": "constant", + "times": 10, + "concurrency": 2 + }, + "context": { + "users": { + "tenants": 3, + "users_per_tenant": 2 + } + } + } + ] +} diff --git a/samples/tasks/scenarios/quotas/neutron-update.yaml b/samples/tasks/scenarios/quotas/neutron-update.yaml new file mode 100644 index 0000000000..0650dc5aef --- /dev/null +++ b/samples/tasks/scenarios/quotas/neutron-update.yaml @@ -0,0 +1,13 @@ +--- + Quotas.neutron_update: + - + args: + max_quota: 1024 + runner: + type: "constant" + times: 10 + concurrency: 2 + context: + users: + tenants: 3 + users_per_tenant: 2 diff --git a/tests/unit/benchmark/context/cleanup/test_resources.py b/tests/unit/benchmark/context/cleanup/test_resources.py index 06514ff285..d3511cf3e6 100644 --- a/tests/unit/benchmark/context/cleanup/test_resources.py +++ b/tests/unit/benchmark/context/cleanup/test_resources.py @@ -172,6 +172,21 @@ class NeutronPortTestCase(test.TestCase): raw_res["device_id"], {"port_id": raw_res["id"]}) +class NeutronQuotaTestCase(test.TestCase): + + @mock.patch("%s.NeutronQuota._manager" % BASE) + def test_delete(self, mock_manager): + user = mock.MagicMock() + resources.NeutronQuota(user=user, tenant_uuid="fake").delete() + mock_manager().delete_quota.assert_called_once_with("fake") + + def test__manager(self): + admin = mock.MagicMock(neutron=mock.Mock(return_value="foo")) + res = resources.NeutronQuota(admin=admin, tenant_uuid="fake") + res._manager() + self.assertEqual("foo", getattr(admin, res._service)()) + + class GlanceImageTestCase(test.TestCase): @mock.patch("%s.GlanceImage._manager" % BASE) @@ -205,6 +220,15 @@ class CeilometerTestCase(test.TestCase): q=[{"field": "project_id", "op": "eq", "value": ceil.tenant_uuid}]) +class ZaqarQueuesTestCase(test.TestCase): + + def test_list(self): + user = mock.Mock() + zaqar = resources.ZaqarQueues(user=user) + zaqar.list() + user.zaqar().queues.assert_called_once_with() + + class KeystoneMixinTestCase(test.TestCase): def test_is_deleted(self): diff --git a/tests/unit/benchmark/context/quotas/test_quotas.py b/tests/unit/benchmark/context/quotas/test_quotas.py index 86a774eb05..64823e8675 100644 --- a/tests/unit/benchmark/context/quotas/test_quotas.py +++ b/tests/unit/benchmark/context/quotas/test_quotas.py @@ -235,7 +235,7 @@ class QuotasTestCase(test.TestCase): @mock.patch("rally.benchmark.context.quotas.nova_quotas.NovaQuotas") @mock.patch("rally.benchmark.context.quotas.cinder_quotas.CinderQuotas") @mock.patch("rally.benchmark.context.quotas.neutron_quotas.NeutronQuotas") - def test_no_quotas(self, mock_neutron_quotas, mock_cinder_quotas, + def test_no_quotas(self, mock_neutron_quota, mock_cinder_quotas, mock_nova_quotas, mock_osclients): ctx = copy.deepcopy(self.context) if "quotas" in ctx["config"]: @@ -245,14 +245,14 @@ class QuotasTestCase(test.TestCase): quotas_ctx.setup() self.assertFalse(mock_cinder_quotas.update.called) self.assertFalse(mock_nova_quotas.update.called) - self.assertFalse(mock_neutron_quotas.update.called) + self.assertFalse(mock_neutron_quota.update.called) self.assertFalse(mock_cinder_quotas.delete.called) self.assertFalse(mock_nova_quotas.delete.called) - self.assertFalse(mock_neutron_quotas.delete.called) + self.assertFalse(mock_neutron_quota.delete.called) @mock.patch("rally.benchmark.context.quotas.nova_quotas.NovaQuotas") - def test_exception_during_cleanup(self, mock_nova_quotas): + def test_exception_during_cleanup_nova(self, mock_nova_quotas): mock_nova_quotas.delete.side_effect = Exception("boom") @@ -264,3 +264,17 @@ class QuotasTestCase(test.TestCase): self.assertEqual(mock_nova_quotas().delete.call_count, len(self.context["tenants"])) + + @mock.patch("rally.benchmark.context.quotas.neutron_quotas.NeutronQuotas") + def test_exception_during_cleanup_neutron(self, mock_neutron_quota): + + mock_neutron_quota.delete.side_effect = Exception("boom") + + ctx = copy.deepcopy(self.context) + ctx["config"]["quotas"] = {"neutron": {"cpu": 1}} + + # NOTE(boris-42): ensure that cleanup didn't raise exceptions. + quotas.Quotas(ctx).cleanup() + + self.assertEqual(mock_neutron_quota().delete.call_count, + len(self.context["tenants"])) diff --git a/tests/unit/benchmark/scenarios/nova/test_servers.py b/tests/unit/benchmark/scenarios/nova/test_servers.py index 109687d202..f3d7dca639 100644 --- a/tests/unit/benchmark/scenarios/nova/test_servers.py +++ b/tests/unit/benchmark/scenarios/nova/test_servers.py @@ -217,7 +217,7 @@ class NovaServersTestCase(test.TestCase): scenario._create_volume.assert_called_once_with(5, imageRef="img") scenario._boot_server.assert_called_once_with( "img", 0, - block_device_mapping={'vda': 'volume_id:::1'}, + block_device_mapping={"vda": "volume_id:::1"}, fakearg="f") scenario.sleep_between.assert_called_once_with(10, 20) scenario._delete_server.assert_called_once_with(fake_server, diff --git a/tests/unit/benchmark/scenarios/nova/test_utils.py b/tests/unit/benchmark/scenarios/nova/test_utils.py index 38f9f6899d..594096e164 100644 --- a/tests/unit/benchmark/scenarios/nova/test_utils.py +++ b/tests/unit/benchmark/scenarios/nova/test_utils.py @@ -99,8 +99,8 @@ class NovaScenarioTestCase(test.TestCase): def test__boot_server(self, mock_clients): mock_clients("nova").servers.create.return_value = self.server nova_scenario = utils.NovaScenario(context={}) - return_server = nova_scenario._boot_server('image_id', - 'flavor_id') + return_server = nova_scenario._boot_server("image_id", + "flavor_id") self._test_assert_called_once_with( self.wait_for.mock, self.server, CONF.benchmark.nova_server_boot_poll_interval, @@ -145,7 +145,7 @@ class NovaScenarioTestCase(test.TestCase): def test__boot_server_with_ssh(self, mock_clients): mock_clients("nova").servers.create.return_value = self.server nova_scenario = utils.NovaScenario(context={"allow_ssh": "test"}) - return_server = nova_scenario._boot_server('image_id', 'flavor_id') + return_server = nova_scenario._boot_server("image_id", "flavor_id") self._test_assert_called_once_with( self.wait_for.mock, self.server, CONF.benchmark.nova_server_boot_poll_interval, @@ -160,8 +160,8 @@ class NovaScenarioTestCase(test.TestCase): mock_clients("nova").servers.create.return_value = self.server nova_scenario = utils.NovaScenario(context={"allow_ssh": "new"}) return_server = nova_scenario._boot_server( - 'image_id', 'flavor_id', - security_groups=['test1']) + "image_id", "flavor_id", + security_groups=["test1"]) self._test_assert_called_once_with( self.wait_for.mock, self.server, CONF.benchmark.nova_server_boot_poll_interval, @@ -176,8 +176,8 @@ class NovaScenarioTestCase(test.TestCase): mock_clients("nova").servers.create.return_value = self.server nova_scenario = utils.NovaScenario(context={"allow_ssh": "test1"}) return_server = nova_scenario._boot_server( - 'image_id', 'flavor_id', - security_groups=['test1']) + "image_id", "flavor_id", + security_groups=["test1"]) self._test_assert_called_once_with( self.wait_for.mock, self.server, CONF.benchmark.nova_server_boot_poll_interval, diff --git a/tests/unit/benchmark/scenarios/quotas/test_quotas.py b/tests/unit/benchmark/scenarios/quotas/test_quotas.py index 9d318fffd2..05f9490975 100644 --- a/tests/unit/benchmark/scenarios/quotas/test_quotas.py +++ b/tests/unit/benchmark/scenarios/quotas/test_quotas.py @@ -21,14 +21,21 @@ from tests.unit import test class QuotasTestCase(test.TestCase): + def setUp(self): + super(QuotasTestCase, self).setUp() + self.context = { + "user": {"tenant_id": "fake"}, + "tenant": {"id": "fake"} + } + def test_nova_update(self): - scenario = quotas.Quotas(context={"user": {"tenant_id": "fake"}}) + scenario = quotas.Quotas(self.context) scenario._update_quotas = mock.MagicMock() scenario.nova_update(max_quota=1024) scenario._update_quotas.assert_called_once_with("nova", "fake", 1024) def test_nova_update_and_delete(self): - scenario = quotas.Quotas(context={"user": {"tenant_id": "fake"}}) + scenario = quotas.Quotas(self.context) scenario._update_quotas = mock.MagicMock() scenario._delete_quotas = mock.MagicMock() scenario.nova_update_and_delete(max_quota=1024) @@ -36,15 +43,26 @@ class QuotasTestCase(test.TestCase): scenario._delete_quotas.assert_called_once_with("nova", "fake") def test_cinder_update(self): - scenario = quotas.Quotas(context={"user": {"tenant_id": "fake"}}) + scenario = quotas.Quotas(self.context) scenario._update_quotas = mock.MagicMock() scenario.cinder_update(max_quota=1024) scenario._update_quotas.assert_called_once_with("cinder", "fake", 1024) def test_cinder_update_and_delete(self): - scenario = quotas.Quotas(context={"user": {"tenant_id": "fake"}}) + scenario = quotas.Quotas(self.context) scenario._update_quotas = mock.MagicMock() scenario._delete_quotas = mock.MagicMock() scenario.cinder_update_and_delete(max_quota=1024) scenario._update_quotas.assert_called_once_with("cinder", "fake", 1024) scenario._delete_quotas.assert_called_once_with("cinder", "fake") + + @mock.patch("rally.benchmark.scenarios.base.Scenario.admin_clients") + def test_neutron_update(self, mock_clients): + scenario = quotas.Quotas(self.context) + + scenario._update_quotas = mock.MagicMock() + mock_quota_update_fn = mock_clients().update_quota + scenario.neutron_update(max_quota=1024) + scenario._update_quotas.assert_called_once_with("neutron", "fake", + 1024, + mock_quota_update_fn) diff --git a/tests/unit/benchmark/scenarios/quotas/test_utils.py b/tests/unit/benchmark/scenarios/quotas/test_utils.py index 6daaac32c6..ee067f9974 100644 --- a/tests/unit/benchmark/scenarios/quotas/test_utils.py +++ b/tests/unit/benchmark/scenarios/quotas/test_utils.py @@ -52,6 +52,34 @@ class QuotasScenarioTestCase(test.TestCase): self._test_atomic_action_timer(scenario.atomic_actions(), "quotas.update_quotas") + def test__update_quotas_fn(self): + tenant_id = "fake_tenant" + quotas = { + "metadata_items": 10, + "key_pairs": 10, + "injected_file_content_bytes": 1024, + "injected_file_path_bytes": 1024, + "ram": 5120, + "instances": 10, + "injected_files": 10, + "cores": 10, + } + fake_nova = fakes.FakeNovaClient() + fake_nova.quotas.update = mock.MagicMock(return_value=quotas) + fake_clients = fakes.FakeClients() + fake_clients._nova = fake_nova + scenario = utils.QuotasScenario(admin_clients=fake_clients) + scenario._generate_quota_values = mock.MagicMock(return_value=quotas) + + mock_quota = mock.Mock(return_value=quotas) + + result = scenario._update_quotas("nova", tenant_id, + quota_update_fn=mock_quota) + + self.assertEqual(quotas, result) + self._test_atomic_action_timer(scenario.atomic_actions(), + "quotas.update_quotas") + def test__generate_quota_values_nova(self): max_quota = 1024 scenario = utils.QuotasScenario(admin_clients=fakes.FakeClients()) @@ -66,6 +94,15 @@ class QuotasScenarioTestCase(test.TestCase): for k, v in six.iteritems(quotas): self.assertTrue(-1 <= v <= max_quota) + def test__generate_quota_values_neutron(self): + max_quota = 1024 + scenario = utils.QuotasScenario(admin_clients=fakes.FakeClients()) + quotas = scenario._generate_quota_values(max_quota, "neutron") + for v in six.itervalues(quotas): + for v1 in six.itervalues(v): + for v2 in six.itervalues(v1): + self.assertTrue(-1 <= v2 <= max_quota) + def test__delete_quotas(self): tenant_id = "fake_tenant" fake_nova = fakes.FakeNovaClient()