From e01072a0c723cdd5762c07012ffe02574f841cdf Mon Sep 17 00:00:00 2001 From: Sven Rosenzweig Date: Tue, 27 Feb 2024 14:33:13 +0100 Subject: [PATCH] Segment Ports QoS Profile Mappings support --- networking_nsxv3/api/rpc.py | 6 +- .../nsxv3/agent/provider_nsx_policy.py | 75 ++++++++++++++----- networking_nsxv3/tests/e2e/README.md | 15 ++++ .../functional/test_realization_brownfield.py | 59 ++++++++++++--- 4 files changed, 122 insertions(+), 33 deletions(-) create mode 100644 networking_nsxv3/tests/e2e/README.md diff --git a/networking_nsxv3/api/rpc.py b/networking_nsxv3/api/rpc.py index 76300aa..228c97b 100644 --- a/networking_nsxv3/api/rpc.py +++ b/networking_nsxv3/api/rpc.py @@ -50,18 +50,18 @@ def get_network_bridge(self, current, network_segments, network_current, host): ) def create_policy(self, context, policy): - LOG.debug("All gents. Creating policy={}.".format(policy.name)) + LOG.debug("All gents. Creating policy={}.".format(policy["id"])) return self._get_call_context().cast( self.context, 'create_policy', policy=policy) def update_policy(self, context, policy): - LOG.debug("All gents. Updating policy={}.".format(policy.name)) + LOG.debug("All gents. Updating policy={}.".format(policy["id"])) if (hasattr(policy, "rules")): return self._get_call_context().cast( self.context, 'update_policy', policy=policy) def delete_policy(self, context, policy): - LOG.debug("All gents. Deleting policy={}.".format(policy.name)) + LOG.debug("All gents. Deleting policy={}.".format(policy["id"])) return self._get_call_context().cast( self.context, 'delete_policy', policy=policy) diff --git a/networking_nsxv3/plugins/ml2/drivers/nsxv3/agent/provider_nsx_policy.py b/networking_nsxv3/plugins/ml2/drivers/nsxv3/agent/provider_nsx_policy.py index a248516..6846c4c 100644 --- a/networking_nsxv3/plugins/ml2/drivers/nsxv3/agent/provider_nsx_policy.py +++ b/networking_nsxv3/plugins/ml2/drivers/nsxv3/agent/provider_nsx_policy.py @@ -72,18 +72,12 @@ class API(object): SEARCH_Q_SEG_PORTS = {"query": "resource_type:SegmentPort AND marked_for_delete:false"} SEARCH_Q_QOS_PROFILES = { "query": "resource_type:QoSProfile AND NOT display_name:*default* AND marked_for_delete:false"} - SEARCH_Q_ALL_SEG_PROFILES = { - "query": - "resource_type:QoSProfile" + - " OR resource_type:SpoofGuardProfile" + - " OR resource_type:SegmentSecurityProfile" + - " OR resource_type:PortMirroringProfile" + - " OR resource_type:MacDiscoveryProfile" + - " OR resource_type:IPDiscoveryProfile" - } + SEARCH_Q_QOS_BIND = "resource_type:PortQoSProfileBindingMap AND qos_profile_path:\"/infra/qos-profiles/{}\"" + SEARCH_Q_QOS_BIND_BY_PPATH = "resource_type:PortQoSProfileBindingMap AND parent_path:\"{}\"" SEARCH_DSL = POLICY_BASE + "/search" - SEARCH_DSL_QUERY = lambda res_type, dsl: { + + def SEARCH_DSL_QUERY(res_type, dsl): return { "query": f"resource_type:{res_type}", "dsl": f"{dsl}", "data_source": "INTENT", @@ -96,6 +90,7 @@ class API(object): SEGMENT_PORTS = INFRA + "/segments/{}/ports" SEGMENT_PORT_PATH = "/infra/segments/{}/ports/{}" SEGMENT_PORT = POLICY_BASE + SEGMENT_PORT_PATH + SEGMENT_PORT_QOS = SEGMENT_PORT + "/port-qos-profile-binding-maps/{}" GROUP_PATH = "/infra/domains/default/groups/{}" GROUPS = INFRA + "/domains/default/groups" @@ -355,9 +350,15 @@ def segment_port(self, os_port, provider_port) -> dict: return segment_port - def qos_profile_binding(self) -> dict: - # TODO: QOS Profile Binding - return {} + def qos_profile_binding(self, qos_id: str, nsx_seg_id: str, nsx_port_id: str) -> dict: + return { + "qos_profile_path": f"/infra/qos-profiles/{qos_id}", + "resource_type": "PortQoSProfileBindingMap", + "id": qos_id, + "display_name": qos_id, + "path": f"/infra/segments/{nsx_seg_id}/ports/{nsx_port_id}/port-qos-profile-binding-maps/{qos_id}", + "parent_path": f"/infra/segments/{nsx_seg_id}/ports/{nsx_port_id}" + } # NSX-T Group Members def sg_members_container(self, os_sg: dict, provider_sg: dict) -> dict: @@ -884,7 +885,8 @@ def address_group_realize(self, os_ag, delete=False): def _clear_all_static_memberships_for_port(self, port_meta: PolicyResourceMeta): # Get all SGs where the port might have been a static member - grps:List[dict] = self.client.get_all(path=API.SEARCH_DSL, params=API.SEARCH_DSL_QUERY("Group", port_meta.real_id)) + grps: List[dict] = self.client.get_all( + path=API.SEARCH_DSL, params=API.SEARCH_DSL_QUERY("Group", port_meta.real_id)) if len(grps) > 0: # Remove the port path from the SGs PathExpressions LOG.info("Removing static member's port.path '%s' from %s SGs", port_meta.path, len(grps)) @@ -966,7 +968,40 @@ def port_realize(self, os_port: dict, delete=False): LOG.info("Port: %s has %s security groups which is more than the maximum allowed %s. \ The port will be added to the security groups as a static member.", port_id, len(port_sgs), max_sg_tags) os_port["security_groups"] = None - return self._realize(Provider.PORT, False, self.payload.segment_port, os_port, provider_port) + + updated_port_meta = self._realize(Provider.PORT, False, self.payload.segment_port, os_port, provider_port) + self.realize_qos_profile_binding(segment_meta=segment_meta, port_meta=updated_port_meta, os_port=os_port) + + return updated_port_meta + + def realize_qos_profile_binding(self, segment_meta: PolicyResourceMeta, port_meta: PolicyResourceMeta, os_port: dict): + if not segment_meta or not port_meta: + LOG.debug("QoS Profile Binding Segment: '%s', Port: '%s'", segment_meta, port_meta) + LOG.info("Skipping QoS Profile Binding for Port:%s", os_port.get("id")) + return + + os_qos_id = os_port.get("qos_policy_id") + + try: + if os_qos_id: + qos_meta = self.metadata(Provider.QOS, os_qos_id) + if not qos_meta: + LOG.warning("Not found. QoS:%s for Port:%s. QoS Profile Binding skipped.", + os_qos_id, os_port.get("id")) + return + qos_bind = self.payload.qos_profile_binding(qos_meta.real_id, segment_meta.real_id, port_meta.real_id) + api_path = API.SEGMENT_PORT_QOS.format(segment_meta.real_id, port_meta.real_id, qos_meta.real_id) + self.client.patch(api_path, data=qos_bind) + else: + qos_maps = self.client.get_all( + API.SEARCH_QUERY, {"query": API.SEARCH_Q_QOS_BIND_BY_PPATH.format(port_meta.path)}) + for qm in qos_maps: + self.client.delete(API.POLICY_BASE + qm["path"]) + except Exception as e: + b = "bind" if os_qos_id else "unbind" + LOG.warning(f"Unable to {b} a QOS: '{os_qos_id}' for Port: '{os_port.get('id')}'") + LOG.debug(e) + def get_port(self, os_id): port = self.client.get_unique(path=API.SEARCH_QUERY, params={"query": API.SEARCH_Q_SEG_PORT.format(os_id)}) if port: @@ -993,11 +1028,6 @@ def network_realize(self, segmentation_id: int) -> PolicyResourceMeta: segment = self._realize(Provider.NETWORK, False, self.payload.segment, os_net, provider_net) return segment - def get_non_default_switching_profiles(self) -> list: - prfls = self.client.get_all(path=API.SEARCH_QUERY, params=API.SEARCH_Q_ALL_SEG_PROFILES) - # filter the list - return [p for p in prfls if p and p.get("id").find("default") == -1] - # overrides def sg_rules_realize(self, os_sg, delete=False, logged=False): os_id = os_sg.get("id") @@ -1016,6 +1046,11 @@ def qos_realize(self, qos: dict, delete=False): meta = self.metadata(Provider.QOS, qos_id) provider_o = {"id": qos_id, "_revision": None} if not meta else { "id": meta.real_id, "_revision": meta.revision} + if delete and meta: + qos_maps: list[Dict] = self.client.get_all( + API.SEARCH_QUERY, {"query": API.SEARCH_Q_QOS_BIND.format(meta.real_id)}) + for qm in qos_maps: + self.client.delete(API.POLICY_BASE + qm["path"]) return self._realize(Provider.QOS, delete, self.payload.qos, qos, provider_o) def sg_members_realize(self, os_sg: dict, delete=False): diff --git a/networking_nsxv3/tests/e2e/README.md b/networking_nsxv3/tests/e2e/README.md new file mode 100644 index 0000000..45e5e02 --- /dev/null +++ b/networking_nsxv3/tests/e2e/README.md @@ -0,0 +1,15 @@ +# End-to-End test cases +## Address Groups + * Create IPv4 address groups + * Create IPv6 address groups + * Create mixed IPv4/IPv6 address groups + * Update address groups + * Membership of address group in multiple Security Groups + * Delete address groups + +## Security Groups + * TODO +## Ports + * TODO +## QoS + * TODO \ No newline at end of file diff --git a/networking_nsxv3/tests/functional/test_realization_brownfield.py b/networking_nsxv3/tests/functional/test_realization_brownfield.py index 4aabc92..d8150f7 100644 --- a/networking_nsxv3/tests/functional/test_realization_brownfield.py +++ b/networking_nsxv3/tests/functional/test_realization_brownfield.py @@ -6,7 +6,7 @@ from neutron.tests import base from networking_nsxv3.tests.environment import Environment from networking_nsxv3.tests.datasets import coverage -from networking_nsxv3.plugins.ml2.drivers.nsxv3.agent import provider_nsx_policy +from networking_nsxv3.plugins.ml2.drivers.nsxv3.agent import provider_nsx_policy as pp import copy import os import re @@ -225,7 +225,7 @@ def end_to_end_test_generator(): yield 11 @staticmethod - def _assert_create(os_inventory, environment): + def _assert_create(os_inventory: coverage, environment: Environment): c = os_inventory mgmt_meta, plcy_meta = environment.dump_provider_inventory(printable=False) m = {**mgmt_meta, **plcy_meta} @@ -240,6 +240,45 @@ def _assert_create(os_inventory, environment): TestAgentRealizer.instance.assertEquals(c.QOS_INTERNAL["id"] in m[p.QOS]["meta"], True) TestAgentRealizer.instance.assertEquals(c.QOS_EXTERNAL["id"] in m[p.QOS]["meta"], True) TestAgentRealizer.instance.assertEquals(c.QOS_NOT_REFERENCED["id"] in m[p.QOS]["meta"], False) + + # Validate QoS Bindings + internal_qos_id = m[p.QOS]["meta"][c.QOS_INTERNAL["id"]]["id"] + internal_qos_meta = m[p.QOS]["meta"][c.QOS_INTERNAL["id"]] + internal_port_meta = m[p.PORT]["meta"][c.PORT_FRONTEND_INTERNAL["id"]] + + external_qos_id = m[p.QOS]["meta"][c.QOS_EXTERNAL["id"]]["id"] + external_qos_meta = m[p.QOS]["meta"][c.QOS_EXTERNAL["id"]] + external_port_meta = m[p.PORT]["meta"][c.PORT_FRONTEND_EXTERNAL["id"]] + + internal_qos_query = pp.API.SEARCH_Q_QOS_BIND.format(internal_qos_id) + internal_qos_mappings = p.client.get_all(path=pp.API.SEARCH_QUERY, params={"query": internal_qos_query}) + + external_qos_query = pp.API.SEARCH_Q_QOS_BIND.format(external_qos_id) + external_qos_mappings = p.client.get_all(path=pp.API.SEARCH_QUERY, params={"query": external_qos_query}) + + internal_qos_data = { + "display_name": internal_qos_meta["real_id"], + "id": internal_qos_meta["real_id"], + "marked_for_delete": False, + "parent_path": internal_port_meta["path"], + "path": internal_port_meta["path"] + f"/port-qos-profile-binding-maps/{internal_qos_id}", + "qos_profile_path": internal_qos_meta["path"], + "resource_type": "PortQoSProfileBindingMap" + } + external_qos_data = { + "display_name": external_qos_meta["real_id"], + "id": external_qos_meta["real_id"], + "marked_for_delete": False, + "parent_path": external_port_meta["path"], + "path": external_port_meta["path"] + f"/port-qos-profile-binding-maps/{external_qos_id}", + "qos_profile_path": external_qos_meta["path"], + "resource_type": "PortQoSProfileBindingMap" + } + + TestAgentRealizer.instance.assertEqual(1, len(external_qos_mappings)) + TestAgentRealizer.instance.assertEqual(1, len(internal_qos_mappings)) + TestAgentRealizer.instance.assertDictSupersetOf(external_qos_data, external_qos_mappings[0]) + TestAgentRealizer.instance.assertDictSupersetOf(internal_qos_data, internal_qos_mappings[0]) # Validate Security Groups Members TestAgentRealizer.instance.assertEquals(c.SECURITY_GROUP_FRONTEND["id"] in m[p.SG_MEMBERS]["meta"], True) @@ -275,7 +314,7 @@ def _assert_create(os_inventory, environment): TestAgentRealizer.instance.assertEquals("0.0.0.0/" in id or "::/" in id, True) @staticmethod - def _assert_update(os_inventory, environment): + def _assert_update(os_inventory: coverage, environment: Environment): c = os_inventory mgmt_meta, plcy_meta = environment.dump_provider_inventory(printable=False) m = {**mgmt_meta, **plcy_meta} @@ -329,7 +368,7 @@ def _assert_update(os_inventory, environment): TestAgentRealizer.instance.assertEquals("0.0.0.0/" in id or "::/" in id, True) params = {"default_service": False} # User services only - services = p.client.get_all(path=provider_nsx_policy.API.SERVICES, params=params) + services = p.client.get_all(path=pp.API.SERVICES, params=params) services = [s for s in services if not s.get("is_default")] TestAgentRealizer.instance.assertEquals(len(services), 0) @@ -344,14 +383,14 @@ def _pollute(env, index): ipv4_id = re.sub(r"\.|:|\/", "-", ipv4) ipv6_id = re.sub(r"\.|:|\/", "-", ipv6) - pp = provider_nsx_policy.Payload() - api = provider_nsx_policy.API + ppp = pp.Payload() + api = pp.API - p.client.put(path=api.GROUP.format(ipv4_id), data=pp.sg_rule_remote(ipv4)) - p.client.put(path=api.GROUP.format(ipv6_id), data=pp.sg_rule_remote(ipv6)) + p.client.put(path=api.GROUP.format(ipv4_id), data=ppp.sg_rule_remote(ipv4)) + p.client.put(path=api.GROUP.format(ipv6_id), data=ppp.sg_rule_remote(ipv6)) - p.client.put(path=api.GROUP.format(_id), data=pp.sg_members_container({"id": _id}, dict())) - data = pp.sg_rules_container({"id": _id}, {"rules": [], "scope": _id}) + p.client.put(path=api.GROUP.format(_id), data=ppp.sg_members_container({"id": _id}, dict())) + data = ppp.sg_rules_container({"id": _id}, {"rules": [], "scope": _id}) p.client.put(path=api.POLICY.format(_id), data=data)