From 87c4b821ffca7598de18f9672db894d32923dc78 Mon Sep 17 00:00:00 2001 From: Anna Khmelnitsky Date: Thu, 4 Oct 2018 14:59:33 -0700 Subject: [PATCH] Initial support for policy transactions Policy PATCH API supports creating multiple objects in one call, in transactional manner. This patch suggest using "with NsxPolicyTransaction" syntax to achive single batch creation for multiple objects. For now, only top level objects and their descendants are supported, under top level url (/infra). Change-Id: I209e63fc41d3c4644142df587eca6295797ed6af --- tools/policy/poltool.py | 42 ++++- .../tests/unit/v3/policy_testcase.py | 35 ++++ .../tests/unit/v3/test_policy_api.py | 34 +--- .../tests/unit/v3/test_policy_resources.py | 18 +- .../tests/unit/v3/test_policy_transaction.py | 114 ++++++++++++ vmware_nsxlib/v3/policy_defs.py | 64 ++++++- vmware_nsxlib/v3/policy_resources.py | 76 ++++---- vmware_nsxlib/v3/policy_transaction.py | 164 ++++++++++++++++++ 8 files changed, 474 insertions(+), 73 deletions(-) create mode 100644 vmware_nsxlib/tests/unit/v3/policy_testcase.py create mode 100644 vmware_nsxlib/tests/unit/v3/test_policy_transaction.py create mode 100644 vmware_nsxlib/v3/policy_transaction.py diff --git a/tools/policy/poltool.py b/tools/policy/poltool.py index bcfd847d..77ec0106 100755 --- a/tools/policy/poltool.py +++ b/tools/policy/poltool.py @@ -121,12 +121,36 @@ def build_args(resource_type, resource_id, args, add_name=True): return args -def create_resource(lib, resource_type, resource_id, args): +def create_resource(lib, transaction, count, resource_type, resource_id, args): + + from vmware_nsxlib.v3 import policy_transaction as trans args = build_args(resource_type, resource_id, args) api = get_resource_api(lib, resource_type) - api.create_or_overwrite(**args) + def create_multiple(): + if count == 1: + api.create_or_overwrite(**args) + + else: + for i in range(1, count + 1): + new_args = copy.deepcopy(args) + print(args) + if 'name' in args: + new_args['name'] = "%s%d" % (args['name'], i) + + id_marker = resource_type + '_id' + if id_marker in args: + new_args[id_marker] = "%s%d" % (args[id_marker], i) + + api.create_or_overwrite(**new_args) + + if transaction: + with trans.NsxPolicyTransaction(): + create_multiple() + + else: + create_multiple() def update_resource(lib, resource_type, resource_id, args): @@ -199,11 +223,14 @@ def main(argv=sys.argv): usage = "Usage: %s -o -r " \ "-i -a " % argv[0] try: - opts, args = getopt.getopt(argv[1:], "o:r:i:a:") + opts, args = getopt.getopt(argv[1:], "to:r:i:a:c:") except getopt.GetoptError: print(usage) sys.exit(1) + transaction = False + count = 1 + for opt, val in opts: if opt in ('-o'): op = val @@ -213,6 +240,12 @@ def main(argv=sys.argv): elif opt in ('-p'): policy_ip = val + elif opt in ('-t'): + transaction = True + + elif opt in ('-c'): + count = val + elif opt in ('-r'): resource_type = val if resource_type not in RESOURCES: @@ -245,7 +278,8 @@ def main(argv=sys.argv): print(json.dumps(result, indent=4)) elif op == 'create': - create_resource(nsxlib, resource_type, resource_id, resource_args) + create_resource(nsxlib, transaction, int(count), + resource_type, resource_id, resource_args) elif op == 'delete': delete_resource(nsxlib, resource_type, resource_id) elif op == 'update': diff --git a/vmware_nsxlib/tests/unit/v3/policy_testcase.py b/vmware_nsxlib/tests/unit/v3/policy_testcase.py new file mode 100644 index 00000000..d164d407 --- /dev/null +++ b/vmware_nsxlib/tests/unit/v3/policy_testcase.py @@ -0,0 +1,35 @@ +# Copyright 2018 VMware, 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 vmware_nsxlib.tests.unit.v3 import nsxlib_testcase +from vmware_nsxlib.v3 import client +from vmware_nsxlib.v3 import policy_defs as policy + +BASE_POLICY_URI = "https://1.2.3.4/policy/api/v1/" + + +class TestPolicyApi(nsxlib_testcase.NsxClientTestCase): + + def setUp(self): + self.client = self.new_mocked_client(client.NSX3Client, + url_prefix='policy/api/v1/') + self.policy_api = policy.NsxPolicyApi(self.client) + + super(TestPolicyApi, self).setUp() + + def assert_json_call(self, method, client, url, data=None): + url = BASE_POLICY_URI + url + return super(TestPolicyApi, self).assert_json_call( + method, client, url, data=data) diff --git a/vmware_nsxlib/tests/unit/v3/test_policy_api.py b/vmware_nsxlib/tests/unit/v3/test_policy_api.py index 843df2af..f29d035c 100644 --- a/vmware_nsxlib/tests/unit/v3/test_policy_api.py +++ b/vmware_nsxlib/tests/unit/v3/test_policy_api.py @@ -13,31 +13,13 @@ # License for the specific language governing permissions and limitations # under the License. # -from vmware_nsxlib.tests.unit.v3 import nsxlib_testcase -from vmware_nsxlib.v3 import client +from vmware_nsxlib.tests.unit.v3 import policy_testcase from vmware_nsxlib.v3 import nsx_constants from vmware_nsxlib.v3 import policy_constants from vmware_nsxlib.v3 import policy_defs as policy -BASE_POLICY_URI = "https://1.2.3.4/policy/api/v1/" - -class TestPolicyApi(nsxlib_testcase.NsxClientTestCase): - - def setUp(self): - self.client = self.new_mocked_client(client.NSX3Client, - url_prefix='policy/api/v1/') - self.policy_api = policy.NsxPolicyApi(self.client) - - super(TestPolicyApi, self).setUp() - - def assert_json_call(self, method, client, url, data=None): - url = BASE_POLICY_URI + url - return super(TestPolicyApi, self).assert_json_call( - method, client, url, data=data) - - -class TestPolicyDomain(TestPolicyApi): +class TestPolicyDomain(policy_testcase.TestPolicyApi): def test_create(self): domain_def = policy.DomainDef( @@ -67,7 +49,7 @@ class TestPolicyDomain(TestPolicyApi): self.assert_json_call('GET', self.client, 'infra/domains') -class TestPolicyGroup(TestPolicyApi): +class TestPolicyGroup(policy_testcase.TestPolicyApi): def test_create(self): group_def = policy.GroupDef( @@ -142,7 +124,7 @@ class TestPolicyGroup(TestPolicyApi): 'infra/domains/eukarya/groups/giraffe') -class TestPolicyService(TestPolicyApi): +class TestPolicyService(policy_testcase.TestPolicyApi): def test_create(self): service_def = policy.ServiceDef(service_id='roomservice') @@ -194,7 +176,7 @@ class TestPolicyService(TestPolicyApi): data=expected_data) -class TestPolicyCommunicationMap(TestPolicyApi): +class TestPolicyCommunicationMap(policy_testcase.TestPolicyApi): def setUp(self): super(TestPolicyCommunicationMap, self).setUp() @@ -286,7 +268,7 @@ class TestPolicyCommunicationMap(TestPolicyApi): 'rules/en2') -class TestPolicyEnforcementPoint(TestPolicyApi): +class TestPolicyEnforcementPoint(policy_testcase.TestPolicyApi): def test_create(self): ep_def = policy.EnforcementPointDef(ep_id='ep1', name='The Point', @@ -301,7 +283,7 @@ class TestPolicyEnforcementPoint(TestPolicyApi): data=ep_def.get_obj_dict()) -class TestPolicyTransportZone(TestPolicyApi): +class TestPolicyTransportZone(policy_testcase.TestPolicyApi): def test_get(self): tz_def = policy.TransportZoneDef(tz_id='tz1', ep_id='default') @@ -310,7 +292,7 @@ class TestPolicyTransportZone(TestPolicyApi): self.assert_json_call('GET', self.client, tz_path) -class TestPolicyDeploymentMap(TestPolicyApi): +class TestPolicyDeploymentMap(policy_testcase.TestPolicyApi): def test_create(self): map_def = policy.DeploymentMapDef(map_id='dm1', diff --git a/vmware_nsxlib/tests/unit/v3/test_policy_resources.py b/vmware_nsxlib/tests/unit/v3/test_policy_resources.py index 2f469648..f2414954 100644 --- a/vmware_nsxlib/tests/unit/v3/test_policy_resources.py +++ b/vmware_nsxlib/tests/unit/v3/test_policy_resources.py @@ -821,7 +821,7 @@ class TestPolicyCommunicationMap(NsxPolicyLibTestCase): direction = nsx_constants.IN_OUT get_return_value = {'rules': [{'sequence_number': 1}]} with mock.patch.object(self.policy_api, - "create_or_update") as api_call,\ + "create_with_parent") as api_call,\ mock.patch.object(self.policy_api, "get", return_value=get_return_value): self.resourceApi.create_or_overwrite(name, domain_id, @@ -834,7 +834,7 @@ class TestPolicyCommunicationMap(NsxPolicyLibTestCase): direction=direction, logged=True, tenant=TEST_TENANT) - expected_def = policy_defs.CommunicationMapDef( + map_def = policy_defs.CommunicationMapDef( domain_id=domain_id, map_id=map_id, name=name, @@ -842,9 +842,8 @@ class TestPolicyCommunicationMap(NsxPolicyLibTestCase): category=policy_constants.CATEGORY_APPLICATION, precedence=0, tenant=TEST_TENANT) - self.assert_called_with_def(api_call, expected_def) - expected_def = policy_defs.CommunicationMapEntryDef( + entry_def = policy_defs.CommunicationMapEntryDef( domain_id=domain_id, map_id=map_id, entry_id='entry', @@ -858,7 +857,7 @@ class TestPolicyCommunicationMap(NsxPolicyLibTestCase): direction=direction, logged=True, tenant=TEST_TENANT) - self.assert_called_with_def(api_call, expected_def, call_num=1) + self.assert_called_with_defs(api_call, [map_def, entry_def]) def test_create_first_seqnum(self): domain_id = '111' @@ -871,7 +870,7 @@ class TestPolicyCommunicationMap(NsxPolicyLibTestCase): category = 'Emergency' get_return_value = {'rules': []} with mock.patch.object(self.policy_api, - "create_or_update") as api_call, \ + "create_with_parent") as api_call, \ mock.patch.object(self.resourceApi, "get", return_value=get_return_value): self.resourceApi.create_or_overwrite(name, domain_id, @@ -884,7 +883,7 @@ class TestPolicyCommunicationMap(NsxPolicyLibTestCase): logged=False, tenant=TEST_TENANT) - expected_def = policy_defs.CommunicationMapDef( + map_def = policy_defs.CommunicationMapDef( domain_id=domain_id, map_id=map_id, name=name, @@ -892,9 +891,8 @@ class TestPolicyCommunicationMap(NsxPolicyLibTestCase): category=category, precedence=0, tenant=TEST_TENANT) - self.assert_called_with_def(api_call, expected_def) - expected_def = policy_defs.CommunicationMapEntryDef( + entry_def = policy_defs.CommunicationMapEntryDef( domain_id=domain_id, map_id=map_id, entry_id='entry', @@ -907,7 +905,7 @@ class TestPolicyCommunicationMap(NsxPolicyLibTestCase): dest_groups=[dest_group], logged=False, tenant=TEST_TENANT) - self.assert_called_with_def(api_call, expected_def, call_num=1) + self.assert_called_with_defs(api_call, [map_def, entry_def]) def test_create_without_seqnum(self): domain_id = '111' diff --git a/vmware_nsxlib/tests/unit/v3/test_policy_transaction.py b/vmware_nsxlib/tests/unit/v3/test_policy_transaction.py new file mode 100644 index 00000000..16e8f4a1 --- /dev/null +++ b/vmware_nsxlib/tests/unit/v3/test_policy_transaction.py @@ -0,0 +1,114 @@ +# Copyright 2018 VMware, 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 vmware_nsxlib import v3 + +from vmware_nsxlib.tests.unit.v3 import nsxlib_testcase +from vmware_nsxlib.tests.unit.v3 import policy_testcase + +from vmware_nsxlib.v3 import policy_transaction as trans + + +class TestPolicyTransaction(policy_testcase.TestPolicyApi): + + def setUp(self): + + super(TestPolicyTransaction, self).setUp() + + nsxlib_config = nsxlib_testcase.get_default_nsxlib_config() + self.policy_lib = v3.NsxPolicyLib(nsxlib_config) + self.policy_api = self.policy_lib.policy_api + self.policy_api.client = self.client + + def assert_infra_patch_call(self, body): + self.assert_json_call('PATCH', self.client, 'infra', + data=body) + + def test_domains_only(self): + + tags = [{'scope': 'color', 'tag': 'green'}] + d1 = {'resource_type': 'Domain', 'id': 'domain1', + 'display_name': 'd1', 'description': 'first domain', + 'tags': tags} + + d2 = {'resource_type': 'Domain', 'id': 'domain2', + 'display_name': 'd2', 'description': 'no tags'} + with trans.NsxPolicyTransaction(): + + for d in (d1, d2): + self.policy_lib.domain.create_or_overwrite( + d['display_name'], + d['id'], + d['description'], + tags=d['tags'] if 'tags' in d else None) + + expected_body = {'resource_type': 'Infra', + 'children': [{'resource_type': 'ChildDomain', + 'Domain': d1}, + {'resource_type': 'ChildDomain', + 'Domain': d2}]} + + self.assert_infra_patch_call(expected_body) + + def test_domains_and_groups(self): + + tags = [{'scope': 'color', 'tag': 'green'}] + g1 = {'resource_type': 'Group', 'id': 'group1', + 'display_name': 'g1', + 'description': 'first group'} + g2 = {'resource_type': 'Group', 'id': 'group2', + 'description': 'second group', + 'display_name': 'g2', + 'tags': tags} + g3 = {'resource_type': 'Group', 'id': 'group3', + 'display_name': 'g3', + 'description': 'third group'} + d1 = {'resource_type': 'Domain', 'id': 'domain1', + 'display_name': 'd1', 'description': 'first domain', + 'tags': tags} + + d2 = {'resource_type': 'Domain', 'id': 'domain2', + 'display_name': 'd2', 'description': 'no tags'} + + with trans.NsxPolicyTransaction(): + + for d in (d1, d2): + self.policy_lib.domain.create_or_overwrite( + d['display_name'], + d['id'], + d['description'], + tags=d['tags'] if 'tags' in d else None) + + d['children'] = [] + + for g in (g1, g2, g3): + self.policy_lib.group.create_or_overwrite( + g['display_name'], + d['id'], + g['id'], + g['description'], + tags=g['tags'] if 'tags' in g else None) + + d['children'].append({'resource_type': 'ChildGroup', + 'Group': g}) + + expected_body = {'resource_type': 'Infra', + 'children': [{'resource_type': 'ChildDomain', + 'Domain': d1}, + {'resource_type': 'ChildDomain', + 'Domain': d2}]} + + self.assert_infra_patch_call(expected_body) diff --git a/vmware_nsxlib/v3/policy_defs.py b/vmware_nsxlib/v3/policy_defs.py index df682f8c..2b6f60f0 100644 --- a/vmware_nsxlib/v3/policy_defs.py +++ b/vmware_nsxlib/v3/policy_defs.py @@ -79,6 +79,9 @@ class ResourceDef(object): def resource_type(): pass + def path_defs(self): + pass + def get_id(self): if self.attrs and self.path_ids: return self.attrs.get(self.path_ids[-1]) @@ -170,6 +173,29 @@ class ResourceDef(object): return len(body_args) == 0 +class TenantDef(ResourceDef): + @property + def path_pattern(self): + return TENANTS_PATH_PATTERN + + @staticmethod + def resource_type(): + return 'Infra' + + def path_defs(self): + return () + + @property + def path_ids(self): + return ('tenant',) + + def get_resource_path(self): + return 'infra/' + + def get_section_path(self): + return 'infra/' + + class DomainDef(ResourceDef): @property @@ -184,6 +210,9 @@ class DomainDef(ResourceDef): def resource_type(): return 'Domain' + def path_defs(self): + return (TenantDef,) + class RouteAdvertisement(object): @@ -218,6 +247,9 @@ class RouteAdvertisement(object): class RouterDef(ResourceDef): + def path_defs(self): + return (TenantDef,) + def get_obj_dict(self): body = super(RouterDef, self).get_obj_dict() @@ -332,6 +364,9 @@ class Tier1SegmentDef(BaseSegmentDef): def path_ids(self): return ('tenant', 'tier1_id', 'segment_id') + def path_defs(self): + return (TenantDef, Tier1Def) + class SegmentDef(BaseSegmentDef): '''These segments don't belong to particular tier1. @@ -347,6 +382,9 @@ class SegmentDef(BaseSegmentDef): def path_ids(self): return ('tenant', 'segment_id') + def path_defs(self): + return (TenantDef,) + def get_obj_dict(self): body = super(SegmentDef, self).get_obj_dict() if self.get_attr('tier1_id'): @@ -390,6 +428,9 @@ class SegmentPortDef(ResourceDef): def resource_type(): return 'SegmentPort' + def path_defs(self): + return (TenantDef, SegmentDef) + def get_obj_dict(self): body = super(SegmentPortDef, self).get_obj_dict() address_bindings = self.get_attr('address_bindings') @@ -471,6 +512,9 @@ class GroupDef(ResourceDef): def resource_type(): return 'Group' + def path_defs(self): + return (TenantDef, DomainDef) + def get_obj_dict(self): body = super(GroupDef, self).get_obj_dict() conds = self.get_attr('conditions') @@ -510,6 +554,9 @@ class ServiceDef(ResourceDef): def resource_type(): return 'Service' + def path_defs(self): + return (TenantDef,) + def get_obj_dict(self): body = super(ServiceDef, self).get_obj_dict() entries = [entry.get_obj_dict() @@ -533,6 +580,9 @@ class ServiceEntryDef(ResourceDef): def path_ids(self): return ('tenant', 'service_id', 'entry_id') + def path_defs(self): + return (TenantDef, ServiceDef) + class L4ServiceEntryDef(ServiceEntryDef): @@ -595,6 +645,9 @@ class CommunicationMapDef(ResourceDef): def resource_type(): return 'SecurityPolicy' + def path_defs(self): + return (TenantDef, DomainDef) + def get_obj_dict(self): body = super(CommunicationMapDef, self).get_obj_dict() for attr in ('category', 'precedence'): @@ -642,6 +695,9 @@ class CommunicationMapEntryDef(ResourceDef): def resource_type(): return 'Rule' + def path_defs(self): + return (TenantDef, DomainDef, CommunicationMapDef) + def get_obj_dict(self): body = super(CommunicationMapEntryDef, self).get_obj_dict() domain_id = self.get_attr('domain_id') @@ -650,7 +706,7 @@ class CommunicationMapEntryDef(ResourceDef): body['destination_groups'] = self.get_groups_path( domain_id, self.get_attr('dest_groups')) - self._set_attrs_in_body(body, ['sequence_number', 'services', 'scope', + self._set_attrs_in_body(body, ['sequence_number', 'scope', 'action', 'direction', 'logged']) service_ids = self.get_attr('service_ids') @@ -706,6 +762,9 @@ class EnforcementPointDef(ResourceDef): def resource_type(): return 'EnforcementPoint' + def path_defs(self): + return (TenantDef,) + def get_obj_dict(self): body = super(EnforcementPointDef, self).get_obj_dict() body['id'] = self.get_id() @@ -761,6 +820,9 @@ class DeploymentMapDef(ResourceDef): def resource_type(): return 'DeploymentMap' + def path_defs(self): + return (TenantDef, DomainDef) + def get_obj_dict(self): body = super(DeploymentMapDef, self).get_obj_dict() body['id'] = self.get_id() diff --git a/vmware_nsxlib/v3/policy_resources.py b/vmware_nsxlib/v3/policy_resources.py index aaf48d62..cf34a87a 100644 --- a/vmware_nsxlib/v3/policy_resources.py +++ b/vmware_nsxlib/v3/policy_resources.py @@ -25,6 +25,7 @@ from vmware_nsxlib.v3 import exceptions from vmware_nsxlib.v3 import nsx_constants from vmware_nsxlib.v3 import policy_constants from vmware_nsxlib.v3 import policy_defs +from vmware_nsxlib.v3 import policy_transaction as policy_trans from vmware_nsxlib.v3 import utils LOG = logging.getLogger(__name__) @@ -154,6 +155,20 @@ class NsxPolicyResourceBase(object): def _list(self, obj_def): return self.policy_api.list(obj_def).get('results', []) + def _create_or_store(self, policy_def, child_def=None): + transaction = policy_trans.NsxPolicyTransaction.get_current() + if transaction: + # Store this def for batch apply for this transaction + transaction.store_def(policy_def, self.policy_api.client) + if child_def: + transaction.store_def(child_def, self.policy_api.client) + else: + # No transaction - apply now + if child_def: + self.policy_api.create_with_parent(policy_def, child_def) + else: + self.policy_api.create_or_update(policy_def) + class NsxPolicyDomainApi(NsxPolicyResourceBase): """NSX Policy Domain.""" @@ -171,7 +186,7 @@ class NsxPolicyDomainApi(NsxPolicyResourceBase): tags=tags, tenant=tenant) - self.policy_api.create_or_update(domain_def) + self._create_or_store(domain_def) return domain_id def delete(self, domain_id, tenant=policy_constants.POLICY_INFRA_TENANT): @@ -232,7 +247,7 @@ class NsxPolicyGroupApi(NsxPolicyResourceBase): conditions=conditions, tags=tags, tenant=tenant) - self.policy_api.create_or_update(group_def) + self._create_or_store(group_def) return group_id def build_condition( @@ -280,7 +295,8 @@ class NsxPolicyGroupApi(NsxPolicyResourceBase): conditions=conditions, tags=tags, tenant=tenant) - return self.policy_api.create_or_update(group_def) + self._create_or_store(group_def) + return group_id def delete(self, domain_id, group_id, tenant=policy_constants.POLICY_INFRA_TENANT): @@ -416,7 +432,7 @@ class NsxPolicyL4ServiceApi(NsxPolicyServiceBase): dest_ports=dest_ports, tenant=tenant) - self.policy_api.create_with_parent(service_def, entry_def) + self._create_or_store(service_def, entry_def) return service_id def update(self, service_id, @@ -470,7 +486,7 @@ class NsxPolicyIcmpServiceApi(NsxPolicyServiceBase): icmp_code=icmp_code, tenant=tenant) - self.policy_api.create_with_parent(service_def, entry_def) + self._create_or_store(service_def, entry_def) return service_id def update(self, service_id, @@ -522,7 +538,7 @@ class NsxPolicyIPProtocolServiceApi(NsxPolicyServiceBase): protocol_number=protocol_number, tenant=tenant) - self.policy_api.create_with_parent(service_def, entry_def) + self._create_or_store(service_def, entry_def) return service_id def update(self, service_id, @@ -578,7 +594,7 @@ class NsxPolicyTier1Api(NsxPolicyResourceBase): failover_mode=failover_mode, route_advertisement=route_advertisement, tenant=tenant) - self.policy_api.create_or_update(tier1_def) + self._create_or_store(tier1_def) return tier1_id def delete(self, tier1_id, tenant=policy_constants.POLICY_INFRA_TENANT): @@ -625,6 +641,7 @@ class NsxPolicyTier1Api(NsxPolicyResourceBase): nat=nat, lb_vip=lb_vip, lb_snat=lb_snat) + tier1_def = self.entry_def(tier1_id=tier1_id, route_adv=route_adv, tenant=tenant) @@ -723,7 +740,7 @@ class NsxPolicyTier1SegmentApi(NsxPolicyResourceBase): default_rule_logging=default_rule_logging, tags=tags, tenant=tenant) - self.policy_api.create_or_update(segment_def) + self._create_or_store(segment_def) return segment_id def delete(self, tier1_id, segment_id, @@ -796,7 +813,7 @@ class NsxPolicySegmentApi(NsxPolicyResourceBase): transport_zone_id=transport_zone_id, tags=tags, tenant=tenant) - self.policy_api.create_or_update(segment_def) + self._create_or_store(segment_def) return segment_id def delete(self, segment_id, @@ -883,7 +900,7 @@ class NsxPolicySegmentPortApi(NsxPolicyResourceBase): allocate_addresses=allocate_addresses, tags=tags, tenant=tenant) - self.policy_api.create_or_update(port_def) + self._create_or_store(port_def) return port_id def delete(self, segment_id, port_id, @@ -996,13 +1013,10 @@ class NsxPolicyCommunicationMapApi(NsxPolicyResourceBase): seq_nums.sort() return seq_nums[-1] - def _get_seq_num(self, sequence_number, last_sequence): - if not sequence_number: - if last_sequence < 0: - sequence_number = 1 - else: - sequence_number = last_sequence + 1 - return sequence_number + def _get_seq_num(self, last_sequence): + if last_sequence < 0: + return 1 + return last_sequence + 1 def create_or_overwrite(self, name, domain_id, map_id=None, description=None, precedence=0, @@ -1020,15 +1034,17 @@ class NsxPolicyCommunicationMapApi(NsxPolicyResourceBase): this call under lock to prevent race condition where two entries end up with same sequence number. """ + last_sequence = -1 if map_id: - # get the next available sequence number - last_sequence = self._get_last_seq_num(domain_id, map_id, - tenant=tenant) + if not sequence_number: + # get the next available sequence number + last_sequence = self._get_last_seq_num(domain_id, map_id, + tenant=tenant) else: map_id = self._init_obj_uuid(map_id) - last_sequence = -1 - sequence_number = self._get_seq_num(sequence_number, last_sequence) + if not sequence_number: + sequence_number = self._get_seq_num(last_sequence) # Build the communication entry. Since we currently support only one # it will have the same id as its parent @@ -1051,13 +1067,8 @@ class NsxPolicyCommunicationMapApi(NsxPolicyResourceBase): domain_id=domain_id, map_id=map_id, tenant=tenant, name=name, description=description, precedence=precedence, category=category, tags=tags) - if last_sequence < 0: - # if communication map is absent, we need to create it - return self.policy_api.create_with_parent(map_def, entry_def) - # TODO(asarfaty) combine both calls together - self.policy_api.create_or_update(map_def) - self.policy_api.create_or_update(entry_def) + self._create_or_store(map_def, entry_def) return map_id def create_or_overwrite_map_only( @@ -1116,6 +1127,7 @@ class NsxPolicyCommunicationMapApi(NsxPolicyResourceBase): tenant=tenant, name=name, description=description, precedence=precedence, category=category, tags=tags) + # TODO(annak): support transactional create self.policy_api.create_with_parent(map_def, entries) return map_id @@ -1134,7 +1146,7 @@ class NsxPolicyCommunicationMapApi(NsxPolicyResourceBase): if not sequence_number: last_sequence = self._get_last_seq_num(domain_id, map_id, tenant=tenant) - sequence_number = self._get_seq_num(sequence_number, last_sequence) + sequence_number = self._get_seq_num(last_sequence) entry_id = self._init_obj_uuid(entry_id) # Build the communication entry @@ -1153,7 +1165,7 @@ class NsxPolicyCommunicationMapApi(NsxPolicyResourceBase): logged=logged, tenant=tenant) - self.policy_api.create_or_update(entry_def) + self._create_or_store(entry_def) return entry_id def delete(self, domain_id, map_id, @@ -1300,7 +1312,7 @@ class NsxPolicyEnforcementPointApi(NsxPolicyResourceBase): edge_cluster_id=edge_cluster_id, transport_zone_id=transport_zone_id, tenant=tenant) - self.policy_api.create_or_update(ep_def) + self._create_or_store(ep_def) return ep_id def delete(self, ep_id, @@ -1450,7 +1462,7 @@ class NsxPolicyDeploymentMapApi(NsxPolicyResourceBase): ep_id=ep_id, domain_id=domain_id, tenant=tenant) - self.policy_api.create_or_update(map_def) + self._create_or_store(map_def) return map_id def delete(self, map_id, domain_id=None, diff --git a/vmware_nsxlib/v3/policy_transaction.py b/vmware_nsxlib/v3/policy_transaction.py new file mode 100644 index 00000000..edaa609f --- /dev/null +++ b/vmware_nsxlib/v3/policy_transaction.py @@ -0,0 +1,164 @@ +# Copyright 2017 VMware, 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 threading + +from vmware_nsxlib._i18n import _ + +from vmware_nsxlib.v3 import exceptions +from vmware_nsxlib.v3 import policy_constants +from vmware_nsxlib.v3 import policy_defs + + +class NsxPolicyTransactionException(exceptions.NsxLibException): + message = _("Policy Transaction Error: %(msg)s") + + +class NsxPolicyTransaction(object): + # stores current transaction per thread + # nested transactions not supported + + data = threading.local() + + def __init__(self): + # For now only infra tenant is supported + self.defs = [policy_defs.TenantDef( + tenant=policy_constants.POLICY_INFRA_TENANT)] + self.client = None + + def __enter__(self): + if self.get_current(): + raise NsxPolicyTransactionException( + "Nested transactions not supported") + + self.data.instance = self + return self + + def __exit__(self, e_type, e_value, e_traceback): + # Always reset transaction regardless of exceptions + self.data.instance = None + + if e_type: + # If exception occured in the "with" block, raise it + # without applying to backend + return False + + # exception might happen here and will be raised + self.apply_defs() + + def store_def(self, resource_def, client): + if self.client and client != self.client: + raise NsxPolicyTransactionException( + "All operations under transaction must have same client") + + self.client = client + # TODO(annak): raise exception for different tenants + self.defs.append(resource_def) + + def _sort_defs(self): + sorted_defs = [] + + while len(self.defs): + for resource_def in self.defs: + if resource_def in sorted_defs: + continue + + # We want all parents to appear before the child + if not resource_def.path_defs(): + # top level resource + sorted_defs.append(resource_def) + continue + + parent_type = resource_def.path_defs()[-1] + parents = [d for d in self.defs if isinstance(d, parent_type)] + missing_parents = [d for d in parents if d not in sorted_defs] + + if not missing_parents: + # All parents are appended to sorted list, child can go in + sorted_defs.append(resource_def) + + unsorted = [d for d in self.defs if d not in sorted_defs] + self.defs = unsorted + + self.defs = sorted_defs + + def _find_parent_in_dict(self, d, resource_def, level=1): + + if len(resource_def.path_defs()) <= level: + return + + parent_type = resource_def.path_defs()[level] + is_leaf = (level + 1 == len(resource_def.path_defs())) + resource_type = parent_type.resource_type() + parent_id = resource_def.get_attr(resource_def.path_ids[level]) + # iterate over all objects in d, and look for resource type + for child in d: + if resource_type in child and child[resource_type]: + parent = child[resource_type] + # If resource type matches, check for id + if parent['id'] == parent_id: + if is_leaf: + return parent + if 'children' in parent: + return self._find_parent_in_dict( + parent['children'], resource_def, level + 1) + + # Parent not found - for now, raise an exception + # Support for this will come later + # TODO(annak): remove this when missing parent body is + # created on demand + raise NsxPolicyTransactionException( + "Transactional create is supported for infra level" + " objects and their children") + + def apply_defs(self): + # TODO(annak): find longest common URL, for now always + # applying on tenant level + + if not self.defs: + return + + self._sort_defs() + + top_def = self.defs[0] + url = top_def.get_resource_path() + body = {'resource_type': top_def.resource_type()} + # iterate over defs (except top level def) + for resource_def in self.defs[1:]: + parent_dict = None + if 'children' in body: + parent_dict = self._find_parent_in_dict(body['children'], + resource_def) + + if not parent_dict: + parent_dict = body + + if 'children' not in parent_dict: + parent_dict['children'] = [] + + resource_type = resource_def.resource_type() + parent_dict['children'].append({ + 'resource_type': 'Child%s' % resource_type, + resource_type: resource_def.get_obj_dict() + }) + + if body: + self.client.patch(url, body) + + @staticmethod + def get_current(): + if hasattr(NsxPolicyTransaction.data, 'instance'): + return NsxPolicyTransaction.data.instance