Loading src/device/service/drivers/mikrotik/MikrotikRouterOSDriver.py +246 −60 Original line number Diff line number Diff line Loading @@ -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() Loading @@ -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 Loading
src/device/service/drivers/mikrotik/MikrotikRouterOSDriver.py +246 −60 Original line number Diff line number Diff line Loading @@ -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() Loading @@ -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