Loading src/device/service/drivers/mikrotik/MikrotikRouterOSDriver.py +40 −0 Original line number Diff line number Diff line Loading @@ -228,6 +228,16 @@ class MikrotikRouterOSDriver(_Driver): if "routing-table" in resource_value: payload["routing-table"] = str(resource_value["routing-table"]) # --- Simple Queue --- elif resource_key.startswith("/qos/queue/simple"): endpoint = "/queue/simple" payload = { "name": str(resource_value["name"]), "target": str(resource_value["target"]), "max-limit": str(resource_value["max-limit"]), "comment": str(resource_value.get("comment", "")), } # --- VRF --- elif resource_key.startswith("/ip/vrf"): endpoint = "/ip/vrf" Loading Loading @@ -451,6 +461,27 @@ class MikrotikRouterOSDriver(_Driver): patch_response.raise_for_status() results.append(True) continue if endpoint == "/queue/simple" and payload.get("name"): queue = next( (candidate for candidate in candidates if str(candidate.get("name", "")) == str(payload["name"])), None, ) candidate_id = ( queue.get(".id") or queue.get("id") or queue.get("number") ) if isinstance(queue, dict) else None if candidate_id: patch_url = f"{url}/{candidate_id}" patch_response = requests.patch( patch_url, json=payload, auth=self.__auth, timeout=self.__timeout, verify=False, ) patch_response.raise_for_status() results.append(True) continue except Exception: LOGGER.debug("Could not pre-check existing MikroTik resource for %s", url, exc_info=True) Loading Loading @@ -499,6 +530,8 @@ class MikrotikRouterOSDriver(_Driver): return 5 if resource_key.startswith("/ip/route"): return 5 if resource_key.startswith("/qos/queue/simple"): return 5 if resource_key.startswith("/routing/filter/rule"): return 6 if resource_key.startswith("/routing/bgp/template"): Loading Loading @@ -566,6 +599,13 @@ class MikrotikRouterOSDriver(_Driver): if "routing-table" in resource_value: payload["routing-table"] = str(resource_value["routing-table"]) # --- Simple Queue --- elif resource_key.startswith("/qos/queue/simple"): endpoint = "/queue/simple" payload = { "name": str(resource_value["name"]), } # --- VRF --- elif resource_key.startswith("/ip/vrf"): endpoint = "/ip/vrf" Loading src/nbi/service/ietf_l3vpn/Handlers.py +1 −0 Original line number Diff line number Diff line Loading @@ -58,6 +58,7 @@ def update_service_endpoint( vlan_tag : int, ipv4_address : str, neighbor_ipv4_address : str, ipv4_prefix_length : int, capacity_gbps : Optional[float] = None, e2e_latency_ms : Optional[float] = None, availability : Optional[float] = None, mtu : Optional[int] = None, service_input_bandwidth : Optional[int] = None, service_output_bandwidth : Optional[int] = None, static_routing : Optional[Dict[Tuple[str, int, int], str]] = None, context_uuid : Optional[str] = DEFAULT_CONTEXT_NAME, ) -> Optional[Exception]: Loading src/nbi/tests/data/ietf_l3vpn_req_mikrotik_leaf1_leaf4_jetsons.json 0 → 100644 +185 −0 Original line number Diff line number Diff line { "ietf-l3vpn-svc:l3vpn-svc": { "vpn-services": { "vpn-service": [ { "vpn-id": "svc-mikrotik-leaf1-leaf4-jetson-a-jetson-b" } ] }, "sites": { "site": [ { "site-id": "site_MikroTik-5-Leaf1-JetsonA", "management": { "type": "ietf-l3vpn-svc:provider-managed" }, "locations": { "location": [ { "location-id": "MikroTik-5-Leaf1" } ] }, "devices": { "device": [ { "device-id": "6ac9614b-6893-549d-b39c-d4af4320678a", "location": "MikroTik-5-Leaf1" } ] }, "routing-protocols": { "routing-protocol": [ { "type": "ietf-l3vpn-svc:static", "static": { "cascaded-lan-prefixes": { "ipv4-lan-prefixes": [ { "lan": "172.16.1.0/24", "lan-tag": "vlan10", "next-hop": "172.16.1.10" } ] } } } ] }, "site-network-accesses": { "site-network-access": [ { "site-network-access-id": "sfp-sfpplus6", "site-network-access-type": "ietf-l3vpn-svc:multipoint", "device-reference": "6ac9614b-6893-549d-b39c-d4af4320678a", "vpn-attachment": { "vpn-id": "svc-mikrotik-leaf1-leaf4-jetson-a-jetson-b", "site-role": "ietf-l3vpn-svc:hub-role" }, "ip-connection": { "ipv4": { "address-allocation-type": "ietf-l3vpn-svc:static-address", "addresses": { "provider-address": "172.16.1.254", "customer-address": "172.16.1.10", "prefix-length": 24 } } }, "service": { "svc-mtu": 1500, "svc-input-bandwidth": 100000000, "svc-output-bandwidth": 100000000, "qos": { "qos-profile": { "classes": { "class": [ { "class-id": "qos-realtime", "direction": "ietf-l3vpn-svc:both", "latency": { "latency-boundary": 10 }, "bandwidth": { "guaranteed-bw-percent": 100 } } ] } } } } } ] } }, { "site-id": "site_MikroTik-8-Leaf4-JetsonB", "management": { "type": "ietf-l3vpn-svc:provider-managed" }, "locations": { "location": [ { "location-id": "MikroTik-8-Leaf4" } ] }, "devices": { "device": [ { "device-id": "03d3c087-da28-539e-aae0-60da7b90bad6", "location": "MikroTik-8-Leaf4" } ] }, "routing-protocols": { "routing-protocol": [ { "type": "ietf-l3vpn-svc:static", "static": { "cascaded-lan-prefixes": { "ipv4-lan-prefixes": [ { "lan": "172.16.4.0/24", "lan-tag": "vlan10", "next-hop": "172.16.4.10" } ] } } } ] }, "site-network-accesses": { "site-network-access": [ { "site-network-access-id": "sfp-sfpplus6", "site-network-access-type": "ietf-l3vpn-svc:multipoint", "device-reference": "03d3c087-da28-539e-aae0-60da7b90bad6", "vpn-attachment": { "vpn-id": "svc-mikrotik-leaf1-leaf4-jetson-a-jetson-b", "site-role": "ietf-l3vpn-svc:spoke-role" }, "ip-connection": { "ipv4": { "address-allocation-type": "ietf-l3vpn-svc:static-address", "addresses": { "provider-address": "172.16.4.254", "customer-address": "172.16.4.10", "prefix-length": 24 } } }, "service": { "svc-mtu": 1500, "svc-input-bandwidth": 100000000, "svc-output-bandwidth": 100000000, "qos": { "qos-profile": { "classes": { "class": [ { "class-id": "qos-realtime", "direction": "ietf-l3vpn-svc:both", "latency": { "latency-boundary": 10 }, "bandwidth": { "guaranteed-bw-percent": 100 } } ] } } } } } ] } } ] } } } src/service/service/service_handlers/l3nm_mikrotik_routeros/ConfigRules.py +56 −3 Original line number Diff line number Diff line Loading @@ -53,6 +53,38 @@ def _route_rule(service_uuid: str, dst_address: str, gateway: str) -> Dict[str, } def _routeros_rate(bps: Any) -> str: value = int(bps or 0) if value <= 0: return "" units = ( (1_000_000_000, "G"), (1_000_000, "M"), (1_000, "k"), ) for divisor, suffix in units: if value >= divisor and value % divisor == 0: return f"{value // divisor}{suffix}" return str(value) def _queue_rule(service_uuid: str, customer_ip: str, input_bps: Any, output_bps: Any) -> Dict[str, Any]: upload_rate = _routeros_rate(input_bps) download_rate = _routeros_rate(output_bps) if not upload_rate and not download_rate: return {} if not upload_rate: upload_rate = download_rate if not download_rate: download_rate = upload_rate return { "name": f"tfs-l3vpn-{service_uuid[:8]}-{customer_ip.replace('.', '-')}", "target": f"{customer_ip}/32", "max-limit": f"{upload_rate}/{download_rate}", "comment": f"tfs-l3vpn:{service_uuid}:bandwidth", } def setup_config_rules( service_uuid: str, local_settings: Dict[str, Any], Loading @@ -65,6 +97,7 @@ def setup_config_rules( remote_transit = transit_b if local_index == 0 else transit_a local_provider = local_settings["neighbor_address"] local_customer = local_settings["ip_address"] remote_customer = remote_settings["ip_address"] local_prefix = int(local_settings["prefix_length"]) remote_prefix = int(remote_settings["prefix_length"]) Loading @@ -72,7 +105,7 @@ def setup_config_rules( transit_prefix = ipaddress.ip_network(DEFAULT_TRANSIT_CIDR, strict=False).prefixlen remote_lan = _network(remote_customer, remote_prefix) return [ rules = [ json_config_rule_set( f"/interfaces/ip/l3vpn[{service_uuid}]/gateway", _ip_address_rule(service_uuid, "gateway", local_provider, local_prefix, bridge), Loading @@ -86,6 +119,15 @@ def setup_config_rules( _route_rule(service_uuid, remote_lan, remote_transit), ), ] queue = _queue_rule( service_uuid, local_customer, local_settings.get("svc_input_bandwidth"), local_settings.get("svc_output_bandwidth"), ) if queue: rules.append(json_config_rule_set(f"/qos/queue/simple/l3vpn[{service_uuid}]", queue)) return rules def teardown_config_rules( Loading @@ -100,6 +142,7 @@ def teardown_config_rules( remote_transit = transit_b if local_index == 0 else transit_a local_provider = local_settings["neighbor_address"] local_customer = local_settings["ip_address"] remote_customer = remote_settings["ip_address"] local_prefix = int(local_settings["prefix_length"]) remote_prefix = int(remote_settings["prefix_length"]) Loading @@ -107,7 +150,16 @@ def teardown_config_rules( transit_prefix = ipaddress.ip_network(DEFAULT_TRANSIT_CIDR, strict=False).prefixlen remote_lan = _network(remote_customer, remote_prefix) return [ queue = _queue_rule( service_uuid, local_customer, local_settings.get("svc_input_bandwidth"), local_settings.get("svc_output_bandwidth"), ) rules = [] if queue: rules.append(json_config_rule_delete(f"/qos/queue/simple/l3vpn[{service_uuid}]", queue)) rules.extend([ json_config_rule_delete( f"/ip/route/l3vpn[{service_uuid}]/remote", _route_rule(service_uuid, remote_lan, remote_transit), Loading @@ -120,4 +172,5 @@ def teardown_config_rules( f"/interfaces/ip/l3vpn[{service_uuid}]/gateway", _ip_address_rule(service_uuid, "gateway", local_provider, local_prefix, bridge), ), ] ]) return rules src/tests/mikrotik_routeros/fabric-descriptor-spine1-spine2-leaf1-leaf4-evpn-vxlan-jetsons-static.json 0 → 100644 +380 −0 File added.Preview size limit exceeded, changes collapsed. Show changes Loading
src/device/service/drivers/mikrotik/MikrotikRouterOSDriver.py +40 −0 Original line number Diff line number Diff line Loading @@ -228,6 +228,16 @@ class MikrotikRouterOSDriver(_Driver): if "routing-table" in resource_value: payload["routing-table"] = str(resource_value["routing-table"]) # --- Simple Queue --- elif resource_key.startswith("/qos/queue/simple"): endpoint = "/queue/simple" payload = { "name": str(resource_value["name"]), "target": str(resource_value["target"]), "max-limit": str(resource_value["max-limit"]), "comment": str(resource_value.get("comment", "")), } # --- VRF --- elif resource_key.startswith("/ip/vrf"): endpoint = "/ip/vrf" Loading Loading @@ -451,6 +461,27 @@ class MikrotikRouterOSDriver(_Driver): patch_response.raise_for_status() results.append(True) continue if endpoint == "/queue/simple" and payload.get("name"): queue = next( (candidate for candidate in candidates if str(candidate.get("name", "")) == str(payload["name"])), None, ) candidate_id = ( queue.get(".id") or queue.get("id") or queue.get("number") ) if isinstance(queue, dict) else None if candidate_id: patch_url = f"{url}/{candidate_id}" patch_response = requests.patch( patch_url, json=payload, auth=self.__auth, timeout=self.__timeout, verify=False, ) patch_response.raise_for_status() results.append(True) continue except Exception: LOGGER.debug("Could not pre-check existing MikroTik resource for %s", url, exc_info=True) Loading Loading @@ -499,6 +530,8 @@ class MikrotikRouterOSDriver(_Driver): return 5 if resource_key.startswith("/ip/route"): return 5 if resource_key.startswith("/qos/queue/simple"): return 5 if resource_key.startswith("/routing/filter/rule"): return 6 if resource_key.startswith("/routing/bgp/template"): Loading Loading @@ -566,6 +599,13 @@ class MikrotikRouterOSDriver(_Driver): if "routing-table" in resource_value: payload["routing-table"] = str(resource_value["routing-table"]) # --- Simple Queue --- elif resource_key.startswith("/qos/queue/simple"): endpoint = "/queue/simple" payload = { "name": str(resource_value["name"]), } # --- VRF --- elif resource_key.startswith("/ip/vrf"): endpoint = "/ip/vrf" Loading
src/nbi/service/ietf_l3vpn/Handlers.py +1 −0 Original line number Diff line number Diff line Loading @@ -58,6 +58,7 @@ def update_service_endpoint( vlan_tag : int, ipv4_address : str, neighbor_ipv4_address : str, ipv4_prefix_length : int, capacity_gbps : Optional[float] = None, e2e_latency_ms : Optional[float] = None, availability : Optional[float] = None, mtu : Optional[int] = None, service_input_bandwidth : Optional[int] = None, service_output_bandwidth : Optional[int] = None, static_routing : Optional[Dict[Tuple[str, int, int], str]] = None, context_uuid : Optional[str] = DEFAULT_CONTEXT_NAME, ) -> Optional[Exception]: Loading
src/nbi/tests/data/ietf_l3vpn_req_mikrotik_leaf1_leaf4_jetsons.json 0 → 100644 +185 −0 Original line number Diff line number Diff line { "ietf-l3vpn-svc:l3vpn-svc": { "vpn-services": { "vpn-service": [ { "vpn-id": "svc-mikrotik-leaf1-leaf4-jetson-a-jetson-b" } ] }, "sites": { "site": [ { "site-id": "site_MikroTik-5-Leaf1-JetsonA", "management": { "type": "ietf-l3vpn-svc:provider-managed" }, "locations": { "location": [ { "location-id": "MikroTik-5-Leaf1" } ] }, "devices": { "device": [ { "device-id": "6ac9614b-6893-549d-b39c-d4af4320678a", "location": "MikroTik-5-Leaf1" } ] }, "routing-protocols": { "routing-protocol": [ { "type": "ietf-l3vpn-svc:static", "static": { "cascaded-lan-prefixes": { "ipv4-lan-prefixes": [ { "lan": "172.16.1.0/24", "lan-tag": "vlan10", "next-hop": "172.16.1.10" } ] } } } ] }, "site-network-accesses": { "site-network-access": [ { "site-network-access-id": "sfp-sfpplus6", "site-network-access-type": "ietf-l3vpn-svc:multipoint", "device-reference": "6ac9614b-6893-549d-b39c-d4af4320678a", "vpn-attachment": { "vpn-id": "svc-mikrotik-leaf1-leaf4-jetson-a-jetson-b", "site-role": "ietf-l3vpn-svc:hub-role" }, "ip-connection": { "ipv4": { "address-allocation-type": "ietf-l3vpn-svc:static-address", "addresses": { "provider-address": "172.16.1.254", "customer-address": "172.16.1.10", "prefix-length": 24 } } }, "service": { "svc-mtu": 1500, "svc-input-bandwidth": 100000000, "svc-output-bandwidth": 100000000, "qos": { "qos-profile": { "classes": { "class": [ { "class-id": "qos-realtime", "direction": "ietf-l3vpn-svc:both", "latency": { "latency-boundary": 10 }, "bandwidth": { "guaranteed-bw-percent": 100 } } ] } } } } } ] } }, { "site-id": "site_MikroTik-8-Leaf4-JetsonB", "management": { "type": "ietf-l3vpn-svc:provider-managed" }, "locations": { "location": [ { "location-id": "MikroTik-8-Leaf4" } ] }, "devices": { "device": [ { "device-id": "03d3c087-da28-539e-aae0-60da7b90bad6", "location": "MikroTik-8-Leaf4" } ] }, "routing-protocols": { "routing-protocol": [ { "type": "ietf-l3vpn-svc:static", "static": { "cascaded-lan-prefixes": { "ipv4-lan-prefixes": [ { "lan": "172.16.4.0/24", "lan-tag": "vlan10", "next-hop": "172.16.4.10" } ] } } } ] }, "site-network-accesses": { "site-network-access": [ { "site-network-access-id": "sfp-sfpplus6", "site-network-access-type": "ietf-l3vpn-svc:multipoint", "device-reference": "03d3c087-da28-539e-aae0-60da7b90bad6", "vpn-attachment": { "vpn-id": "svc-mikrotik-leaf1-leaf4-jetson-a-jetson-b", "site-role": "ietf-l3vpn-svc:spoke-role" }, "ip-connection": { "ipv4": { "address-allocation-type": "ietf-l3vpn-svc:static-address", "addresses": { "provider-address": "172.16.4.254", "customer-address": "172.16.4.10", "prefix-length": 24 } } }, "service": { "svc-mtu": 1500, "svc-input-bandwidth": 100000000, "svc-output-bandwidth": 100000000, "qos": { "qos-profile": { "classes": { "class": [ { "class-id": "qos-realtime", "direction": "ietf-l3vpn-svc:both", "latency": { "latency-boundary": 10 }, "bandwidth": { "guaranteed-bw-percent": 100 } } ] } } } } } ] } } ] } } }
src/service/service/service_handlers/l3nm_mikrotik_routeros/ConfigRules.py +56 −3 Original line number Diff line number Diff line Loading @@ -53,6 +53,38 @@ def _route_rule(service_uuid: str, dst_address: str, gateway: str) -> Dict[str, } def _routeros_rate(bps: Any) -> str: value = int(bps or 0) if value <= 0: return "" units = ( (1_000_000_000, "G"), (1_000_000, "M"), (1_000, "k"), ) for divisor, suffix in units: if value >= divisor and value % divisor == 0: return f"{value // divisor}{suffix}" return str(value) def _queue_rule(service_uuid: str, customer_ip: str, input_bps: Any, output_bps: Any) -> Dict[str, Any]: upload_rate = _routeros_rate(input_bps) download_rate = _routeros_rate(output_bps) if not upload_rate and not download_rate: return {} if not upload_rate: upload_rate = download_rate if not download_rate: download_rate = upload_rate return { "name": f"tfs-l3vpn-{service_uuid[:8]}-{customer_ip.replace('.', '-')}", "target": f"{customer_ip}/32", "max-limit": f"{upload_rate}/{download_rate}", "comment": f"tfs-l3vpn:{service_uuid}:bandwidth", } def setup_config_rules( service_uuid: str, local_settings: Dict[str, Any], Loading @@ -65,6 +97,7 @@ def setup_config_rules( remote_transit = transit_b if local_index == 0 else transit_a local_provider = local_settings["neighbor_address"] local_customer = local_settings["ip_address"] remote_customer = remote_settings["ip_address"] local_prefix = int(local_settings["prefix_length"]) remote_prefix = int(remote_settings["prefix_length"]) Loading @@ -72,7 +105,7 @@ def setup_config_rules( transit_prefix = ipaddress.ip_network(DEFAULT_TRANSIT_CIDR, strict=False).prefixlen remote_lan = _network(remote_customer, remote_prefix) return [ rules = [ json_config_rule_set( f"/interfaces/ip/l3vpn[{service_uuid}]/gateway", _ip_address_rule(service_uuid, "gateway", local_provider, local_prefix, bridge), Loading @@ -86,6 +119,15 @@ def setup_config_rules( _route_rule(service_uuid, remote_lan, remote_transit), ), ] queue = _queue_rule( service_uuid, local_customer, local_settings.get("svc_input_bandwidth"), local_settings.get("svc_output_bandwidth"), ) if queue: rules.append(json_config_rule_set(f"/qos/queue/simple/l3vpn[{service_uuid}]", queue)) return rules def teardown_config_rules( Loading @@ -100,6 +142,7 @@ def teardown_config_rules( remote_transit = transit_b if local_index == 0 else transit_a local_provider = local_settings["neighbor_address"] local_customer = local_settings["ip_address"] remote_customer = remote_settings["ip_address"] local_prefix = int(local_settings["prefix_length"]) remote_prefix = int(remote_settings["prefix_length"]) Loading @@ -107,7 +150,16 @@ def teardown_config_rules( transit_prefix = ipaddress.ip_network(DEFAULT_TRANSIT_CIDR, strict=False).prefixlen remote_lan = _network(remote_customer, remote_prefix) return [ queue = _queue_rule( service_uuid, local_customer, local_settings.get("svc_input_bandwidth"), local_settings.get("svc_output_bandwidth"), ) rules = [] if queue: rules.append(json_config_rule_delete(f"/qos/queue/simple/l3vpn[{service_uuid}]", queue)) rules.extend([ json_config_rule_delete( f"/ip/route/l3vpn[{service_uuid}]/remote", _route_rule(service_uuid, remote_lan, remote_transit), Loading @@ -120,4 +172,5 @@ def teardown_config_rules( f"/interfaces/ip/l3vpn[{service_uuid}]/gateway", _ip_address_rule(service_uuid, "gateway", local_provider, local_prefix, bridge), ), ] ]) return rules
src/tests/mikrotik_routeros/fabric-descriptor-spine1-spine2-leaf1-leaf4-evpn-vxlan-jetsons-static.json 0 → 100644 +380 −0 File added.Preview size limit exceeded, changes collapsed. Show changes