diff --git a/rally-jobs/rally-designate.yaml b/rally-jobs/rally-designate.yaml index eab18ad378..fba7f1fc2d 100644 --- a/rally-jobs/rally-designate.yaml +++ b/rally-jobs/rally-designate.yaml @@ -134,7 +134,7 @@ runner: type: "constant" times: 10 - concurrency: 1 + concurrency: 2 context: users: tenants: 2 @@ -157,6 +157,38 @@ failure_rate: max: 0 + DesignateBasic.create_and_list_recordsets: + - + runner: + type: "constant" + times: 10 + concurrency: 2 + context: + users: + tenants: 2 + users_per_tenant: 2 + zones: + zones_per_tenant: 1 + sla: + failure_rate: + max: 0 + + DesignateBasic.create_and_delete_recordsets: + - + runner: + type: "constant" + times: 10 + concurrency: 2 + context: + users: + tenants: 2 + users_per_tenant: 2 + zones: + zones_per_tenant: 1 + sla: + failure_rate: + max: 0 + DesignateBasic.list_zones: - runner: diff --git a/rally/plugins/openstack/cleanup/resources.py b/rally/plugins/openstack/cleanup/resources.py index b05665ffa3..470cd8efc9 100644 --- a/rally/plugins/openstack/cleanup/resources.py +++ b/rally/plugins/openstack/cleanup/resources.py @@ -470,28 +470,72 @@ _designate_order = get_order(900) class DesignateResource(SynchronizedDeletion, base.ResourceManager): def _manager(self): - # NOTE: service name contains version, so we should split them - service_name, version = self._service.split("_v") - return getattr(getattr(self.user, service_name)(version), + # Map resource names to api / client version + resource_versions = { + "domains": "1", + "servers": "1", + "recordsets": 2, + "zones": "2" + } + version = resource_versions[self._resource] + return getattr(getattr(self.user, self._service)(version), self._resource) + def _walk_pages(self, func, *args, **kwargs): + """Generator that keeps fetching pages until there's none left.""" + marker = None -@base.resource("designate_v1", "domains", order=next(_designate_order)) + while True: + items = func(marker=marker, limit=100, *args, **kwargs) + if not items: + break + for item in items: + yield item + marker = items[-1]["id"] + + +@base.resource("designate", "domains", order=next(_designate_order)) class DesignateDomain(DesignateResource): pass -@base.resource("designate_v2", "zones", order=next(_designate_order)) -class DesignateZones(DesignateResource): - pass - - -@base.resource("designate_v1", "servers", order=next(_designate_order), +@base.resource("designate", "servers", order=next(_designate_order), admin_required=True, perform_for_admin_only=True) class DesignateServer(DesignateResource): pass +@base.resource("designate", "recordsets", order=next(_designate_order), + tenant_resource=True) +class DesignateRecordSets(DesignateResource): + def _client(self): + # Map resource names to api / client version + resource_versions = { + "domains": "1", + "servers": "1", + "recordsets": 2, + "zones": "2" + } + version = resource_versions[self._resource] + return getattr(self.user, self._service)(version) + + def list(self): + criterion = {"name": "s_rally_*"} + for zone in self._walk_pages(self._client().zones.list, + criterion=criterion): + for recordset in self._walk_pages(self._client().recordsets.list, + zone["id"]): + yield recordset + + +@base.resource("designate", "zones", order=next(_designate_order), + tenant_resource=True) +class DesignateZones(DesignateResource): + def list(self): + criterion = {"name": "s_rally_*"} + return self._walk_pages(self._manager().list, criterion=criterion) + + # SWIFT _swift_order = get_order(1000) diff --git a/rally/plugins/openstack/scenarios/designate/basic.py b/rally/plugins/openstack/scenarios/designate/basic.py index b57fde29d0..c628bfc269 100644 --- a/rally/plugins/openstack/scenarios/designate/basic.py +++ b/rally/plugins/openstack/scenarios/designate/basic.py @@ -13,6 +13,7 @@ # 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 random from rally import consts from rally.plugins.openstack import scenario @@ -26,7 +27,7 @@ class DesignateBasic(utils.DesignateScenario): @validation.required_services(consts.Service.DESIGNATE) @validation.required_openstack(users=True) - @scenario.configure(context={"cleanup": ["designate_v1"]}) + @scenario.configure(context={"cleanup": ["designate"]}) def create_and_list_domains(self): """Create a domain and list all domains. @@ -43,7 +44,7 @@ class DesignateBasic(utils.DesignateScenario): @validation.required_services(consts.Service.DESIGNATE) @validation.required_openstack(users=True) - @scenario.configure(context={"cleanup": ["designate_v1"]}) + @scenario.configure(context={"cleanup": ["designate"]}) def list_domains(self): """List Designate domains. @@ -59,7 +60,7 @@ class DesignateBasic(utils.DesignateScenario): @validation.required_services(consts.Service.DESIGNATE) @validation.required_openstack(users=True) - @scenario.configure(context={"cleanup": ["designate_v1"]}) + @scenario.configure(context={"cleanup": ["designate"]}) def create_and_delete_domain(self): """Create and then delete a domain. @@ -71,7 +72,7 @@ class DesignateBasic(utils.DesignateScenario): @validation.required_services(consts.Service.DESIGNATE) @validation.required_openstack(users=True) - @scenario.configure(context={"cleanup": ["designate_v1"]}) + @scenario.configure(context={"cleanup": ["designate"]}) def create_and_update_domain(self): """Create and then update a domain. @@ -83,7 +84,7 @@ class DesignateBasic(utils.DesignateScenario): @validation.required_services(consts.Service.DESIGNATE) @validation.required_openstack(users=True) - @scenario.configure(context={"cleanup": ["designate_v1"]}) + @scenario.configure(context={"cleanup": ["designate"]}) def create_and_delete_records(self, records_per_domain=5): """Create and then delete records. @@ -110,7 +111,7 @@ class DesignateBasic(utils.DesignateScenario): @validation.required_services(consts.Service.DESIGNATE) @validation.required_openstack(users=True) - @scenario.configure(context={"cleanup": ["designate_v1"]}) + @scenario.configure(context={"cleanup": ["designate"]}) def list_records(self, domain_id): """List Designate records. @@ -128,7 +129,7 @@ class DesignateBasic(utils.DesignateScenario): @validation.required_services(consts.Service.DESIGNATE) @validation.required_openstack(users=True) - @scenario.configure(context={"cleanup": ["designate_v1"]}) + @scenario.configure(context={"cleanup": ["designate"]}) def create_and_list_records(self, records_per_domain=5): """Create and then list records. @@ -151,7 +152,7 @@ class DesignateBasic(utils.DesignateScenario): @validation.required_services(consts.Service.DESIGNATE) @validation.required_openstack(admin=True) - @scenario.configure(context={"cleanup": ["designate_v1"]}) + @scenario.configure(context={"cleanup": ["designate"]}) def create_and_list_servers(self): """Create a Designate server and list all servers. @@ -166,7 +167,7 @@ class DesignateBasic(utils.DesignateScenario): @validation.required_services(consts.Service.DESIGNATE) @validation.required_openstack(admin=True) - @scenario.configure(context={"cleanup": ["designate_v1"]}) + @scenario.configure(context={"cleanup": ["designate"]}) def create_and_delete_server(self): """Create and then delete a server. @@ -178,7 +179,7 @@ class DesignateBasic(utils.DesignateScenario): @validation.required_services(consts.Service.DESIGNATE) @validation.required_openstack(admin=True) - @scenario.configure(context={"cleanup": ["designate_v1"]}) + @scenario.configure(context={"cleanup": ["designate"]}) def list_servers(self): """List Designate servers. @@ -190,7 +191,7 @@ class DesignateBasic(utils.DesignateScenario): # NOTE: API V2 @validation.required_services(consts.Service.DESIGNATE) @validation.required_openstack(users=True) - @scenario.configure(context={"cleanup": ["designate_v2"]}) + @scenario.configure(context={"cleanup": ["designate"]}) def create_and_list_zones(self): """Create a zone and list all zones. @@ -207,7 +208,7 @@ class DesignateBasic(utils.DesignateScenario): @validation.required_services(consts.Service.DESIGNATE) @validation.required_openstack(users=True) - @scenario.configure(context={"cleanup": ["designate_v2"]}) + @scenario.configure(context={"cleanup": ["designate"]}) def list_zones(self): """List Designate zones. @@ -219,7 +220,7 @@ class DesignateBasic(utils.DesignateScenario): @validation.required_services(consts.Service.DESIGNATE) @validation.required_openstack(users=True) - @scenario.configure(context={"cleanup": ["designate_v2"]}) + @scenario.configure(context={"cleanup": ["designate"]}) def create_and_delete_zone(self): """Create and then delete a zone. @@ -231,7 +232,7 @@ class DesignateBasic(utils.DesignateScenario): @validation.required_services(consts.Service.DESIGNATE) @validation.required_openstack(users=True) - @scenario.configure(context={"cleanup": ["designate_v2"]}) + @scenario.configure(context={"cleanup": ["designate"]}) def list_recordsets(self, zone_id): """List Designate recordsets. @@ -242,3 +243,55 @@ class DesignateBasic(utils.DesignateScenario): """ self._list_recordsets(zone_id) + + @validation.required_services(consts.Service.DESIGNATE) + @validation.required_openstack(users=True) + @validation.required_contexts("zones") + @scenario.configure(context={"cleanup": ["designate"]}) + def create_and_delete_recordsets(self, recordsets_per_zone=5): + """Create and then delete recordsets. + + Measure the performance of creating and deleting recordsets + with different level of load. + + :param recordsets_per_zone: recordsets to create pr zone. + """ + zone = random.choice(self.context["tenant"]["zones"]) + + recordsets = [] + + key = "designate.create_%s_recordsets" % recordsets_per_zone + with atomic.ActionTimer(self, key): + for i in range(recordsets_per_zone): + recordset = self._create_recordset(zone, atomic_action=False) + recordsets.append(recordset) + + key = "designate.delete_%s_recordsets" % recordsets_per_zone + with atomic.ActionTimer(self, key): + for recordset in recordsets: + self._delete_recordset( + zone["id"], recordset["id"], atomic_action=False) + + @validation.required_services(consts.Service.DESIGNATE) + @validation.required_openstack(users=True) + @validation.required_contexts("zones") + @scenario.configure(context={"cleanup": ["designate"]}) + def create_and_list_recordsets(self, recordsets_per_zone=5): + """Create and then list recordsets. + + If you have only 1 user in your context, you will + add 1 recordset on every iteration. So you will have more + and more recordsets and will be able to measure the + performance of the "openstack recordset list" command depending on + the number of zones/recordsets owned by users. + + :param recordsets_per_zone: recordsets to create pr zone. + """ + zone = random.choice(self.context["tenant"]["zones"]) + + key = "designate.create_%s_recordsets" % recordsets_per_zone + with atomic.ActionTimer(self, key): + for i in range(recordsets_per_zone): + self._create_recordset(zone, atomic_action=False) + + self._list_recordsets(zone["id"]) diff --git a/rally/plugins/openstack/scenarios/designate/utils.py b/rally/plugins/openstack/scenarios/designate/utils.py index bd7677bf18..18b5577e3d 100644 --- a/rally/plugins/openstack/scenarios/designate/utils.py +++ b/rally/plugins/openstack/scenarios/designate/utils.py @@ -187,3 +187,44 @@ class DesignateScenario(scenario.OpenStackScenario): """ return self.clients("designate", version="2").recordsets.list( zone_id, criterion=criterion, marker=marker, limit=limit) + + def _create_recordset(self, zone, recordset=None, atomic_action=True): + """Create a recordset in a zone. + + :param zone: zone dict + :param recordset: recordset dict + :param atomic_action: True if the recordset creation should be tracked + as an atomic action + :returns: Designate recordset dict + """ + recordset = recordset or {} + recordset.setdefault("type_", recordset.pop("type", "A")) + if "name" not in recordset: + recordset["name"] = "%s.%s" % (self.generate_random_name(), + zone["name"]) + if "records" not in recordset: + recordset["records"] = ["10.0.0.1"] + + client = self.clients("designate", version="2") + + if atomic_action: + with atomic.ActionTimer(self, "designate.create_recordset"): + return client.recordsets.create(zone["id"], **recordset) + + return client.recordsets.create(zone["id"], **recordset) + + def _delete_recordset(self, zone_id, recordset_id, atomic_action=True): + """Delete a zone recordset. + + :param zone_id: Zone ID + :param recordset_id: Recordset ID + :param atomic_action: True if the recordset creation should be tracked + as an atomic action + """ + client = self.clients("designate", version="2") + + if atomic_action: + with atomic.ActionTimer(self, "designate.delete_recordset"): + client.recordsets.delete(zone_id, recordset_id) + else: + client.recordsets.delete(zone_id, recordset_id) diff --git a/samples/tasks/scenarios/designate/create-and-delete-recordsets.json b/samples/tasks/scenarios/designate/create-and-delete-recordsets.json new file mode 100644 index 0000000000..742c475d6d --- /dev/null +++ b/samples/tasks/scenarios/designate/create-and-delete-recordsets.json @@ -0,0 +1,31 @@ +{ + "DesignateBasic.create_and_delete_recordsets": [ + { + "args": { + "recordsets_per_zone": 10 + }, + "runner": { + "type": "constant", + "times": 10, + "concurrency": 10 + }, + "context": { + "quotas": { + "designate": { + "domains": 100, + "domain_recordsets": 2000, + "domain_records": 2000, + "recordset_records": 2000 + } + }, + "users": { + "tenants": 2, + "users_per_tenant": 2 + }, + "zones": { + "zones_per_tenant": 1 + } + } + } + ] +} diff --git a/samples/tasks/scenarios/designate/create-and-delete-recordsets.yaml b/samples/tasks/scenarios/designate/create-and-delete-recordsets.yaml new file mode 100644 index 0000000000..ac80f3152d --- /dev/null +++ b/samples/tasks/scenarios/designate/create-and-delete-recordsets.yaml @@ -0,0 +1,21 @@ +--- + DesignateBasic.create_and_delete_recordsets: + - + args: + recordsets_per_zone: 10 + runner: + type: "constant" + times: 10 + concurrency: 10 + context: + quotas: + designate: + domains: 100 + domain_recordsets: 2000 + domain_records: 2000 + recordset_records: 2000 + users: + tenants: 2 + users_per_tenant: 2 + zones: + zones_per_tenant: 1 diff --git a/samples/tasks/scenarios/designate/create-and-list-recordsets.json b/samples/tasks/scenarios/designate/create-and-list-recordsets.json new file mode 100644 index 0000000000..4dbba95553 --- /dev/null +++ b/samples/tasks/scenarios/designate/create-and-list-recordsets.json @@ -0,0 +1,31 @@ +{ + "DesignateBasic.create_and_list_recordsets": [ + { + "args": { + "recordsets_per_zone": 10 + }, + "runner": { + "type": "constant", + "times": 10, + "concurrency": 10 + }, + "context": { + "quotas": { + "designate": { + "domains": 100, + "domain_recordsets": 2000, + "domain_records": 2000, + "recordset_records": 2000 + } + }, + "users": { + "tenants": 2, + "users_per_tenant": 2 + }, + "zones": { + "zones_per_tenant": 1 + } + } + } + ] +} diff --git a/samples/tasks/scenarios/designate/create-and-list-recordsets.yaml b/samples/tasks/scenarios/designate/create-and-list-recordsets.yaml new file mode 100644 index 0000000000..7660f4add6 --- /dev/null +++ b/samples/tasks/scenarios/designate/create-and-list-recordsets.yaml @@ -0,0 +1,21 @@ +--- + DesignateBasic.create_and_list_recordsets: + - + args: + recordsets_per_zone: 10 + runner: + type: "constant" + times: 10 + concurrency: 10 + context: + quotas: + designate: + domains: 100 + domain_recordsets: 2000 + domain_records: 2000 + recordset_records: 2000 + users: + tenants: 2 + users_per_tenant: 2 + zones: + zones_per_tenant: 1 diff --git a/tests/unit/plugins/openstack/cleanup/test_resources.py b/tests/unit/plugins/openstack/cleanup/test_resources.py index 3cbb4aef97..88fd2892fb 100644 --- a/tests/unit/plugins/openstack/cleanup/test_resources.py +++ b/tests/unit/plugins/openstack/cleanup/test_resources.py @@ -17,41 +17,13 @@ from boto import exception as boto_exception import mock from neutronclient.common import exceptions as neutron_exceptions -from rally.common.plugin import discover from rally.common import utils -from rally.plugins.openstack.cleanup import base from rally.plugins.openstack.cleanup import resources from tests.unit import test BASE = "rally.plugins.openstack.cleanup.resources" -class AllResourceManagerTestCase(test.TestCase): - - def test_res_manager_special_field(self): - - for res_mgr in discover.itersubclasses(base.ResourceManager): - manager_name = "%s.%s" % (res_mgr.__module__, res_mgr.__name__) - - fields = filter(lambda x: not x.startswith("__"), dir(res_mgr)) - - available_opts = set([ - "_admin_required", "_perform_for_admin_only", - "_tenant_resource", "_service", "_resource", "_order", - "_max_attempts", "_timeout", "_interval", "_threads", - "_manager", "id", "is_deleted", "delete", "list", - "supports_extension" - ]) - - extra_opts = set(fields) - available_opts - - self.assertFalse( - extra_opts, - ("ResourceManager %(name)s contains extra fields: %(opts)s." - " Remove them to pass this test") - % {"name": manager_name, "opts": ", ".join(extra_opts)}) - - class SynchronizedDeletionTestCase(test.TestCase): def test_is_deleted(self): diff --git a/tests/unit/plugins/openstack/scenarios/designate/test_basic.py b/tests/unit/plugins/openstack/scenarios/designate/test_basic.py index d0fae78629..1cf1cf60dd 100644 --- a/tests/unit/plugins/openstack/scenarios/designate/test_basic.py +++ b/tests/unit/plugins/openstack/scenarios/designate/test_basic.py @@ -24,7 +24,6 @@ DESIGNATE_BASIC = ("rally.plugins.openstack.scenarios.designate.basic" class DesignateBasicTestCase(test.ScenarioTestCase): - @mock.patch(DESIGNATE_BASIC + "._list_domains") @mock.patch(DESIGNATE_BASIC + "._create_domain") def test_create_and_list_domains(self, mock_designate_basic__create_domain, @@ -214,3 +213,53 @@ class DesignateBasicTestCase(test.ScenarioTestCase): # Default options scenario.list_recordsets("123") mock_designate_basic__list_recordsets.assert_called_once_with("123") + + @mock.patch(DESIGNATE_BASIC + "._delete_recordset") + @mock.patch(DESIGNATE_BASIC + "._create_recordset") + def test_create_and_delete_recordsets( + self, mock_designate_basic__create_recordset, + mock_designate_basic__delete_recordset): + zone = {"id": "1234"} + self.context.update({ + "tenant": { + "zones": [zone] + } + }) + + scenario = basic.DesignateBasic(self.context) + mock_designate_basic__create_recordset.return_value = {"id": "321"} + recordsets_per_zone = 5 + + scenario.create_and_delete_recordsets( + recordsets_per_zone=recordsets_per_zone) + self.assertEqual( + mock_designate_basic__create_recordset.mock_calls, + [mock.call(zone, atomic_action=False)] + * recordsets_per_zone) + self.assertEqual( + mock_designate_basic__delete_recordset.mock_calls, + [mock.call(zone["id"], "321", atomic_action=False)] + * recordsets_per_zone) + + @mock.patch(DESIGNATE_BASIC + "._list_recordsets") + @mock.patch(DESIGNATE_BASIC + "._create_recordset") + def test_create_and_list_recordsets( + self, mock_designate_basic__create_recordset, + mock_designate_basic__list_recordsets): + zone = {"id": "1234"} + self.context.update({ + "tenant": { + "zones": [zone] + } + }) + scenario = basic.DesignateBasic(self.context) + recordsets_per_zone = 5 + + scenario.create_and_list_recordsets( + recordsets_per_zone=recordsets_per_zone) + self.assertEqual( + mock_designate_basic__create_recordset.mock_calls, + [mock.call(zone, atomic_action=False)] + * recordsets_per_zone) + mock_designate_basic__list_recordsets.assert_called_once_with( + zone["id"]) diff --git a/tests/unit/plugins/openstack/scenarios/designate/test_utils.py b/tests/unit/plugins/openstack/scenarios/designate/test_utils.py index 43074277e8..2251e68d0d 100644 --- a/tests/unit/plugins/openstack/scenarios/designate/test_utils.py +++ b/tests/unit/plugins/openstack/scenarios/designate/test_utils.py @@ -221,3 +221,67 @@ class DesignateScenarioTestCase(test.ScenarioTestCase): return_recordsets_list) self._test_atomic_action_timer(scenario.atomic_actions(), "designate.list_recordsets") + + @ddt.data( + {}, + {"data": "127.0.0.1"}) + def test_create_recordset(self, recordset_data): + scenario = utils.DesignateScenario() + + random_name = "foo" + zone_name = "zone.name." + random_recordset_name = "%s.%s" % (random_name, zone_name) + + scenario = utils.DesignateScenario(context=self.context) + scenario.generate_random_name = mock.Mock(return_value=random_name) + + zone = {"name": zone_name, "id": "123"} + + # Create with randoms (name and type) + scenario._create_recordset(zone) + + self.client.recordsets.create.assert_called_once_with( + zone["id"], + name=random_recordset_name, + type_="A", + records=["10.0.0.1"]) + + self._test_atomic_action_timer(scenario.atomic_actions(), + "designate.create_recordset") + + self.client.recordsets.create.reset_mock() + + # Specify name + recordset = {"name": "www.zone.name.", "type_": "ASD"} + scenario._create_recordset(zone, recordset) + self.client.recordsets.create.assert_called_once_with( + zone["id"], + name="www.zone.name.", + type_="ASD", + records=["10.0.0.1"]) + + self.client.recordsets.create.reset_mock() + + # Specify type without underscore + scenario._create_recordset(zone, {"type": "A"}) + self.client.recordsets.create.assert_called_once_with( + zone["id"], + name="foo.zone.name.", + type_="A", + records=["10.0.0.1"]) + + def test_delete_recordset(self): + scenario = utils.DesignateScenario(context=self.context) + + zone_id = mock.Mock() + recordset_id = mock.Mock() + scenario._delete_recordset(zone_id, recordset_id) + self.client.recordsets.delete.assert_called_once_with( + zone_id, recordset_id) + self._test_atomic_action_timer(scenario.atomic_actions(), + "designate.delete_recordset") + + self.client.recordsets.delete.reset_mock() + scenario._delete_recordset(zone_id, recordset_id, atomic_action=False) + self.client.recordsets.delete.assert_called_once_with( + zone_id, recordset_id)