Commit 93f0c777 authored by Mohamad Rahhal's avatar Mohamad Rahhal
Browse files

MikrotikDriver:

-Added Delete support on the devices
parent 5cac3530
Loading
Loading
Loading
Loading
+246 −60
Original line number Diff line number Diff line
@@ -274,13 +274,6 @@ class MikrotikRouterOSDriver(_Driver):
                        )
        
                        if response.status_code >= 400:
                            if resource_key.startswith("/interfaces/ip") and "already have such address" in response.text:
                                LOGGER.info(
                                    "MikroTik already has address for %s; treating as success",
                                    payload.get("address"),
                                )
                                results.append(True)
                                continue
                            LOGGER.error("MikroTik Error (%s): %s", response.status_code, response.text)
        
                        response.raise_for_status()
@@ -296,56 +289,249 @@ class MikrotikRouterOSDriver(_Driver):


    
#    @metered_subclass_method(METRICS_POOL)
#    def DeleteConfig(self, resources: List[Tuple[str, Any]]) -> List[Union[bool, Exception]]:
#        LOGGER.info("[DeleteConfig] resources = {:s}".format(str(resources)))
#        
#        results = []
#        if not resources:
#            return results
#        with self.__lock:
#            for resource in resources:
#                try:
#                    resource_key, resource_value = resource
#
#                    if not resource_key.startswith("/device[") or not "/flow[" in resource_key:
#                        LOGGER.error(f"Invalid resource_key format: {resource_key}")
#                        results.append(Exception(f"Invalid resource_key format: {resource_key}"))
#                        continue
#
#                    try:
#                        resource_value_dict = json.loads(resource_value)
#                        LOGGER.debug('resource_value_dict = {:s}'.format(str(resource_value_dict)))
#                        dpid = int(resource_value_dict["dpid"], 16)
#                        in_port = int(resource_value_dict["in-port"].split("-")[1][3:])
#                        out_port = int(resource_value_dict["out-port"].split("-")[1][3:])
#                        ip_src_addr = resource_value_dict.get("ip_address_source", "")
#                        ip_dst_addr = resource_value_dict.get("ip_address_destination", "")
#
#                        if "h1-h3" in resource_key:
#                            priority = 1000
#                        elif "h3-h1" in resource_key:
#                            priority = 1000
#                        elif "h2-h4" in resource_key:
#                            priority = 1500
#                        elif "h4-h2" in resource_key:
#                            priority = 1500
#                        else:
#                            priority = 65535
#                    except (KeyError, ValueError, IndexError) as e:
#                        MSG = "Error processing resource {:s}"
#                        LOGGER.exception(MSG.format(str(resource)))
#                        results.append(e)
#                        continue
#
#                    results.append(self.rac.del_flow_rule(
#                        dpid, in_port, out_port, 0x0800, ip_src_addr, ip_dst_addr,
#                        priority=priority
#                    ))
#                except Exception as e:
#                    MSG = "Error processing resource {:s}"
#                    LOGGER.exception(MSG.format(str(resource)))
#                    results.append(e)
#
#        return results
#
 No newline at end of file
    @metered_subclass_method(METRICS_POOL)
    def DeleteConfig(self, resources: List[Tuple[str, Any]]) -> List[Union[bool, Exception]]:
            results: List[Union[bool, Exception]] = []
            if not resources:
                return results

            def resource_delete_priority(resource_key: str) -> int:
                if resource_key.startswith("/network_instances/bgp_session"):
                    return 0
                if resource_key.startswith("/interfaces/ip"):
                    return 1
                if resource_key.startswith("/network_instances/bgp_instance"):
                    return 2
                return 10

            with self.__lock:
                for resource in sorted(resources, key=lambda item: resource_delete_priority(item[0])):
                    try:
                        resource_key, resource_value = resource
                        if isinstance(resource_value, str):
                            resource_value = json.loads(resource_value)

                        payload = {}
                        endpoint = ""

                        # --- IP Address ---
                        if resource_key.startswith("/interfaces/ip"):
                            endpoint = "/ip/address"
                            payload = {
                                "address": str(resource_value["address"]),
                                "interface": str(resource_value["interface"]),
                                "comment": str(resource_value.get("comment", ""))
                            }

                        # --- BGP Instance  ---
                        elif resource_key.startswith("/network_instances/bgp_instance"):
                            endpoint = "/routing/bgp/instance"
                            payload = {
                                "name": str(resource_value["name"]),
                                "as": int(resource_value["as"]),
                                "router-id": str(resource_value["router_id"])
                            }

                        # --- BGP Connection  ---
                        elif resource_key.startswith("/network_instances/bgp_session"):
                            endpoint = "/routing/bgp/connection"
                            if "name" in resource_value:
                                payload["name"] = str(resource_value["name"])
                            if "local.address" in resource_value:
                                payload["local.address"] = resource_value["local.address"]
                            if "remote.address" in resource_value:
                                payload["remote.address"] = resource_value["remote.address"]
                            if "remote.as" in resource_value:
                                payload["remote.as"] = resource_value["remote.as"]
                            if "routing-table" in resource_value:
                                payload["routing-table"] = resource_value["routing-table"]
                            if "local.role" in resource_value:
                                payload["local.role"] = resource_value["local.role"]
                            if "multihop" in resource_value:
                                payload["multihop"] = resource_value["multihop"]
                            if "afi" in resource_value:
                                payload["afi"] = resource_value["afi"]

                        # ---VXLAN Interface ---
                        elif resource_key.startswith("/interfaces/vxlan"):
                            endpoint = "/interface/vxlan"
                            if "name" in resource_value:
                                payload["name"] = str(resource_value["name"])
                            if "vni" in resource_value:
                                payload["vni"] = int(resource_value["vni"])
                            if "local-address" in resource_value:
                                payload["local-address"] = str(resource_value["local-address"])
                            if "port" in resource_value:
                                payload["port"] = int(resource_value.get("port", 4789))

                        # --- Bridge Port (Add VXLAN/Server ports to Bridge) ---
                        elif resource_key.startswith("/interfaces/bridge/port"):
                            endpoint = "/interface/bridge/port"
                            if "bridge" in resource_value and "interface" in resource_value:
                                payload = {
                                    "bridge": str(resource_value["bridge"]),
                                    "interface": str(resource_value["interface"])
                                }

                        # --- BGP EVPN Mapping ---
                        elif resource_key.startswith("/network_instances/bgp_evpn"):
                            endpoint = "/routing/bgp/evpn"
                            if "name" in resource_value:
                                payload["name"] = str(resource_value["name"])
                            if "instance" in resource_value:
                                payload["instance"] = str(resource_value["instance"])
                            if "vni" in resource_value:
                                payload["vni"] = int(resource_value["vni"])

                        # --- OSPF Instance ---
                        elif resource_key.startswith("/network_instances/ospf_instance"):
                            endpoint = "/routing/ospf/instance"
                            payload = {
                                "name": str(resource_value["name"]),
                                "router-id": str(resource_value["router-id"]),
                                "version": int(resource_value.get("version", 2))
                            }

                        # --- OSPF Area ---
                        elif resource_key.startswith("/network_instances/ospf_area"):
                            endpoint = "/routing/ospf/area"
                            payload = {
                                "name": str(resource_value["name"]),
                                "instance": str(resource_value["instance"]),
                                "area-id": str(resource_value.get("area-id", "0.0.0.0"))
                            }

                        # --- OSPF Interface Template ---
                        elif resource_key.startswith("/network_instances/ospf_interface"):
                            endpoint = "/routing/ospf/interface-template"
                            payload = {
                                "interfaces": [str(resource_value["interface"])],
                                "area": str(resource_value.get("area", "backbone"))
                            }

                        # --- IS-IS Instance ---
                        elif resource_key.startswith("/network_instances/isis_instance"):
                            endpoint = "/routing/isis/instance"
                            payload = {
                                "name": str(resource_value["name"]),
                                "areas": str(resource_value["areas"]),
                                "system-id": str(resource_value["system-id"])
                            }

                        # --- IS-IS Interface Template ---
                        elif resource_key.startswith("/network_instances/isis_interface"):
                            endpoint = "/routing/isis/interface-template"
                            level_val = resource_value.get("levels", "l2")
                            payload = {
                                "instance": str(resource_value["instance"]),
                                "interfaces": [str(resource_value["interface"])],
                                "levels": [str(level_val)]
                            }
                        else:
                            raise NotImplementedError(f"Resource not implemented: {resource_key}")

                        url = f"{self.__base_url}{endpoint}"
                        LOGGER.info("DELETE %s", url)

                        response = requests.get(
                            url,
                            auth=self.__auth,
                            timeout=self.__timeout,
                            verify=False,
                        )
                        if response.status_code >= 400:
                            LOGGER.error("MikroTik Error (%s): %s", response.status_code, response.text)
                            response.raise_for_status()

                        candidates = response.json()
                        if not isinstance(candidates, list):
                            candidates = [candidates]

                        def candidate_matches(candidate: dict, payload: dict) -> bool:
                            # Flexible matching: RouterOS may use dots or dashes in keys
                            # and may stringify numbers. Try several key variants.
                            if not isinstance(candidate, dict):
                                return False
                            for key, value in payload.items():
                                found = False
                                variants = [
                                    key,
                                    key.replace('.', '-'),
                                    key.replace('.', '_'),
                                    key.replace('-', '.'),
                                    key.replace('-', '_'),
                                ]
                                for vkey in variants:
                                    if vkey in candidate and str(candidate.get(vkey)) == str(value):
                                        found = True
                                        break
                                if not found:
                                    # Additionally, try relaxed matching for common BGP fields
                                    # e.g., compare local/remote addresses by suffix match
                                    if key.endswith('address'):
                                        for vkey in variants:
                                            cv = candidate.get(vkey)
                                            if cv is None:
                                                continue
                                            if str(cv).endswith(str(value)) or str(value).endswith(str(cv)):
                                                found = True
                                                break
                                if not found:
                                    return False
                            return True

                        matched_candidates = [c for c in candidates if candidate_matches(c, payload)]

                        if len(matched_candidates) == 0:
                            LOGGER.info("No matching MikroTik resource found for delete %s", payload)
                            results.append(True)
                            continue

                        delete_errors = []
                        for candidate in matched_candidates:
                            candidate_id = candidate.get('.id') or candidate.get('id') or candidate.get('number')
                            if not candidate_id:
                                delete_errors.append(Exception(
                                    f"Could not determine MikroTik item id for delete: {candidate}"
                                ))
                                continue

                            delete_targets = [f"{url}/{candidate_id}"]
                            if str(candidate_id).startswith('*'):
                                delete_targets.append(f"{url}/{str(candidate_id).lstrip('*')}")

                            deleted = False
                            last_error = None
                            for delete_url in delete_targets:
                                try:
                                    delete_response = requests.delete(
                                        delete_url,
                                        auth=self.__auth,
                                        timeout=self.__timeout,
                                        verify=False,
                                    )
                                    if delete_response.status_code in (200, 202, 204, 404):
                                        deleted = True
                                        break
                                    last_error = Exception(
                                        f"DELETE {delete_url} returned {delete_response.status_code}: {delete_response.text}"
                                    )
                                except Exception as e:
                                    last_error = e

                            if not deleted:
                                delete_errors.append(last_error or Exception("Unknown MikroTik delete failure"))

                        if len(delete_errors) > 0:
                            for error in delete_errors:
                                LOGGER.error("MikroTik delete error for %s: %s", resource_key, error)
                            results.append(delete_errors[0])
                        else:
                            results.append(True)

                    except Exception as e:
                        LOGGER.exception("Error processing delete resource %s", resource_key)
                        results.append(e)

            return results
    
 No newline at end of file